feat: user settings and profile pages

This commit is contained in:
Bart van der Braak 2024-02-21 03:12:41 +01:00
parent a50f2c12a8
commit 68ae0e7d1e
13 changed files with 122 additions and 302 deletions

View file

@ -34,8 +34,9 @@
</DropdownMenu.Label>
<DropdownMenu.Group>
<DropdownMenu.Item href="/settings">Profile</DropdownMenu.Item>
<DropdownMenu.Item href="/settings/account">Account</DropdownMenu.Item>
<DropdownMenu.Item href="/settings/appearance">Appearance</DropdownMenu.Item>
<DropdownMenu.Item href="/settings/notifications">Notifications</DropdownMenu.Item>
<!-- <DropdownMenu.Item href="/settings/notifications">Notifications</DropdownMenu.Item> -->
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/logout">

View file

@ -15,10 +15,6 @@
title: "Appearance",
href: "/settings/appearance",
},
{
title: "Notifications",
href: "/settings/notifications",
},
];
</script>

View file

@ -18,5 +18,5 @@
<Separator />
<UsernameForm user={data.user} data={data.usernameForm} />
<EmailForm user={data.user} requestData={data.emailRequestForm} confirmData={data.emailConfirmForm} />
<PasswordForm user={data.user} data={data.passwordForm} />
<PasswordForm data={data.passwordForm} />
</div>

View file

@ -80,7 +80,12 @@
<Form.Label>New email</Form.Label>
<div class="flex space-x-2">
<Input placeholder={user?.email} {...attrs} bind:value={$requestFormData.newEmail} />
<Form.Button variant="secondary" disabled={isLoading}>Request token</Form.Button>
<Form.Button variant="secondary" disabled={isLoading}>
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{/if}
Request token
</Form.Button>
</div>
</Form.Control>
<Form.FieldErrors />
@ -121,7 +126,7 @@
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{/if}
Update password
Update email
</Form.Button>
</form>

View file

@ -63,7 +63,7 @@
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{/if}
Update password
Update username
</Form.Button>
</form>

View file

