feat: re-import of settings

This commit is contained in:
Bart van der Braak 2024-02-20 20:31:40 +01:00
parent 6e12f454b3
commit 28721b4ba5
46 changed files with 913 additions and 793 deletions

View file

@ -1,85 +0,0 @@
<script lang="ts" context="module">
// const MAX_IMAGE_SIZE = 5;
// const ACCEPTED_IMAGE_TYPES = [
// 'image/jpeg',
// 'image/png',
// 'image/svg+xml',
// 'image/gif',
// 'image/webp'
// ];
import { z } from 'zod';
// const sizeInMB = (sizeInBytes: number, decimalsNum = 2) => {
// const result = sizeInBytes / (1024 * 1024);
// return +result.toFixed(decimalsNum);
// };
export const avatarFormSchema = z.object({
avatar: z.any()
// .custom<FileList>()
// .refine((files) => {
// return Array.from(files ?? []).length !== 0;
// }, 'Image is required')
// .refine((files) => {
// return Array.from(files ?? []).every((file) => sizeInMB(file.size) <= MAX_IMAGE_SIZE);
// }, `The maximum image size is ${MAX_IMAGE_SIZE}MB`)
// .refine((files) => {
// return Array.from(files ?? []).every((file) => ACCEPTED_IMAGE_TYPES.includes(file.type));
// }, 'File type is not supported')
});
export type AvatarFormSchema = typeof avatarFormSchema;
</script>
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Form from '$lib/components/ui/form';
import type { SuperValidated } from 'sveltekit-superforms';
import type { LayoutData } from '../../$types';
import * as Avatar from '$lib/components/ui/avatar';
import { toast } from 'svelte-sonner';
import type { FormOptions } from 'formsnap';
export let avatarFormSchemaData: SuperValidated<AvatarFormSchema>;
export let user: LayoutData['user'];
export let debug: boolean;
const options: FormOptions<AvatarFormSchema> = {
onSubmit() {
toast.info('Uploading avatar...');
},
onResult({ result }) {
if (result.status === 200) toast.success('Your avatar has been updated!');
if (result.status === 400) toast.error('There was an error updating your avatar.');
},
dataType: 'form'
};
</script>
<Card.Root>
<Card.Header>
<Card.Title>Avatar image</Card.Title>
<Card.Description>
This is the image that will be displayed on your profile, in dashboards and in emails.
</Card.Description>
</Card.Header>
<Card.Content>
<Form.Root
{options}
form={avatarFormSchemaData}
schema={avatarFormSchema}
let:config
action="?/avatar"
method="POST"
class="space-y-2"
{debug}
><Form.Item>
<Form.Field {config} name="avatar">
<Form.Label class="sr-only">Avatar</Form.Label>
<Avatar.Root class="aspect-square h-auto w-full">
<Avatar.Image src={user?.avatarUrl} alt={user?.name} />
<Avatar.Fallback class="text-8xl">{user?.initials}</Avatar.Fallback>
</Avatar.Root>
<Form.Input type="file" />
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button>Update avatar</Form.Button>
</Form.Root>
</Card.Content>
</Card.Root>

View file

@ -1,104 +0,0 @@
<script lang="ts" context="module">
import { z } from 'zod';
export const emailRequestFormSchema = z.object({
newEmail: z.string().email()
});
export const emailConfirmFormSchema = z.object({
token: z.string(),
password: z.string()
});
export type EmailRequestFormSchema = typeof emailRequestFormSchema;
export type EmailConfirmFormSchema = typeof emailConfirmFormSchema;
</script>
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Form from '$lib/components/ui/form';
import { toast } from 'svelte-sonner';
import type { SuperValidated } from 'sveltekit-superforms';
import type { LayoutData } from '../$types';
import type { FormOptions } from 'formsnap';
export let emailRequestFormSchemaData: SuperValidated<EmailRequestFormSchema>;
export let emailConfirmFormSchemaData: SuperValidated<EmailConfirmFormSchema>;
const requestOptions: FormOptions<EmailRequestFormSchema> = {
onSubmit() {
toast.info('Requesting token...');
},
onResult({ result }) {
if (result.status === 200) toast.success('Token sent! Check your email.');
if (result.status === 400)
toast.error('There was an error sending the token. Is this email already in use?');
}
};
const confirmOptions: FormOptions<EmailConfirmFormSchema> = {
onSubmit() {
toast.info('Changing email...');
},
onResult({ result }) {
console.log(result);
if (result.status === 200) toast.success('Your email has been changed!');
if (result.status === 400)
toast.error('There was an error changing your email. Is the token correct?');
}
};
export let user: LayoutData['user'];
export let debug: boolean;
</script>
<Card.Root>
<Card.Header>
<Card.Title>Modify your email</Card.Title>
<Card.Description
>Use a different email by requesting a token and entering it for verification below.</Card.Description
>
</Card.Header>
<Card.Content>
<Form.Root
schema={emailRequestFormSchema}
form={emailRequestFormSchemaData}
options={requestOptions}
let:config
method="POST"
action="?/emailRequest"
class="space-y-8"
{debug}
>
<Form.Item>
<Form.Field {config} name="newEmail">
<Form.Label>New email</Form.Label>
<div class="flex space-x-2">
<Form.Input placeholder={user?.email} />
<Form.Button variant="secondary">Request token</Form.Button>
</div>
<Form.Validation />
</Form.Field>
</Form.Item>
</Form.Root>
<Form.Root
options={confirmOptions}
form={emailConfirmFormSchemaData}
schema={emailConfirmFormSchema}
let:config
action="?/emailConfirm"
method="POST"
class="space-y-2"
{debug}
>
<Form.Item>
<Form.Field {config} name="token">
<Form.Label>Email verification token</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="password">
<Form.Label>Password</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button>Confirm email change</Form.Button>
</Form.Root>
</Card.Content>
</Card.Root>

