mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-05-03 18:21:20 +00:00
feat: re-import of settings
This commit is contained in:
parent
6e12f454b3
commit
28721b4ba5
46 changed files with 913 additions and 793 deletions
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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>
|
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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>
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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> -->
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from "$lib/utils";
|
||||||
import { page } from '$app/stores';
|
import { page } from "$app/stores";
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from "$lib/components/ui/button";
|
||||||
import { cubicInOut } from 'svelte/easing';
|
import { cubicInOut } from "svelte/easing";
|
||||||
import { crossfade } from 'svelte/transition';
|
import { crossfade } from "svelte/transition";
|
||||||
|
|
||||||
let className: string | undefined | null = undefined;
|
let className: string | undefined | null = undefined;
|
||||||
export let items: { href: string; title: string }[];
|
export let items: { href: string; title: string }[];
|
||||||
|
@ -11,25 +11,28 @@
|
||||||
|
|
||||||
const [send, receive] = crossfade({
|
const [send, receive] = crossfade({
|
||||||
duration: 250,
|
duration: 250,
|
||||||
easing: cubicInOut
|
easing: cubicInOut,
|
||||||
});
|
});
|
||||||
</script>
|
</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}
|
{#each items as item}
|
||||||
{@const isActive = $page.url.pathname === item.href}
|
{@const isActive = $page.url.pathname === item.href}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
href={item.href}
|
href={item.href}
|
||||||
variant="ghost"
|
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
|
data-sveltekit-noscroll
|
||||||
>
|
>
|
||||||
{#if isActive}
|
{#if isActive}
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 rounded-md bg-muted"
|
class="absolute inset-0 rounded-md bg-muted"
|
||||||
in:send={{ key: 'active-sidebar-tab' }}
|
in:send={{ key: "active-sidebar-tab" }}
|
||||||
out:receive={{ key: 'active-sidebar-tab' }}
|
out:receive={{ key: "active-sidebar-tab" }}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="relative">
|
<div class="relative">
|
45
src/routes/(user)/settings/+layout.svelte
Normal file
45
src/routes/(user)/settings/+layout.svelte
Normal 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>
|
25
src/routes/(user)/settings/+page.server.ts
Normal file
25
src/routes/(user)/settings/+page.server.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
15
src/routes/(user)/settings/+page.svelte
Normal file
15
src/routes/(user)/settings/+page.svelte
Normal 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>
|
26
src/routes/(user)/settings/account/+page.server.ts
Normal file
26
src/routes/(user)/settings/account/+page.server.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
18
src/routes/(user)/settings/account/+page.svelte
Normal file
18
src/routes/(user)/settings/account/+page.svelte
Normal 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>
|
179
src/routes/(user)/settings/account/account-form.svelte
Normal file
179
src/routes/(user)/settings/account/account-form.svelte
Normal 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}
|
25
src/routes/(user)/settings/appearance/+page.server.ts
Normal file
25
src/routes/(user)/settings/appearance/+page.server.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,10 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from "$lib/components/ui/separator";
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from "./$types";
|
||||||
import AppearanceForm from './appearance-form.svelte';
|
import AppearanceForm from "./appearance-form.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let { form, user } = data;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
@ -15,5 +14,5 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<AppearanceForm data={form} {user} />
|
<AppearanceForm data={data.form} />
|
||||||
</div>
|
</div>
|
132
src/routes/(user)/settings/appearance/appearance-form.svelte
Normal file
132
src/routes/(user)/settings/appearance/appearance-form.svelte
Normal 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}
|
25
src/routes/(user)/settings/display/+page.server.ts
Normal file
25
src/routes/(user)/settings/display/+page.server.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
17
src/routes/(user)/settings/display/+page.svelte
Normal file
17
src/routes/(user)/settings/display/+page.svelte
Normal 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>
|
94
src/routes/(user)/settings/display/display-form.svelte
Normal file
94
src/routes/(user)/settings/display/display-form.svelte
Normal 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}
|
25
src/routes/(user)/settings/notifications/+page.server.ts
Normal file
25
src/routes/(user)/settings/notifications/+page.server.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from "$lib/components/ui/separator";
|
||||||
import NotificationsForm from './notifications-form.svelte';
|
import NotificationsForm from "./notifications-form.svelte";
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
</script>
|
</script>
|
|
@ -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}
|
122
src/routes/(user)/settings/profile-form.svelte
Normal file
122
src/routes/(user)/settings/profile-form.svelte
Normal 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}
|
Loading…
Reference in a new issue