mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-05-03 10:21:19 +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
src/routes
(dashboard)/settings
(user)
+layout.server.ts
dashboard
(components)
data-table-checkbox.sveltedata-table-column-header.sveltedata-table-faceted-filter.sveltedata-table-pagination.sveltedata-table-priority-cell.sveltedata-table-row-actions.sveltedata-table-status-cell.sveltedata-table-title-cell.sveltedata-table-toolbar.sveltedata-table-view-options.sveltedata-table.svelteindex.ts
(data)
+layout.svelte+page.sveltesettings
(components)
+layout.svelte+page.server.ts+page.svelteaccount
appearance
display
notifications
profile-form.svelte
|
@ -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">
|
||||
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">
|
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">
|
||||
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>
|
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">
|
||||
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>
|
|
@ -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