View file

@ -1,79 +0,0 @@
<script lang="ts" context="module">
import { z } from 'zod';
const emptyStringToUndefined = z.literal('').transform(() => undefined);
export const nameFormSchema = z.object({
name: z
.string()
.min(2, 'Name must be at least 2 characters.')
.max(30, 'Name must not be longer than 30 characters')
.optional()
.or(emptyStringToUndefined),
username: z
.string()
.min(2, 'Username must be at least 2 characters.')
.max(30, 'Username must not be longer than 30 characters')
.optional()
.or(emptyStringToUndefined)
});
export type NameFormSchema = typeof nameFormSchema;
</script>
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Form from '$lib/components/ui/form';
import type { SuperValidated } from 'sveltekit-superforms';
import type { LayoutData } from '../$types';
import type { FormOptions } from 'formsnap';
import { toast } from 'svelte-sonner';
export let nameFormSchemaData: SuperValidated<NameFormSchema>;
export let user: LayoutData['user'];
export let debug: boolean;
const options: FormOptions<NameFormSchema> = {
onSubmit() {
toast.info('Updating name...');
},
onResult({ result }) {
if (result.status === 200) toast.success('Your name has been updated!');
if (result.status === 400) toast.error('There was an error updating your name.');
},
dataType: 'form'
};
</script>
<Card.Root>
<Card.Header>
<Card.Title>Change your name</Card.Title>
<Card.Description>
You can change the name that will be displayed on your profile and in emails as well as your
username.
</Card.Description>
</Card.Header>
<Card.Content>
<Form.Root
{options}
form={nameFormSchemaData}
schema={nameFormSchema}
let:config
action="?/name"
method="POST"
class="space-y-2"
{debug}
>
<Form.Item>
<Form.Field name="name" {config}>
<Form.Label>Display name</Form.Label>
<Form.Input placeholder={user?.name} />
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="username">
<Form.Label>Username</Form.Label>
<Form.Input placeholder={user?.username} />
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button>Update name</Form.Button>
</Form.Root>
</Card.Content>
</Card.Root>

View file

@ -1,78 +0,0 @@
<script lang="ts" context="module">
import { z } from 'zod';
export const passwordFormSchema = z
.object({
oldPassword: z.string(),
password: z.string().min(4),
passwordConfirm: z.string().min(4)
})
.refine(
(values) => {
return values.password === values.passwordConfirm;
},
{
message: 'Passwords must match.',
path: ['passwordConfirm']
}
);
export type PasswordFormSchema = typeof passwordFormSchema;
</script>
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Form from '$lib/components/ui/form';
import type { SuperValidated } from 'sveltekit-superforms';
import type { FormOptions } from 'formsnap';
import { toast } from 'svelte-sonner';
export let passwordFormSchemaData: SuperValidated<PasswordFormSchema>;
export let debug: boolean;
const options: FormOptions<PasswordFormSchema> = {
onSubmit() {
toast.info('Changing password...');
},
onResult({ result }) {
if (result.status === 200)
toast.success('Your password has been changed! Please log in again.');
if (result.status === 400)
toast.error('There was an error changing your password. Is the old password correct?');
}
};
</script>
<Card.Root>
<Card.Header>
<Card.Title>Password change</Card.Title>
<Card.Description>You can change your account password here.</Card.Description>
</Card.Header>
<Card.Content>
<Form.Root
{options}
form={passwordFormSchemaData}
schema={passwordFormSchema}
let:config
action="?/password"
method="POST"
class="space-y-2"
{debug}
>
<Form.Item>
<Form.Field {config} name="oldPassword">
<Form.Label>Current Password</Form.Label>
<Form.Input type="password" />
<Form.Validation />
</Form.Field>
<Form.Field {config} name="password">
<Form.Label>New Password</Form.Label>
<Form.Input type="password" />
<Form.Validation />
</Form.Field>
<Form.Field {config} name="passwordConfirm">
<Form.Label>Confirm new password</Form.Label>
<Form.Input type="password" />
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button>Update password</Form.Button>
</Form.Root>
</Card.Content>
</Card.Root>

