feat: added crud

This commit is contained in:
Bart van der Braak 2024-02-09 03:08:40 +01:00
parent a43f74cc7b
commit abe1d003c6
37 changed files with 628 additions and 125 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -1,24 +1,79 @@
import type { PageServerLoad } from './$types';
import { superValidate } from 'sveltekit-superforms/server';
import { profileFormSchema } from './profile-form.svelte';
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 {
form: await superValidate(profileFormSchema)
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 = {
default: async (event) => {
const form = await superValidate(event, profileFormSchema);
name: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, nameFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
return {
form
};
await locals.pocketBase.collection('users').update(locals.id, form.data);
return { form };
},
emailRequest: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
console.log(request);
const form = await superValidate(request, emailRequestFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase.collection('users').requestEmailChange(form.data.newEmail);
return { form };
},
emailConfirm: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, emailConfirmFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase
.collection('users')
.confirmEmailChange(form.data.token, form.data.password);
return { form };
},
password: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, passwordFormSchema);
console.log(form);
if (!form.valid) {
return fail(400, {
form
});
}
await locals.pocketBase.collection('users').update(locals.id, form.data);
return { form };
},
avatar: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const formData = await request.formData();
const form = await superValidate(request, avatarFormSchema);
if (!form.valid) return fail(400, { form });
const file = formData.get('file');
if (file instanceof File) {
await locals.pocketBase.collection('users').update(locals.id, { avatar: file });
}
return { form };
}
};

View file

@ -1,10 +1,13 @@
<script lang="ts">
import type { PageData } from './$types';
import ProfileForm from './profile-form.svelte';
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;
export let { form, user } = data;
let { forms, user } = data;
</script>
<div class="space-y-6">
@ -13,5 +16,19 @@
<p class="text-sm text-muted-foreground">Update your account and profile settings.</p>
</div>
<Separator />
<ProfileForm data={form} {user} />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-[4fr,3fr]">
<div class="space-y-8">
<NameForm nameFormSchemaData={forms.name} {user} debug={forms.debug} />
<EmailForm
emailRequestFormSchemaData={forms.emailRequest}
emailConfirmFormSchemaData={forms.emailConfirm}
{user}
debug={forms.debug}
/>
<PasswordForm passwordFormSchemaData={forms.password} debug={forms.debug} />
</div>
<div>
<AvatarForm avatarFormSchemaData={forms.avatar} {user} debug={forms.debug} />
</div>
</div>
</div>

View file

@ -12,7 +12,6 @@ export const load: PageServerLoad = async () => {
export const actions: Actions = {
default: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const form = await superValidate(request, appearanceFormSchema);
console.log('form: ', form);
if (!form.valid) {
return fail(400, {
form
@ -21,5 +20,6 @@ export const actions: Actions = {
await locals.pocketBase
.collection('users')
.update(locals.id, { appearanceMode: form.data.theme });
return { form };
}
};

View file

@ -15,5 +15,5 @@
</p>
</div>
<Separator />
<AppearanceForm data={form} {user} />
<AppearanceForm data={form} {user} />
</div>

View file

@ -16,12 +16,28 @@
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"
@ -33,7 +49,11 @@
<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}>
<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">
@ -101,5 +121,5 @@
</Form.RadioGroup>
</Form.Field>
</Form.Item>
<Form.Button>Update preferences</Form.Button>
<Form.Button disabled={isLoading}>Update preferences</Form.Button>
</Form.Root>

View file

@ -1,90 +0,0 @@
<script lang="ts" context="module">
import { z } from 'zod';
export const profileFormSchema = 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'),
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 enter a valid email' }).email(),
avatar: z.any().refine((val) => val.length > 0, 'File is required')
});
export type ProfileFormSchema = typeof profileFormSchema;
</script>
<script lang="ts">
import * as Form from '$lib/components/ui/form';
import type { SuperValidated } from 'sveltekit-superforms';
import { dev } from '$app/environment';
import type { LayoutData } from '../$types';
import * as Avatar from '$lib/components/ui/avatar';
export let data: SuperValidated<ProfileFormSchema>;
export let user: LayoutData['user'];
</script>
<Form.Root
form={data}
schema={profileFormSchema}
let:config
method="POST"
class="space-y-8"
debug={dev ? true : false}
>
<div class="grid grid-cols-[1fr,16rem] gap-4">
<div>
<Form.Item>
<Form.Field name="name" {config}>
<Form.Label>Name</Form.Label>
<Form.Input placeholder={user?.name} />
<Form.Description>
This is the name that will be displayed on your profile and in emails.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="username">
<Form.Label>Username</Form.Label>
<Form.Input placeholder={user?.username} />
<Form.Description>
This is your public display name. It can be your real name or a pseudonym.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="email">
<Form.Label>Email</Form.Label>
<Form.Input placeholder={user?.email} />
<Form.Description>
<Form.Description>
This is the email address associated with your account.
</Form.Description>
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
</div>
<Form.Item>
<Form.Field {config} name="avatar">
<Form.Label>Profile Picture</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" placeholder={user?.email} />
<Form.Description>
<Form.Description>Your avatar image displayed.</Form.Description>
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
</div>
<Form.Button>Update profile</Form.Button>
</Form.Root>