@ -11,13 +11,14 @@ export const load: PageServerLoad = async () => {
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(appearanceFormSchema));
default: async ({ request, locals }) => {
const form = await superValidate(request, zod(appearanceFormSchema));
if (!form.valid) {
return fail(400, {
form,
});
}
await locals.pocketBase.collection('users').update(locals.id, form.data);
return {
form,
};

View file

@ -2,7 +2,7 @@
import { z } from 'zod';
export const appearanceFormSchema = z.object({
theme: z.enum(['system', 'light', 'dark'], {
appearanceMode: z.enum(['system', 'light', 'dark'], {
required_error: 'Please select a theme.'
})
});
@ -13,35 +13,86 @@
<script lang="ts">
import { browser, dev } from '$app/environment';
import { PUBLIC_DEBUG_FORMS } from '$env/static/public';
import SuperDebug, { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
import SuperDebug, {
type SuperValidated,
type Infer,
superForm,
} from 'sveltekit-superforms';
import * as Card from '$lib/components/ui/card';
import * as Form from '$lib/components/ui/form';
import * as RadioGroup from '$lib/components/ui/radio-group';
import Label from '$lib/components/ui/label/label.svelte';
import { zodClient } from 'sveltekit-superforms/adapters';
import { toast } from 'svelte-sonner';
import { Icons } from '$lib/components/site';
import { resetMode, setMode } from 'mode-watcher';
export let data: SuperValidated<Infer<AppearanceFormSchema>>;
let isLoading = false;
const form = superForm(data, {
validators: zodClient(appearanceFormSchema)
validators: zodClient(appearanceFormSchema),
onSubmit: () => {
isLoading = true;
toast.loading('Updating appearance...');
},
onUpdated: ({ form: f }) => {
isLoading = false;
setMode(f.data.appearanceMode)
if (f.valid) {
toast.success('Appearance has been updated.');
} else {
toast.error('Please fix the errors in the form.');
}
}
});
const { form: formData, enhance } = form;
</script>
<form method="POST" use:enhance class="space-y-8">
<Form.Fieldset {form} name="theme">
<Form.Legend>Theme</Form.Legend>
<Form.Description>Select the theme for the dashboard.</Form.Description>
<Form.FieldErrors />
<RadioGroup.Root
class="grid grid-cols-3 gap-8 pt-2"
orientation="horizontal"
bind:value={$formData.theme}
>
<Form.Control let:attrs>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<RadioGroup.Item {...attrs} value="system" class="sr-only" />
<!-- <div class="container">
<div class="div1">
<Card.Root>
<Card.Header>
<Card.Title>Change your mode</Card.Title>
<Card.Description>
You can modify the mode for your theme preference.
</Card.Description>
</Card.Header>
<Card.Content>
<form method="POST" use:enhance class="space-y-2">
<Form.Fieldset {form} name="appearanceMode">
<Form.Legend>Theme</Form.Legend>
<Form.FieldErrors />
<RadioGroup.Root
class="grid grid-cols-3 gap-8 pt-2"
orientation="horizontal"
bind:value={$formData.appearanceMode}
>
<Form.Control let:attrs>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<RadioGroup.Item {...attrs} value="system" class="sr-only" />
<div
class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground"
>
<div class="space-y-2 rounded-sm bg-slate-500 p-2">
<div class="space-y-2 rounded-md bg-slate-400 p-2 shadow-sm">
<div class="h-2 w-4 rounded-lg bg-slate-200" />
<div class="h-2 w-full rounded-lg bg-slate-200" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-400 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-200" />
<div class="h-2 w-full rounded-lg bg-slate-200" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-400 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-200" />
<div class="h-2 w-full rounded-lg bg-slate-200" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> System </span>
</Label>
</Form.Control>
<Form.Control let:attrs>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<RadioGroup.Item {...attrs} value="light" class="sr-only" />
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
@ -58,8 +109,12 @@
</div>
</div>
</div>
</div>
<div class="div2"> -->
<span class="block w-full p-2 text-center font-normal"> Light </span>
</Label>
</Form.Control>
<Form.Control let:attrs>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<RadioGroup.Item {...attrs} value="dark" class="sr-only" />
<div
class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground"
>
@ -78,83 +133,22 @@
</div>
</div>
</div>
<!-- </div>
</div> -->
<span class="block w-full p-2 text-center font-normal"> System </span>
</Label>
</Form.Control>
<Form.Control let:attrs>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<RadioGroup.Item {...attrs} value="light" class="sr-only" />
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-2 w-4 rounded-lg bg-[#ecedef]" />
<div class="h-2 w-full rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-full rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-full rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> Light </span>
</Label>
</Form.Control>
<Form.Control let:attrs>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<RadioGroup.Item {...attrs} value="dark" class="sr-only" />
<div
class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground"
>
<div class="space-y-2 rounded-sm bg-slate-950 p-2">
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-2 w-4 rounded-lg bg-slate-400" />
<div class="h-2 w-full rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-full rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-full rounded-lg bg-slate-400" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> Dark </span>
</Label>
</Form.Control>
<RadioGroup.Input name="theme" />
</RadioGroup.Root>
</Form.Fieldset>
<Form.Button>Update preferences</Form.Button>
</form>
<span class="block w-full p-2 text-center font-normal"> Dark </span>
</Label>
</Form.Control>
<RadioGroup.Input name="appearanceMode" />
</RadioGroup.Root>
</Form.Fieldset>
<Form.Button disabled={isLoading}>
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{/if}
Update appearance
</Form.Button>
</form>
{#if dev && PUBLIC_DEBUG_FORMS == 'true' && browser}
<SuperDebug data={$formData} />
{/if}
<style>
.container {
position: relative;
width: 100%;
height: 100%;
}
.div1,
.div2 {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.div2 {
clip-path: polygon(0 0, 100% 0, 0 100%);
}
</style>
{#if dev && PUBLIC_DEBUG_FORMS == 'true' && browser}
<SuperDebug data={$formData} />
{/if}
</Card.Content>
</Card.Root>

View file

@ -1,25 +0,0 @@
import { superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import type { PageServerLoad } from "../$types";
import { notificationsFormSchema } from "./notifications-form.svelte";
import { fail, type Actions } from "@sveltejs/kit";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(zod(notificationsFormSchema)),
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(notificationsFormSchema));
if (!form.valid) {
return fail(400, {
form,
});
}
return {
form,
};
},
};

View file

@ -1,16 +0,0 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import NotificationsForm from "./notifications-form.svelte";
import type { PageData } from "./$types";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Notifications</h3>
<p class="text-sm text-muted-foreground">Configure how you receive notifications.</p>
</div>
<Separator />
<NotificationsForm data={data.form} />
</div>

View file

@ -1,146 +0,0 @@
<script lang="ts" context="module">
import { z } from "zod";
export const notificationsFormSchema = z.object({
type: z.enum(["all", "mentions", "none"], {
required_error: "You need to select a notification type.",
}),
mobile: z.boolean().default(false).optional(),
communication_emails: z.boolean().default(false).optional(),
social_emails: z.boolean().default(false).optional(),
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean(),
});
type NotificationFormSchema = typeof notificationsFormSchema;
</script>
<script lang="ts">
import SuperDebug, { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
import * as Form from "$lib/components/ui/form";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { Switch } from "$lib/components/ui/switch";
import { zodClient } from "sveltekit-superforms/adapters";
import Checkbox from "$lib/components/ui/checkbox/checkbox.svelte";
import { browser, dev } from "$app/environment";
import { PUBLIC_DEBUG_FORMS } from "$env/static/public";
export let data: SuperValidated<Infer<NotificationFormSchema>>;
const form = superForm(data, {
validators: zodClient(notificationsFormSchema),
});
const { form: formData, enhance } = form;
</script>
<form method="POST" use:enhance class="space-y-8">
<Form.Fieldset {form} name="type">
<Form.Legend>Notify me about...</Form.Legend>
<Form.Control>
<RadioGroup.Root bind:value={$formData.type}>
<div class="flex items-center space-x-3">
<Form.Control let:attrs>
<RadioGroup.Item value="all" {...attrs} />
<Form.Label>All new messages</Form.Label>
</Form.Control>
</div>
<div class="flex items-center space-x-3">
<Form.Control let:attrs>
<RadioGroup.Item value="mentions" {...attrs} />
<Form.Label>Direct messages and mentions</Form.Label>
</Form.Control>
</div>
<div class="flex items-center space-x-3">
<Form.Control let:attrs>
<RadioGroup.Item value="none" {...attrs} />
<Form.Label>Nothing</Form.Label>
</Form.Control>
</div>
<RadioGroup.Input name="type" />
</RadioGroup.Root>
</Form.Control>
</Form.Fieldset>
<div>
<h3 class="mb-4 text-lg font-medium">Email Notifications</h3>
<div class="space-y-4">
<Form.Field
{form}
name="communication_emails"
class="flex flex-row items-center justify-between rounded-lg border p-4"
>
<Form.Control let:attrs>
<div class="space-y-0.5">
<Form.Label class="text-base">Communication emails</Form.Label>
<Form.Description>
Receive emails about your account activity.
</Form.Description>
</div>
<Switch includeInput {...attrs} bind:checked={$formData.communication_emails} />
</Form.Control>
</Form.Field>
<Form.Field
{form}
name="marketing_emails"
class="flex flex-row items-center justify-between rounded-lg border p-4"
>
<Form.Control let:attrs>
<div class="space-y-0.5">
<Form.Label class="text-base">Marketing emails</Form.Label>
<Form.Description>
Receive emails about new products, features, and more.
</Form.Description>
</div>
<Switch includeInput {...attrs} bind:checked={$formData.marketing_emails} />
</Form.Control>
</Form.Field>
<Form.Field
{form}
name="social_emails"
class="flex flex-row items-center justify-between rounded-lg border p-4"
>
<Form.Control let:attrs>
<div class="space-y-0.5">
<Form.Label class="text-base">Social emails</Form.Label>
<Form.Description>
Receive emails for friend requests, follows, and more.
</Form.Description>
</div>
<Switch includeInput {...attrs} bind:checked={$formData.social_emails} />
</Form.Control>
</Form.Field>
<Form.Field
{form}
name="security_emails"
class="flex flex-row items-center justify-between rounded-lg border p-4"
>
<Form.Control let:attrs>
<div class="space-y-0.5">
<Form.Label class="text-base">Security emails</Form.Label>
<Form.Description>
Receive emails about your account activity and security.
</Form.Description>
</div>
<Switch includeInput {...attrs} bind:checked={$formData.security_emails} />
</Form.Control>
</Form.Field>
</div>
</div>
<Form.Field {form} name="mobile" class="flex flex-row items-start space-x-3 space-y-0">
<Form.Control let:attrs>
<Checkbox {...attrs} bind:checked={$formData.mobile} />
<div class="space-y-1 leading-none">
<Form.Label>Use different settings for my mobile devices</Form.Label>
<Form.Description>
You can manage your mobile notifications in the <a href="/examples/forms"
>mobile settings</a
> page.
</Form.Description>
</div>
<input name={attrs.name} bind:value={$formData.mobile} hidden />
</Form.Control>
</Form.Field>
<Form.Button>Update notifications</Form.Button>
</form>
{#if dev && PUBLIC_DEBUG_FORMS == 'true' && browser}
<SuperDebug data={$formData} />
{/if}

View file

@ -50,7 +50,7 @@
</Card.Description>
</Card.Header>
<Card.Content>
<form method="POST" class="space-y-8" use:enhance>
<form method="POST" class="space-y-2" use:enhance>
<Form.Field {form} name="name">
<Form.Control let:attrs>
<Form.Label>Name</Form.Label>
@ -64,7 +64,7 @@
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{/if}
Update password
Update name
</Form.Button>
</form>

View file

@ -0,0 +1,5 @@
import { redirect } from '@sveltejs/kit';
export const load = async () => {
throw redirect(303, '/settings');
};

View file

@ -1,17 +1,22 @@
<script lang="ts">
import { dev } from '$app/environment';
import { Metadata, SiteFooter, SiteNavBar, TailwindIndicator } from '$lib/components/site';
import { ModeWatcher } from 'mode-watcher';
import { ModeWatcher, setMode } from 'mode-watcher';
import '../app.pcss';
import type { LayoutData } from './$types';
import DataIndicator from '$lib/components/site/data-indicator.svelte';
import { fly } from 'svelte/transition';
import { Toaster } from 'svelte-sonner';
import { } from "mode-watcher";
export let data: LayoutData;
if (data.user?.appearanceMode) {
setMode(data.user.appearanceMode);
}
</script>
<ModeWatcher />
<ModeWatcher defaultMode={data.user?.appearanceMode ?? 'system'} />
<Toaster />
<Metadata />