View file

@ -1,35 +0,0 @@
<script lang="ts">
import { Separator } from '$lib/components/ui/separator';
import SidebarNav from './(components)/sidebar-nav.svelte';
const sidebarNavItems = [
{
title: 'Profile',
href: '/settings'
},
{
title: 'Appearance',
href: '/settings/appearance'
},
{
title: 'Notifications',
href: '/settings/notifications'
}
];
</script>
<div class="space-y-6 p-10 pb-16 md:block">
<div class="space-y-0.5">
<h2 class="text-2xl font-bold tracking-tight">Settings</h2>
<p class="text-muted-foreground">Manage your account settings and set e-mail preferences.</p>
</div>
<Separator class="my-6" />
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside class="-mx-4 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div class="flex-1">
<slot />
</div>
</div>
</div>

View file

@ -1,79 +0,0 @@
import type { PageServerLoad } from './$types';
import { superValidate } from 'sveltekit-superforms/server';
import { fail, type Actions } from '@sveltejs/kit';
import { nameFormSchema } from './(components)/name-form.svelte';
import { emailConfirmFormSchema, emailRequestFormSchema } from './(components)/email-form.svelte';
import { passwordFormSchema } from './(components)/password-form.svelte';
import { avatarFormSchema } from './(components)/avatar-form.svelte';
export const load: PageServerLoad = async () => {
return {
forms: {
name: await superValidate(nameFormSchema),
emailRequest: await superValidate(emailRequestFormSchema),
emailConfirm: await superValidate(emailConfirmFormSchema),
password: await superValidate(passwordFormSchema),
avatar: await superValidate(avatarFormSchema),
debug: false
}
};
};
export const actions: Actions = {
name: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, nameFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase.collection('users').update(locals.id, form.data);
return { form };
},
emailRequest: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
console.log(request);
const form = await superValidate(request, emailRequestFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase.collection('users').requestEmailChange(form.data.newEmail);
return { form };
},
emailConfirm: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, emailConfirmFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase
.collection('users')
.confirmEmailChange(form.data.token, form.data.password);
return { form };
},
password: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, passwordFormSchema);
console.log(form);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase.collection('users').update(locals.id, form.data);
return { form };
},
avatar: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const formData = await request.formData();
const form = await superValidate(request, avatarFormSchema);
if (!form.valid) return fail(400, { form });
const file = formData.get('file');
if (file instanceof File) {
await locals.pocketBase.collection('users').update(locals.id, { avatar: file });
}
return { form };
}
};

View file

@ -1,34 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import { Separator } from '$lib/components/ui/separator';
import NameForm from './(components)/name-form.svelte';
import EmailForm from './(components)/email-form.svelte';
import PasswordForm from './(components)/password-form.svelte';
import AvatarForm from './(components)/avatar-form.svelte';
export let data: PageData;
let { forms, user } = data;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Account</h3>
<p class="text-sm text-muted-foreground">Update your account and profile settings.</p>
</div>
<Separator />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-[4fr,3fr]">
<div class="space-y-8">
<NameForm nameFormSchemaData={forms.name} {user} debug={forms.debug} />
<EmailForm
emailRequestFormSchemaData={forms.emailRequest}
emailConfirmFormSchemaData={forms.emailConfirm}
{user}
debug={forms.debug}
/>
<PasswordForm passwordFormSchemaData={forms.password} debug={forms.debug} />
</div>
<div>
<AvatarForm avatarFormSchemaData={forms.avatar} {user} debug={forms.debug} />
</div>
</div>
</div>

View file

@ -1,25 +0,0 @@
import { superValidate } from 'sveltekit-superforms/server';
import type { PageServerLoad } from '../$types';
import { appearanceFormSchema } from './appearance-form.svelte';
import { fail, type Actions } from '@sveltejs/kit';
export const load: PageServerLoad = async () => {
return {
form: await superValidate(appearanceFormSchema)
};
};
export const actions: Actions = {
default: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, appearanceFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase
.collection('users')
.update(locals.id, { appearanceMode: form.data.theme });
return { form };
}
};

