Add profile details and avatar uploads
This commit is contained in:
parent
8ab83205dd
commit
e91f552c60
12 changed files with 508 additions and 10 deletions
|
|
@ -5,6 +5,7 @@ import { StatusToastBridge } from "@/components/feedback/status-toast-bridge";
|
|||
import { AppShell } from "@/components/navigation/app-shell";
|
||||
import { PageIntro } from "@/components/navigation/page-intro";
|
||||
import { EnergyMeterCard } from "@/components/planning/energy-meter-card";
|
||||
import { ProfileAvatar } from "@/components/profile/profile-avatar";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -116,13 +117,31 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Profiel
|
||||
</p>
|
||||
<CardTitle className="text-lg text-foreground">{profileTitle}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardContent className="space-y-4 pb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<ProfileAvatar
|
||||
avatarUrl={profile.avatarUrl}
|
||||
displayName={profile.displayName}
|
||||
email={profile.email}
|
||||
size="md"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg text-foreground">{profileTitle}</CardTitle>
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
{profile.tagline ?? "Nog geen korte profielregel toegevoegd."}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
Taal `{profile.locale}` en timezone `{profile.timezone}` staan nu per
|
||||
gebruiker opgeslagen.
|
||||
</CardDescription>
|
||||
{profile.bio ? (
|
||||
<CardDescription className="whitespace-pre-line text-sm leading-7 text-muted-foreground">
|
||||
{profile.bio}
|
||||
</CardDescription>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,58 @@
|
|||
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 {
|
||||
isAllowedProfileAvatarMimeType,
|
||||
PROFILE_AVATAR_MAX_BYTES,
|
||||
} from "@/lib/profile/avatar";
|
||||
import { saveSettingsForCurrentUser } from "@/lib/profile/service";
|
||||
import type { 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_MAX_BYTES ||
|
||||
!isAllowedProfileAvatarMimeType(value.type)
|
||||
) {
|
||||
throw new FormDataValidationError("invalid-avatar-file");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildSettingsSubmission(formData: FormData): SettingsSubmission {
|
||||
const morningReminderEnabled = getBooleanValue(
|
||||
|
|
@ -28,6 +69,25 @@ function buildSettingsSubmission(formData: FormData): SettingsSubmission {
|
|||
);
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
|||
import { StatusToastBridge } from "@/components/feedback/status-toast-bridge";
|
||||
import { AppShell } from "@/components/navigation/app-shell";
|
||||
import { PageIntro } from "@/components/navigation/page-intro";
|
||||
import { ProfileAvatar } from "@/components/profile/profile-avatar";
|
||||
import { SettingsForm } from "@/components/settings/settings-form";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -83,12 +84,30 @@ export default async function SettingsPage({ searchParams }: SettingsPageProps)
|
|||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Account
|
||||
</p>
|
||||
<CardTitle className="text-lg text-foreground">{profileTitle}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardContent className="space-y-4 pb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<ProfileAvatar
|
||||
avatarUrl={profileBundle.profile.avatarUrl}
|
||||
displayName={profileBundle.profile.displayName}
|
||||
email={profileBundle.profile.email}
|
||||
size="md"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg text-foreground">{profileTitle}</CardTitle>
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
{profileBundle.profile.tagline ?? "Nog geen 1-regelige profielregel."}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
E-mailadres: {profileBundle.profile.email ?? authState.email ?? "Onbekend"}
|
||||
</CardDescription>
|
||||
{profileBundle.profile.bio ? (
|
||||
<CardDescription className="whitespace-pre-line text-sm leading-7 text-muted-foreground">
|
||||
{profileBundle.profile.bio}
|
||||
</CardDescription>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue