inspannings-monitor/app/settings/actions.ts
Madhura68 0bf6b96687 Add server-side avatar processing and responsive bottom nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 22:06:41 +02:00

179 lines
4.4 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { buildPathWithQuery } from "@/lib/auth/navigation";
import {
assertMaxLength,
FormDataValidationError,
getBooleanValue,
getEnumValue,
getOptionalString,
getOptionalTimeValue,
} from "@/lib/forms/parse";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import { ProfileAvatarProcessingError } from "@/lib/profile/avatar-processing";
import {
isAllowedProfileAvatarMimeType,
PROFILE_AVATAR_UPLOAD_MAX_BYTES,
} from "@/lib/profile/avatar";
import {
saveProfileAvatarForCurrentUser,
saveSettingsForCurrentUser,
} from "@/lib/profile/service";
import type {
AvatarUploadActionState,
SettingsSubmission,
} from "@/lib/profile/types";
const LOCALE_VALUES = ["nl-NL"] as const;
const ONBOARDING_TIMEZONE_VALUES = ONBOARDING_TIMEZONE_OPTIONS.map((option) => option.value);
const MAX_DISPLAY_NAME_LENGTH = 80;
const MAX_TAGLINE_LENGTH = 160;
const MAX_BIO_LENGTH = 2000;
function getOptionalBoundedString(
formData: FormData,
key: string,
maximumLength: number,
errorCode: string,
) {
const value = getOptionalString(formData, key);
if (value === null) {
return null;
}
return assertMaxLength(value, maximumLength, errorCode);
}
function getOptionalAvatarFile(formData: FormData) {
const value = formData.get("avatar");
if (!(value instanceof File) || value.size === 0) {
return null;
}
if (
value.size > PROFILE_AVATAR_UPLOAD_MAX_BYTES ||
!isAllowedProfileAvatarMimeType(value.type)
) {
throw new FormDataValidationError("invalid-avatar-file");
}
return value;
}
function buildSettingsSubmission(formData: FormData): SettingsSubmission {
const morningReminderEnabled = getBooleanValue(
formData,
"morningReminderEnabled",
"invalid-settings-input",
);
const reminderTime = getOptionalTimeValue(
formData,
"morningReminderTime",
"invalid-settings-input",
);
return {
displayName: getOptionalBoundedString(
formData,
"displayName",
MAX_DISPLAY_NAME_LENGTH,
"invalid-settings-input",
),
tagline: getOptionalBoundedString(
formData,
"tagline",
MAX_TAGLINE_LENGTH,
"invalid-settings-input",
),
bio: getOptionalBoundedString(
formData,
"bio",
MAX_BIO_LENGTH,
"invalid-settings-input",
),
avatarFile: getOptionalAvatarFile(formData),
locale: getEnumValue(formData, "locale", LOCALE_VALUES, "invalid-settings-input"),
timezone: getEnumValue(
formData,
"timezone",
ONBOARDING_TIMEZONE_VALUES,
"invalid-settings-input",
),
morningReminderEnabled,
morningReminderTime: morningReminderEnabled ? reminderTime : null,
reflectionReminderEnabled: getBooleanValue(
formData,
"reflectionReminderEnabled",
"invalid-settings-input",
),
showEnergyPoints: getBooleanValue(
formData,
"showEnergyPoints",
"invalid-settings-input",
),
};
}
export async function uploadAvatarAction(
_previousState: AvatarUploadActionState,
formData: FormData,
): Promise<AvatarUploadActionState> {
try {
const avatarFile = getOptionalAvatarFile(formData);
if (!avatarFile) {
throw new FormDataValidationError("invalid-avatar-file");
}
await saveProfileAvatarForCurrentUser(avatarFile);
revalidatePath("/settings");
revalidatePath("/dashboard");
return {
status: "success",
code: "avatar-saved",
};
} catch (error) {
if (error instanceof FormDataValidationError) {
return {
status: "error",
code: error.code,
};
}
if (error instanceof ProfileAvatarProcessingError) {
return {
status: "error",
code: "invalid-avatar-file",
};
}
throw error;
}
}
export async function saveSettingsAction(
_previousState: null,
formData: FormData,
): Promise<null> {
try {
await saveSettingsForCurrentUser(buildSettingsSubmission(formData));
} catch (error) {
if (error instanceof FormDataValidationError) {
redirect(buildPathWithQuery("/settings", { error: error.code }));
}
if (error instanceof ProfileAvatarProcessingError) {
redirect(buildPathWithQuery("/settings", { error: "invalid-avatar-file" }));
}
throw error;
}
redirect(buildPathWithQuery("/settings", { status: "saved" }));
return null;
}