View file

@ -1,125 +0,0 @@
<script lang="ts" context="module">
import type { SuperValidated } from 'sveltekit-superforms';
import { z } from 'zod';
export const appearanceFormSchema = z.object({
theme: z.enum(['light', 'dark', 'system'], {
required_error: 'Please select a theme.'
})
});
export type AppearanceFormSchema = typeof appearanceFormSchema;
</script>
<script lang="ts">
import * as Form from '$lib/components/ui/form';
import Label from '$lib/components/ui/label/label.svelte';
import { dev } from '$app/environment';
import type { PageData } from '../$types';
import type { FormOptions } from 'formsnap';
import { toast } from 'svelte-sonner';
export let isLoading = false;
export let data: SuperValidated<AppearanceFormSchema>;
const options: FormOptions<AppearanceFormSchema> = {
onSubmit() {
isLoading = true;
toast.info('Updating appearance...');
},
onResult({ result }) {
isLoading = false;
if (result.status === 200) toast.success('Your appearance has been updated!');
if (result.status === 400) toast.error('There was an error updating your appearance.');
},
dataType: 'form'
};
export let user: PageData['user'];
</script>
<Form.Root
schema={appearanceFormSchema}
{options}
form={data}
class="space-y-8"
method="POST"
let:config
debug={dev ? true : false}
>
<Form.Item>
<Form.Field {config} name="theme">
<Form.Label>Theme</Form.Label>
<Form.Description>Select the theme for the dashboard.</Form.Description>
<Form.Validation />
<Form.RadioGroup
class="grid max-w-xl grid-cols-3 gap-8 pt-2"
orientation="horizontal"
value={user?.appearanceMode}
>
<Label for="light" class="[&:has([data-state=checked])>div]:border-primary">
<Form.RadioItem id="light" 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-[80px] rounded-lg bg-[#ecedef]" />
<div class="h-2 w-[100px] 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-[100px] 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-[100px] rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> Light </span>
</Label>
<Label for="dark" class="[&:has([data-state=checked])>div]:border-primary">
<Form.RadioItem id="dark" 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-[80px] rounded-lg bg-slate-400" />
<div class="h-2 w-[100px] 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-[100px] 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-[100px] rounded-lg bg-slate-400" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> Dark </span>
</Label>
<Label for="system" class="[&:has([data-state=checked])>div]:border-primary">
<Form.RadioItem id="system" 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-[80px] rounded-lg bg-slate-200" />
<div class="h-2 w-[100px] 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-[100px] 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-[100px] rounded-lg bg-slate-200" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> System </span>
</Label>
</Form.RadioGroup>
</Form.Field>
</Form.Item>
<Form.Button disabled={isLoading}>Update preferences</Form.Button>
</Form.Root>

View file

@ -1,24 +0,0 @@
import { superValidate } from 'sveltekit-superforms/server';
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(notificationsFormSchema)
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, notificationsFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
return {
form
};
}
};

View file

@ -1,107 +0,0 @@
<script lang="ts" context="module">
import { z } from 'zod';
export const notificationsFormSchema = z.object({
type: z.enum(['all', 'tickets', 'none'], {
required_error: 'You need to select a notification type.'
})
});
type NotificationFormSchema = typeof notificationsFormSchema;
</script>
<script lang="ts">
import type { SuperValidated } from 'sveltekit-superforms';
import * as Card from '$lib/components/ui/card';
import * as Form from '$lib/components/ui/form';
import { Label } from '$lib/components/ui/label';
export let data: SuperValidated<NotificationFormSchema>;
import { dev } from '$app/environment';
import { Bell, EyeNone, Person } from 'radix-icons-svelte';
</script>
<Card.Root>
<Card.Header class="pb-3">
<Card.Title>Notifications</Card.Title>
<Card.Description>Choose what you want to be notified about.</Card.Description>
</Card.Header>
<Card.Content class="grid gap-1">
<Form.Root
form={data}
schema={notificationsFormSchema}
let:config
method="POST"
class="space-y-8"
debug={dev ? true : false}
>
<Form.Item>
<Form.Field {config} name="type">
<Form.RadioGroup class="grid max-w-xl grid-cols-3 gap-8 pt-2" orientation="horizontal">
<!-- value={user?.appearanceMode} -->
<Label
for="all"
class="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-primary"
>
<Form.RadioItem id="all" value="all" class="sr-only" />
<Bell class="mb-3 h-6 w-6" />
<span class="block w-full p-2 text-center font-normal">Everything</span>
<span class="text-center text-sm text-muted-foreground">New tickets and updates.</span
>
</Label>
<Label
for="tickets"
class="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-primary"
>
<Form.RadioItem id="tickets" value="tickets" class="sr-only" />
<Person class="mb-3 h-6 w-6" />
<span class="block w-full p-2 text-center font-normal">New tickets</span>
<span class="text-center text-sm text-muted-foreground"
>Only new unassigned tickets</span
>
</Label>
<Label
for="none"
class="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-primary"
>
<Form.RadioItem id="none" value="none" class="sr-only" />
<EyeNone class="mb-3 h-6 w-6" />
<span class="block w-full p-2 text-center font-normal">Ignore</span>
<span class="text-center text-sm text-muted-foreground"
>Turn off all notifications.</span
>
</Label>
</Form.RadioGroup>
<Form.Validation />
</Form.Field>
</Form.Item>
</Form.Root>
</Card.Content>
</Card.Root>
<!-- <Form.Root
form={data}
schema={notificationsFormSchema}
let:config
method="POST"
class="space-y-8"
debug={dev ? true : false}
>
<Form.Item>
<Form.Field {config} name="type">
<Form.Label>Notify me about...</Form.Label>
<Form.RadioGroup class="flex flex-col space-y-1">
<div class="flex items-center space-x-3">
<Form.RadioItem value="all" id="all" />
<Label for="all" class="font-normal">New tickets and SLA breaches</Label>
</div>
<div class="flex items-center space-x-3">
<Form.RadioItem value="tickets" id="mentions" />
<Label for="mentions" class="font-normal">New tickets</Label>
</div>
<div class="flex items-center space-x-3">
<Form.RadioItem value="none" id="none" />
<Label for="none" class="font-normal">Nothing</Label>
</div>
</Form.RadioGroup>
</Form.Field>
</Form.Item>
<Form.Button>Update notifications</Form.Button>
</Form.Root> -->

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { page } from '$app/stores';
import { Button } from '$lib/components/ui/button';
import { cubicInOut } from 'svelte/easing';
import { crossfade } from 'svelte/transition';
import { cn } from "$lib/utils";
import { page } from "$app/stores";
import { Button } from "$lib/components/ui/button";
import { cubicInOut } from "svelte/easing";
import { crossfade } from "svelte/transition";
let className: string | undefined | null = undefined;
export let items: { href: string; title: string }[];
@ -11,25 +11,28 @@
const [send, receive] = crossfade({
duration: 250,
easing: cubicInOut
easing: cubicInOut,
});
</script>
<nav class={cn('flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1', className)}>
<nav class={cn("flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1", className)}>
{#each items as item}
{@const isActive = $page.url.pathname === item.href}
<Button
href={item.href}
variant="ghost"
class={cn(!isActive && 'hover:underline', 'relative justify-start hover:bg-transparent')}
class={cn(
!isActive && "hover:underline",
"relative justify-start hover:bg-transparent"
)}
data-sveltekit-noscroll
>
{#if isActive}
<div
class="absolute inset-0 rounded-md bg-muted"
in:send={{ key: 'active-sidebar-tab' }}
out:receive={{ key: 'active-sidebar-tab' }}
in:send={{ key: "active-sidebar-tab" }}
out:receive={{ key: "active-sidebar-tab" }}
/>
{/if}
<div class="relative">

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import SidebarNav from "./(components)/sidebar-nav.svelte";
const sidebarNavItems = [
{
title: "Profile",
href: "/settings",
},
{
title: "Account",
href: "/settings/account",
},
{
title: "Appearance",
href: "/settings/appearance",
},
{
title: "Notifications",
href: "/settings/notifications",
},
{
title: "Display",
href: "/settings/display",
},
];
</script>
<div class="hidden space-y-6 p-10 pb-16 md:block">
<div class="space-y-0.5">
<h2 class="text-2xl font-bold tracking-tight">Settings</h2>
<p class="text-muted-foreground">
Manage your account settings and set e-mail preferences.
</p>
</div>
<Separator class="my-6" />
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside class="-mx-4 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div class="flex-1 lg:max-w-2xl">
<slot />
</div>
</div>
</div>

View file

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

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { PageData } from "./$types";
import ProfileForm from "./profile-form.svelte";
import { Separator } from "$lib/components/ui/separator";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Profile</h3>
<p class="text-sm text-muted-foreground">This is how others will see you on the site.</p>
</div>
<Separator />
<ProfileForm data={data.form} />
</div>

View file

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

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import AccountForm from "./account-form.svelte";
import type { PageData } from "./$types";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Account</h3>
<p class="text-sm text-muted-foreground">
Update your account settings. Set your preferred language and timezone.
</p>
</div>
<Separator />
<AccountForm data={data.form} />
</div>

View file

@ -0,0 +1,179 @@
<script lang="ts" context="module">
import { z } from "zod";
const languages = [
{ label: "English", value: "en" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Spanish", value: "es" },
{ label: "Portuguese", value: "pt" },
{ label: "Russian", value: "ru" },
{ label: "Japanese", value: "ja" },
{ label: "Korean", value: "ko" },
{ label: "Chinese", value: "zh" },
] as const;
type Language = (typeof languages)[number]["value"];
export const accountFormSchema = z.object({
name: z
.string({
required_error: "Required.",
})
.min(2, "Name must be at least 2 characters.")
.max(30, "Name must not be longer than 30 characters"),
// Hack: https://github.com/colinhacks/zod/issues/2280
language: z.enum(languages.map((lang) => lang.value) as [Language, ...Language[]]),
dob: z
.string()
.datetime()
// we're setting it optional so the user can clear the date and we don't run into
// type issues, but we refine it to make sure it's not undefined
.optional()
.refine((date) => (date === undefined ? false : true), "Please select a valid date."),
});
export type AccountFormSchema = typeof accountFormSchema;
</script>
<script lang="ts">
import { Calendar as CalendarIcon, CaretSort, Check } from "radix-icons-svelte";
import SuperDebug, { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import * as Form from "$lib/components/ui/form";
import * as Popover from "$lib/components/ui/popover";
import * as Command from "$lib/components/ui/command";
import { Calendar } from "$lib/components/ui/calendar";
import { Input } from "$lib/components/ui/input";
import { buttonVariants } from "$lib/components/ui/button";
import { cn } from "$lib/utils";
import { browser } from "$app/environment";
import {
DateFormatter,
getLocalTimeZone,
type DateValue,
parseDate,
} from "@internationalized/date";
export let data: SuperValidated<Infer<AccountFormSchema>>;
const form = superForm(data, {
validators: zodClient(accountFormSchema),
});
const { form: formData, enhance, validate } = form;
const df = new DateFormatter("en-US", {
dateStyle: "long",
});
let dobValue: DateValue | undefined = $formData.dob ? parseDate($formData.dob) : undefined;
</script>
<form method="POST" class="space-y-8" use:enhance>
<Form.Field name="name" {form}>
<Form.Control let:attrs>
<Form.Label>Name</Form.Label>
<Input {...attrs} bind:value={$formData.name} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="dob" class="flex flex-col">
<Form.Control let:attrs>
<Form.Label>Date of Birth</Form.Label>
<Popover.Root>
<Popover.Trigger
class={cn(
buttonVariants({ variant: "outline" }),
"w-[240px] justify-start text-left font-normal",
!dobValue && "text-muted-foreground"
)}
{...attrs}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{dobValue ? df.format(dobValue.toDate(getLocalTimeZone())) : "Pick a date"}
</Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start">
<Calendar
bind:value={dobValue}
isDateDisabled={(currDate) => {
const currDateObj = currDate.toDate(getLocalTimeZone());
const today = new Date();
today.setHours(0, 0, 0, 0);
if (currDateObj > today || currDate.year < 1900) return true;
return false;
}}
onValueChange={(value) => {
if (value === undefined) {
$formData.dob = undefined;
validate("dob");
return;
}
$formData.dob = value.toDate(getLocalTimeZone()).toISOString();
validate("dob");
}}
/>
</Popover.Content>
<input hidden bind:value={$formData.dob} name={attrs.name} />
</Popover.Root>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="language" class="flex flex-col">
<Popover.Root>
<Form.Control let:attrs>
<Form.Label>Language</Form.Label>
<Popover.Trigger
role="combobox"
class={cn(
buttonVariants({ variant: "outline" }),
"w-[200px] justify-between",
!$formData.language && "text-muted-foreground"
)}
{...attrs}
>
{languages.find((lang) => lang.value === $formData.language)?.label ||
"Select a language"}
<CaretSort class="ml-2 size-4 shrink-0 opacity-50" />
</Popover.Trigger>
<input hidden bind:value={$formData.language} name={attrs.name} />
</Form.Control>
<Popover.Content class="w-[200px] p-0">
<Command.Root>
<Command.Input placeholder="Search language..." />
<Command.Empty>No language found.</Command.Empty>
<Command.List>
{#each languages as language}
<Command.Item
{...form}
value={language.label}
onSelect={() => {
$formData.language = language.value;
validate("language");
}}
>
<Check
class={cn(
"mr-2 size-4",
language.value === $formData.language
? "opacity-100"
: "opacity-0"
)}
/>
{language.label}
</Command.Item>
{/each}
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
</Form.Field>
<Form.Button>Update account</Form.Button>
</form>
{#if browser}
<SuperDebug data={$formData} />
{/if}

View file

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

View file

@ -1,10 +1,9 @@
<script lang="ts">
import { Separator } from '$lib/components/ui/separator';
import type { PageData } from './$types';
import AppearanceForm from './appearance-form.svelte';
import { Separator } from "$lib/components/ui/separator";
import type { PageData } from "./$types";
import AppearanceForm from "./appearance-form.svelte";
export let data: PageData;
export let { form, user } = data;
</script>
<div class="space-y-6">
@ -15,5 +14,5 @@
</p>
</div>
<Separator />
<AppearanceForm data={form} {user} />
<AppearanceForm data={data.form} />
</div>

View file

@ -0,0 +1,132 @@
<script lang="ts" context="module">
import { z } from "zod";
export const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark"], {
required_error: "Please select a theme.",
}),
font: z.enum(["inter", "manrope", "system"], {
invalid_type_error: "Select a font",
required_error: "Please select a font.",
}),
});
export type AppearanceFormSchema = typeof appearanceFormSchema;
</script>
<script lang="ts">
import { ChevronDown } from "radix-icons-svelte";
import { browser } from "$app/environment";
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 Label from "$lib/components/ui/label/label.svelte";
import { zodClient } from "sveltekit-superforms/adapters";
import { cn } from "$lib/utils";
import { buttonVariants } from "$lib/components/ui/button";
export let data: SuperValidated<Infer<AppearanceFormSchema>>;
const form = superForm(data, {
validators: zodClient(appearanceFormSchema),
});
const { form: formData, enhance } = form;
</script>
<form method="POST" use:enhance class="space-y-8">
<Form.Field {form} name="font">
<Form.Control let:attrs>
<Form.Label>Font</Form.Label>
<div class="relative w-max">
<select
{...attrs}
class={cn(
buttonVariants({ variant: "outline" }),
"w-[200px] appearance-none font-normal"
)}
bind:value={$formData.font}
>
<option value="inter">Inter</option>
<option value="manrope">Manrope</option>
<option value="system">System</option>
</select>
<ChevronDown class="absolute right-3 top-2.5 size-4 opacity-50" />
</div>
</Form.Control>
<Form.Description>Set the font you want to use in the dashboard.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<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 max-w-md grid-cols-2 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="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-[80px] rounded-lg bg-[#ecedef]" />
<div class="h-2 w-[100px] 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-[100px] 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-[100px] 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-[80px] rounded-lg bg-slate-400" />
<div class="h-2 w-[100px] 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-[100px] 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-[100px] 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>
{#if browser}
<SuperDebug data={$formData} />
{/if}

View file

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

View file

@ -0,0 +1,17 @@
<script lang="ts">
import type { PageData } from "./$types";
import DisplayForm from "./display-form.svelte";
import { Separator } from "$lib/components/ui/separator";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Display</h3>
<p class="text-sm text-muted-foreground">
Turn items on or off to control what's displayed in the app.
</p>
</div>
<Separator />
<DisplayForm data={data.form} />
</div>

View file

@ -0,0 +1,94 @@
<script lang="ts" context="module">
import { z } from "zod";
const items = [
{
id: "recents",
label: "Recents",
},
{
id: "home",
label: "Home",
},
{
id: "applications",
label: "Applications",
},
{
id: "desktop",
label: "Desktop",
},
{
id: "downloads",
label: "Downloads",
},
{
id: "documents",
label: "Documents",
},
] as const;
export const displayFormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "You have to select at least one item.",
}),
});
export type DisplayFormSchema = typeof displayFormSchema;
</script>
<script lang="ts">
import * as Form from "$lib/components/ui/form";
import * as Checkbox from "$lib/components/ui/checkbox";
import { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
import SuperDebug from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import { browser } from "$app/environment";
export let data: SuperValidated<Infer<DisplayFormSchema>>;
const form = superForm(data, {
validators: zodClient(displayFormSchema),
});
const { form: formData, enhance } = form;
</script>
<form method="POST" class="space-y-8" use:enhance>
<Form.Fieldset {form} name="items" class="space-y-0">
<div class="mb-4">
<Form.Legend class="text-base">Sidebar</Form.Legend>
<Form.Description>
Select the items you want to display in the sidebar.
</Form.Description>
</div>
<div class="space-y-2">
{#each items as item}
{@const checked = $formData.items.includes(item.id)}
<div class="flex flex-row items-center space-x-3">
<Form.Control let:attrs>
{@const { name, ...rest } = attrs}
<Checkbox.Root
{...rest}
{checked}
onCheckedChange={(v) => {
if (v) {
$formData.items = [...$formData.items, item.id];
} else {
$formData.items = $formData.items.filter((i) => i !== item.id);
}
}}
/>
<Form.Label class="font-normal">
{item.label}
</Form.Label>
<input type="checkbox" {name} hidden value={item.id} {checked} />
</Form.Control>
</div>
{/each}
<Form.FieldErrors />
</div>
</Form.Fieldset>
<Form.Button>Update display</Form.Button>
</form>
{#if browser}
<SuperDebug data={$formData} />
{/if}

View file

@ -0,0 +1,25 @@
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,7 +1,7 @@
<script lang="ts">
import { Separator } from '$lib/components/ui/separator';
import NotificationsForm from './notifications-form.svelte';
import type { PageData } from './$types';
import { Separator } from "$lib/components/ui/separator";
import NotificationsForm from "./notifications-form.svelte";
import type { PageData } from "./$types";
export let data: PageData;
</script>

View file

@ -0,0 +1,145 @@
<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 } from "$app/environment";
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 browser}
<SuperDebug data={$formData} />
{/if}

View file

@ -0,0 +1,122 @@
<script lang="ts" context="module">
import { z } from "zod";
export const profileFormSchema = z.object({
username: z
.string()
.min(2, "Username must be at least 2 characters.")
.max(30, "Username must not be longer than 30 characters"),
email: z.string({ required_error: "Please select an email to display" }).email(),
bio: z.string().min(4).max(160).default("I own a computer."),
urls: z
.array(z.string().url())
.default(["https://shadcn.com", "https://twitter.com/shadcn"]),
});
export type ProfileFormSchema = typeof profileFormSchema;
</script>
<script lang="ts">
import * as Form from "$lib/components/ui/form";
import * as Select from "$lib/components/ui/select";
import { Input } from "$lib/components/ui/input";
import { Button } from "$lib/components/ui/button";
import { Textarea } from "$lib/components/ui/textarea";
import { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
import SuperDebug from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import { cn } from "$lib/utils";
import { browser } from "$app/environment";
export let data: SuperValidated<Infer<ProfileFormSchema>>;
const form = superForm(data, {
validators: zodClient(profileFormSchema),
});
const { form: formData, enhance } = form;
function addUrl() {
$formData.urls = [...$formData.urls, ""];
}
$: selectedEmail = {
label: $formData.email,
value: $formData.email,
};
</script>
<form method="POST" class="space-y-8" use:enhance>
<Form.Field {form} name="username">
<Form.Control let:attrs>
<Form.Label>Username</Form.Label>
<Input placeholder="@shadcn" {...attrs} bind:value={$formData.username} />
</Form.Control>
<Form.Description>
This is your public display name. It can be your real name or a pseudonym. You can only
change this once every 30 days.
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="email">
<Form.Control let:attrs>
<Form.Label>Email</Form.Label>
<Select.Root
selected={selectedEmail}
onSelectedChange={(s) => {
s && ($formData.email = s.value);
}}
>
<Select.Trigger {...attrs}>
<Select.Value placeholder="Select a verified email to display" />
</Select.Trigger>
<Select.Content>
<Select.Item value="m@example.com" label="m@example.com" />
<Select.Item value="m@google.com" label="m@google.com" />
<Select.Item value="m@support.com" label="m@supporte.com" />
</Select.Content>
</Select.Root>
<input hidden name={attrs.name} bind:value={$formData.email} />
</Form.Control>
<Form.Description>
You can manage verified email addresses in your <a href="/examples/forms"
>email settings</a
>.
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="bio">
<Form.Control let:attrs>
<Form.Label>Bio</Form.Label>
<Textarea {...attrs} bind:value={$formData.bio} />
</Form.Control>
<Form.Description>
You can <span>@mention</span> other users and organizations to link to them.
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<div>
<Form.Fieldset {form} name="urls">
<Form.Legend>URLs</Form.Legend>
{#each $formData.urls as _, i}
<Form.ElementField {form} name="urls[{i}]">
<Form.Description class={cn(i !== 0 && "sr-only")}>
Add links to your website, blog, or social media profiles.
</Form.Description>
<Form.Control let:attrs>
<Input {...attrs} bind:value={$formData.urls[i]} />
</Form.Control>
<Form.FieldErrors />
</Form.ElementField>
{/each}
</Form.Fieldset>
<Button type="button" variant="outline" size="sm" class="mt-2" on:click={addUrl}>
Add URL
</Button>
</div>
<Form.Button>Update profile</Form.Button>
</form>
{#if browser}
<SuperDebug data={$formData} />
{/if}