Refactor UI & Actions #16

Merged
glazk0 merged 1 commit from dev into main 2024-03-30 00:20:46 +01:00
20 changed files with 254 additions and 305 deletions

View file

@ -3,13 +3,17 @@
import * as Breadcrumb from '$lib/components/ui/breadcrumb'; import * as Breadcrumb from '$lib/components/ui/breadcrumb';
$: segments = $page.url.pathname.slice(1).split('/'); export let breadcrumb: { name: string; href: string }[] = [];
$: breadcrumb = segments.map((segment, index) => {
return { $: page.subscribe(({ url: { pathname } }) => {
name: segment.charAt(0).toUpperCase() + segment.slice(1), breadcrumb = pathname
href: '/' + segments.slice(0, index + 1).join('/') .split('/')
}; .slice(1)
}) as { name: string; href: string }[]; .map((segment, index, segments) => ({
name: segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
href: '/' + segments.slice(0, index + 1).join('/')
}));
});
</script> </script>
<Breadcrumb.Root> <Breadcrumb.Root>

View file

@ -30,6 +30,7 @@
<Boring name={$page.data.user?.pseudo} variant="beam" /> <Boring name={$page.data.user?.pseudo} variant="beam" />
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
<span class="sr-only">Menu utilisateur</span>
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56"> <DropdownMenu.Content class="w-56">

View file

@ -6,12 +6,10 @@
<nav class="w-full border-b border-muted p-4"> <nav class="w-full border-b border-muted p-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Breadcrumb />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<MobileNav />
<Breadcrumb />
</div>
<div class="flex items-center">
<NavbarUser /> <NavbarUser />
<MobileNav />
</div> </div>
</div> </div>
</nav> </nav>

View file

@ -1,19 +1,17 @@
import { z } from 'zod'; import { z } from 'zod';
export const loginSchema = z.object({ export const loginSchema = z.object({
pseudo: z.string({ required_error: "Nom d'utilisateur requis", }) pseudo: z.string()
.trim() .trim()
.min(1, { message: "Nom d'utilisateur requis" }), .min(1, { message: "Nom d'utilisateur requis" }),
passwd: z.string({ required_error: 'Mot de passe requis' }) passwd: z.string()
.trim() .trim()
.min(1, { message: 'Mot de passe requis' }), .min(1, { message: 'Mot de passe requis' }),
}); });
export const registerSchema = z.object({ export const registerSchema = z.object({
email: z email: z
.string({ .string()
required_error: 'Email requis'
})
.trim() .trim()
.max(64, { .max(64, {
message: 'Email trop long (max 64 caractères)' message: 'Email trop long (max 64 caractères)'
@ -22,20 +20,15 @@ export const registerSchema = z.object({
message: 'Email invalide' message: 'Email invalide'
}), }),
firstname: z.string() firstname: z.string()
.trim(), .trim().min(1, { message: "Prénom requis" }),
lastname: z.string() lastname: z.string()
.trim(), .trim().min(1, { message: "Nom requis" }),
pseudo: z.string({ pseudo: z.string().trim().min(1, { message: "Nom d'utilisateur requis" }),
required_error: 'Nom d\'utilisateur requis'
}).trim(),
}); });
export const registerConfirmationSchema = z.object({ export const registerConfirmationSchema = z.object({
email: z email: z
.string({ .string()
required_error: 'Email requis'
})
.trim() .trim()
.max(64, { .max(64, {
message: 'Email trop long (max 64 caractères)' message: 'Email trop long (max 64 caractères)'
@ -44,41 +37,61 @@ export const registerConfirmationSchema = z.object({
message: 'Email invalide' message: 'Email invalide'
}), }),
firstname: z.string() firstname: z.string()
.trim(), .trim()
.min(1, { message: "Prénom requis" }),
lastname: z.string() lastname: z.string()
.trim(), .trim()
pseudo: z.string({ .min(1, { message: "Nom requis" }),
required_error: 'Nom d\'utilisateur requis' pseudo: z.string().trim().min(1, { message: "Nom d'utilisateur requis" }),
}).trim(), passwd: z.string()
passwd: z.string({
required_error: 'Mot de passe requis'
})
.trim() .trim()
.min(1, { message: 'Mot de passe requis' }), .min(1, { message: 'Mot de passe requis' }),
code: z.string({ confirm: z.string()
required_error: 'Code manquant' .trim()
}) .min(1, { message: 'Confirmation du mot de passe requise' }),
code: z.string()
.trim()
.regex(/^[0-9]{4}$/, { message: 'Code invalide, il doit contenir 4 chiffres' }) .regex(/^[0-9]{4}$/, { message: 'Code invalide, il doit contenir 4 chiffres' })
.trim(), }).refine((data) => data.passwd == data.confirm, {
message: 'Les mots de passe ne correspondent pas',
path: ['confirm']
}); });
export const requestPasswordResetSchema = z.object({ export const requestPasswordResetSchema = z.object({
email: z.string({ required_error: 'Email requis' }) email: z.string()
.trim() .trim()
.email({ message: 'Email invalide' }) .max(64, {
.min(1, { message: 'Email requis' }) message: 'Email trop long (max 64 caractères)'
})
.email({
message: 'Email invalide'
}),
}); });
export const resetPasswordSchema = z.object({ export const resetPasswordSchema = z.object({
email: z.string({ required_error: 'Email requis' }) email: z.string()
.trim() .trim()
.email({ message: 'Email invalide' }) .max(64, {
.min(1, { message: 'Email requis' }), message: 'Email trop long (max 64 caractères)'
password: z.string({ required_error: 'Mot de passe requis' }) })
.email({
message: 'Email invalide'
}),
password: z.string()
.trim() .trim()
.min(1, { message: 'Mot de passe requis' }), .min(1, { message: 'Mot de passe requis' }),
code: z.string({ confirm: z.string()
required_error: 'Code manquant' .trim()
}) .min(1, { message: 'Confirmation du mot de passe requise' }),
.regex(/^[0-9]{4}$/, { message: 'Code invalide, il doit contenir 4 chiffres' }), code: z.string()
.regex(/^[0-9]{4}$/, { message: 'Code invalide, il doit contenir 4 chiffres' }),
}).refine((data) => data.password == data.confirm, {
message: 'Les mots de passe ne correspondent pas',
path: ['confirm']
});
export const settingSchema = z.object({
firstname: z.string().trim().min(1, { message: "Prénom requis" }),
lastname: z.string().trim().min(1, { message: "Nom requis" }),
pseudo: z.string().trim().min(1, { message: "Nom d'utilisateur requis" }),
}); });

View file

@ -1,8 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
export const groupSchema = z.object({ export const groupSchema = z.object({
name: z.string({ name: z.string().trim().min(1, 'Un nom de groupe est requis'),
required_error: 'Un nom est requis',
})
.min(1, 'Un nom est requis',),
}); });

View file

@ -1,8 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
export const puzzleSchema = z.object({ export const puzzleSchema = z.object({
answer: z.string({ answer: z.string()
required_error: 'Une réponse est requise',
})
.min(1, 'Une réponse est requise') .min(1, 'Une réponse est requise')
}); });

View file

@ -34,15 +34,7 @@
> >
<div class="flex w-full flex-col justify-between md:flex-row md:items-center md:gap-4"> <div class="flex w-full flex-col justify-between md:flex-row md:items-center md:gap-4">
<span class="text-lg font-semibold">{data.event.name}</span> <span class="text-lg font-semibold">{data.event.name}</span>
{#if data.event.start && data.event.end} <span class="text-muted-foreground"> Participer en équipe de 1 à 4 joueurs </span>
<span class="text-muted-foreground">
{new Date(data.event.start).toLocaleDateString()} - {new Date(
data.event.end
).toLocaleDateString()}
</span>
{:else}
<span class="text-muted-foreground">Aucune date</span>
{/if}
</div> </div>
<Button href="/chapters/{data.event.id}">Participer</Button> <Button href="/chapters/{data.event.id}">Participer</Button>
</div> </div>

View file

@ -29,7 +29,7 @@
Voir les groupes Voir les groupes
</Button> </Button>
{#if data.chapter.start && data.chapter.end} {#if data.chapter.start && data.chapter.end}
<Button href="/leaderboard/{data.chapter.id}"> <Button href="/chapters/{data.chapter.id}/leaderboard">
<BarChart2 class="mr-2 h-4 w-4" /> <BarChart2 class="mr-2 h-4 w-4" />
Voir le classement Voir le classement
</Button> </Button>

View file

@ -1,12 +1,10 @@
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { LeaderboardEvent } from '$lib/types'; import type { LeaderboardEvent } from '$lib/types';
import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals: { user }, fetch, cookies, params: { chapterId } }) => {
export const load = (async ({ locals: { user }, fetch, cookies, params: { chapterId } }) => {
if (!user) redirect(302, '/login'); if (!user) redirect(302, '/login');
@ -20,14 +18,18 @@ export const load = (async ({ locals: { user }, fetch, cookies, params: { chapte
} }
}); });
if (!res.ok) redirect(302, '/'); if (!res.ok) return {
leaderboard: []
}
const leaderboard = (await res.json()) as LeaderboardEvent; const leaderboard = (await res.json()) as LeaderboardEvent;
if (!leaderboard) redirect(302, '/'); if (!leaderboard) return {
leaderboard: []
}
return { return {
title: "Classement", title: "Classement",
leaderboard leaderboard
}; };
}) satisfies PageServerLoad; };

View file

@ -26,14 +26,15 @@
</Button> </Button>
</div> </div>
</header> </header>
<main class="flex flex-col justify-between gap-4 pb-4"> <main class="pb-4">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full min-w-max table-auto"> <table class="w-full min-w-max table">
<thead <thead
class="border-x border-b border-t border-border bg-card/50 text-sm text-muted-foreground" class="border-x border-b border-t border-border bg-card/50 text-sm text-muted-foreground"
> >
<tr> <tr>
<th scope="col" class="text-left">#</th> <th scope="col" class="text-left">#</th>
<th scope="col" class="text-left">Equipe</th>
<th scope="col" class="text-left">Joueurs</th> <th scope="col" class="text-left">Joueurs</th>
<th scope="col" class="text-right">Score</th> <th scope="col" class="text-right">Score</th>
<th scope="col" class="text-right">Essais</th> <th scope="col" class="text-right">Essais</th>
@ -42,14 +43,15 @@
<tbody class="border-x border-b border-border bg-card align-middle"> <tbody class="border-x border-b border-border bg-card align-middle">
{#if !data.leaderboard.groups.length} {#if !data.leaderboard.groups.length}
<tr> <tr>
<td colspan="4" class="text-center text-muted-foreground" <td colspan="5" class="text-center text-muted-foreground">
>Aucun groupe n'a encore de score</td Aucun groupe n'a encore de score
> </td>
</tr> </tr>
{:else} {:else}
{#each data.leaderboard.groups.filter( (g) => g.players.reduce((a, b) => a + b.score, 0) ) as group (group)} {#each data.leaderboard.groups.filter( (g) => g.players.reduce((a, b) => a + b.score, 0) ) as group (group)}
<tr class={cn(SCORE_COLORS[group.rank - 1])}> <tr class={cn(SCORE_COLORS[group.rank - 1])}>
<td>{group.rank}</td> <td>{group.rank}</td>
<td>{group.name}</td>
<td class="text-lg"> <td class="text-lg">
{#if group.players?.length} {#if group.players?.length}
<span>{group.players.map((player) => player?.pseudo).join(', ')} </span> <span>{group.players.map((player) => player?.pseudo).join(', ')} </span>

View file

@ -19,7 +19,7 @@ export const load = (async ({ locals: { user }, fetch, cookies }) => {
if (!res.ok) { if (!res.ok) {
return { return {
leaderboard: [] as Leaderboard[] leaderboard: []
}; };
} }

View file

@ -15,10 +15,12 @@
<p class="text-muted-foreground">Suivez la progression des élèves en direct</p> <p class="text-muted-foreground">Suivez la progression des élèves en direct</p>
</div> </div>
</header> </header>
<main class="flex flex-col justify-between gap-4 pb-4"> <main class="pb-4">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full min-w-max table-auto"> <table class="table w-full min-w-max">
<thead class="border-x border-b text-muted-foreground border-t border-border bg-card/50 text-sm"> <thead
class="border-x border-b border-t border-border bg-card/50 text-sm text-muted-foreground"
>
<tr> <tr>
<th scope="col" class="text-left">#</th> <th scope="col" class="text-left">#</th>
<th scope="col" class="text-left">Nom</th> <th scope="col" class="text-left">Nom</th>

View file

@ -2,17 +2,11 @@ import { API_URL } from '$env/static/private';
import { fail, redirect, type Actions } from '@sveltejs/kit'; import { fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { settingSchema } from '$lib/validations/auth';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { z } from 'zod';
const settingSchema = z.object({ export const load: PageServerLoad = async ({ locals: { user } }) => {
firstname: z.string().min(1, { message: "Prénom requis" }),
lastname: z.string().min(1, { message: "Nom requis" }),
pseudo: z.string().min(1, { message: "Nom d'utilisateur requi" }),
});
export const load = (async ({ locals: { user } }) => {
if (!user) redirect(302, '/login'); if (!user) redirect(302, '/login');
const form = await superValidate(user, zod(settingSchema)); const form = await superValidate(user, zod(settingSchema));
@ -21,10 +15,13 @@ export const load = (async ({ locals: { user } }) => {
title: 'Paramètres', title: 'Paramètres',
form form
}; };
}) satisfies PageServerLoad; };
export const actions: Actions = {
default: async ({ request, cookies, locals: { user } }) => {
if (!user) return fail(401);
export const actions = {
default: async ({ request, cookies }) => {
const session = cookies.get('session'); const session = cookies.get('session');
const form = await superValidate(request, zod(settingSchema)); const form = await superValidate(request, zod(settingSchema));
@ -38,21 +35,20 @@ export const actions = {
headers: { headers: {
Authorization: `Bearer ${session}` Authorization: `Bearer ${session}`
}, },
body: JSON.stringify(form.data) body: JSON.stringify({
...form.data
})
}); });
if (res.ok) { if (!res.ok) {
return { if (res.status === 400) {
success: true return setError(form, "pseudo", "Ce pseudo est déjà utilisé");
}; }
return setError(form, "pseudo", "Une erreur est survenue lors de la sauvegarde des paramètres");
} }
if (res.status === 400) { return {
form.errors.pseudo = ['Le pseudo est déjà utilisé']; form
};
return fail(400, { form });
}
return fail(500, { form });
} }
} satisfies Actions; };

View file

@ -1,106 +1,76 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { Loader2 } from 'lucide-svelte'; import Loader from 'lucide-svelte/icons/loader-circle';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client'; import { superForm } from 'sveltekit-superforms/client';
import Button from '$lib/components/ui/button/button.svelte'; import * as Form from '$lib/components/ui/form';
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import plausible from '$lib/stores/plausible';
import { settingSchema } from '$lib/validations/auth';
export let data: PageData; export let data: PageData;
let submitting = false; const form = superForm(data.form, {
validators: zodClient(settingSchema),
const { form, errors, enhance } = superForm(data.form, { delayMs: 500,
onSubmit() { multipleSubmits: 'prevent',
submitting = true;
},
onResult({ result }) { onResult({ result }) {
if (result.type === 'success') { if (result.type === 'success') {
toast.message('Succès', { description: 'Vos informations ont été mises à jour.' }); toast.message('Succès', { description: 'Vos informations ont été mises à jour.' });
} }
submitting = false;
} }
}); });
const { form: formData, enhance, delayed } = form;
// TODO: Handle overflow X on small screens // TODO: Handle overflow X on small screens
</script> </script>
<section> <section>
<form class="flex flex-col gap-4" method="POST" use:enhance> <form class="flex flex-col gap-2" method="POST" use:enhance>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-bold">Paramètres généraux</h2> <h2 class="text-xl font-bold">Paramètres généraux</h2>
<Button type="submit" variant="default" disabled={submitting}> <Form.Button disabled={$delayed}>
{#if submitting} {#if $delayed}
<Loader2 class="mr-2 h-4 w-4 animate-spin" /> <Loader class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Modifier Modifier
</Button> </Form.Button>
</div> </div>
<Label>Email</Label>
<label for="email">Email</label>
<Input <Input
name="email"
type="email" type="email"
placeholder="philipzcwbarlow@peerat.dev" value={$page.data.user?.email}
value={data.user?.email}
disabled disabled
placeholder="philiphzcwbarlow@peerat.dev"
/> />
<p class="text-sm text-muted-foreground">Votre email ne peut pas être modifié.</p>
<label for="firstname">Prénom</label> <Form.Field {form} name="firstname">
<Input <Form.Control let:attrs>
name="firstname" <Form.Label>Prénom</Form.Label>
type="text" <Input {...attrs} bind:value={$formData.firstname} placeholder="Philip" />
placeholder="Philip" </Form.Control>
aria-invalid={$errors.firstname ? 'true' : undefined} <Form.FieldErrors />
bind:value={$form.firstname} </Form.Field>
/> <Form.Field {form} name="lastname">
{#if $errors.firstname} <Form.Control let:attrs>
<span class="text-sm text-red-500">{$errors.firstname}</span> <Form.Label>Nom de famille</Form.Label>
{/if} <Input {...attrs} bind:value={$formData.lastname} placeholder="Barlow" />
</Form.Control>
<label for="lastname">Nom</label> <Form.FieldErrors />
<Input </Form.Field>
name="lastname" <Form.Field {form} name="pseudo">
type="text" <Form.Control let:attrs>
placeholder="Barlow" <Form.Label>Nom d'utilisateur</Form.Label>
aria-invalid={$errors.lastname ? 'true' : undefined} <Input {...attrs} bind:value={$formData.pseudo} placeholder="Cypherwolf" />
bind:value={$form.lastname} </Form.Control>
/> <Form.Description>Ce nom sera visible par les autres utilisateurs.</Form.Description>
{#if $errors.lastname} <Form.FieldErrors />
<span class="text-sm text-red-500">{$errors.lastname}</span> </Form.Field>
{/if}
<label for="pseudo"> Nom d'utilisateur </label>
<Input
name="pseudo"
type="text"
placeholder="Cypher Wolf"
aria-invalid={$errors.pseudo ? 'true' : undefined}
bind:value={$form.pseudo}
/>
{#if $errors.pseudo}
<span class="text-sm text-red-500">{$errors.pseudo}</span>
{/if}
<div class="flex items-center justify-between">
<label for="optout"> Ne pas me tracer de manière anonyme </label>
<input
class="h-4 w-4"
name="optout"
type="checkbox"
bind:value={$plausible}
on:change={() => plausible.set(!$plausible)}
checked={$plausible}
/>
</div>
<p class="text-highlight-secondary text-sm">
Nous utilisons Plausible pour analyser l'utilisation de notre site web de manière anonyme.
</p>
</form> </form>
</section> </section>

View file

@ -4,7 +4,7 @@ import { fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { loginSchema } from '$lib/validations/auth'; import { loginSchema } from '$lib/validations/auth';
@ -37,19 +37,13 @@ export const actions: Actions = {
}); });
if (!res.ok) { if (!res.ok) {
return setError(form, 'passwd', "Nom d'utilisateur ou mot de passe incorrect");
form.errors.passwd = ["Nom d'utilisateur ou mot de passe incorrect"];
return fail(400, { form });
} }
const token = res.headers.get('Authorization')?.split(' ').pop(); const token = res.headers.get('Authorization')?.split('Bearer ').pop();
if (!token) { if (!token) {
return setError(form, 'passwd', "Une erreur est survenue, veuillez réessayer plus tard");
form.errors.passwd = ["Une erreur est survenue, veuillez réessayer plus tard"];
return fail(500, { form });
} }
cookies.set('session', token, { cookies.set('session', token, {

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { Loader2 } from 'lucide-svelte'; import Loader from 'lucide-svelte/icons/loader-circle';
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client'; import { superForm } from 'sveltekit-superforms/client';
@ -31,7 +31,7 @@
<div class="container flex h-screen"> <div class="container flex h-screen">
<div class="flex w-full flex-col items-center justify-center"> <div class="flex w-full flex-col items-center justify-center">
<div class="flex w-full max-w-xs flex-col gap-4"> <div class="flex w-full max-w-xs flex-col gap-4">
<h1 class="mx-auto text-xl font-bold">Connexion</h1> <h2 class="mx-auto text-xl font-bold">Connexion</h2>
<form class="flex flex-col justify-center gap-2" method="POST" use:enhance> <form class="flex flex-col justify-center gap-2" method="POST" use:enhance>
<Form.Field {form} name="pseudo"> <Form.Field {form} name="pseudo">
<Form.Control let:attrs> <Form.Control let:attrs>
@ -54,7 +54,7 @@
</Form.Field> </Form.Field>
<Form.Button disabled={$delayed}> <Form.Button disabled={$delayed}>
{#if $delayed} {#if $delayed}
<Loader2 class="mr-2 h-4 w-4 animate-spin" /> <Loader class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Se connecter Se connecter
</Form.Button> </Form.Button>

View file

@ -6,12 +6,11 @@ import { fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { registerConfirmationSchema, registerSchema } from '$lib/validations/auth'; import { registerConfirmationSchema, registerSchema } from '$lib/validations/auth';
export const load: PageServerLoad = async ({ locals: { user } }) => {
export const load = (async ({ locals: { user } }) => {
if (user) redirect(302, '/'); if (user) redirect(302, '/');
const registerForm = await superValidate(zod(registerSchema)); const registerForm = await superValidate(zod(registerSchema));
@ -22,10 +21,11 @@ export const load = (async ({ locals: { user } }) => {
registerForm, registerForm,
registerConfirmationForm registerConfirmationForm
}; };
}) satisfies PageServerLoad; }
export const actions = { export const actions: Actions = {
register: async ({ request }) => { register: async ({ request }) => {
const form = await superValidate(request, zod(registerSchema)); const form = await superValidate(request, zod(registerSchema));
if (!form.valid) { if (!form.valid) {
@ -39,26 +39,19 @@ export const actions = {
}) })
}); });
if (res.ok) { if (!res.ok) {
return { if (res.status === 400) {
success: true const { email_valid, username_valid } = await res.json();
}; if (!email_valid) return setError(form, 'email', 'Un compte avec cette adresse email existe déjà');
if (!username_valid) return setError(form, 'pseudo', 'Ce pseudo est déjà utilisé');
}
return setError(form, 'email', "Une erreur est survenue lors de l'inscription");
} }
if (res.status === 400) { return {
const { email_valid, username_valid } = await res.json();
if (!email_valid) form.errors.email = ['Un compte avec cette adresse email existe déjà'];
if (!username_valid) form.errors.pseudo = ['Ce pseudo est déjà utilisé'];
return fail(400, { form });
}
form.errors.pseudo = ["Une erreur s'est produite"];
return fail(400, {
form form
}); };
}, },
confirmation: async ({ request, cookies }) => { confirmation: async ({ request, cookies }) => {
const form = await superValidate(request, zod(registerConfirmationSchema)); const form = await superValidate(request, zod(registerConfirmationSchema));
@ -79,46 +72,27 @@ export const actions = {
}) })
}); });
if (res.ok) { if (!res.ok) {
const token = res.headers.get('Authorization')?.split('Bearer ')[1]; if (res.status === 400) {
if (!token) {
form.errors.code = [`Une erreur s'est produite lors de la confirmation de votre compte.`];
return fail(400, {
form
});
}
cookies.set('session', token, {
path: '/',
secure: !dev,
sameSite: 'strict',
});
redirect(302, '/');
}
if (res.status === 400) {
try {
const { email_valid, username_valid } = await res.json(); const { email_valid, username_valid } = await res.json();
if (!email_valid) return setError(form, 'email', 'Un compte avec cette adresse email existe déjà');
if (email_valid) form.errors.email = ['Un compte avec cette adresse email existe déjà']; if (!username_valid) return setError(form, 'pseudo', "Ce nom d'utilisateur est déjà utilisé");
if (username_valid) form.errors.pseudo = ['Ce pseudo est déjà utilisé'];
return fail(400, { form });
} catch (e) {
console.error(e);
form.errors.code = ['Le code envoyé est invalide.'];
return fail(400, { form });
} }
return setError(form, 'code', "Une erreur est survenue lors de la confirmation");
} }
form.errors.code = [`Le code envoyé est invalide.`]; const token = res.headers.get('Authorization')?.split('Bearer ').pop();
return fail(400, { if (!token) {
form return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
}
cookies.set('session', token, {
path: '/',
secure: !dev,
sameSite: 'strict',
}); });
}
} satisfies Actions; redirect(302, '/');
},
}

View file

@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { PageData, Snapshot } from './$types'; import type { PageData } from './$types';
import Loader from 'lucide-svelte/icons/loader-circle'; import Loader from 'lucide-svelte/icons/loader-circle';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { zod, zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client'; import { superForm } from 'sveltekit-superforms/client';
import * as Form from '$lib/components/ui/form'; import * as Form from '$lib/components/ui/form';
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import { registerConfirmationSchema, registerSchema } from '$lib/validations/auth'; import { registerConfirmationSchema, registerSchema } from '$lib/validations/auth';
import Button from '$lib/components/ui/button/button.svelte'; import { enhance } from '$app/forms';
export let data: PageData; export let data: PageData;
@ -21,6 +21,7 @@
validators: zodClient(registerSchema), validators: zodClient(registerSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
invalidateAll: false,
onResult({ result }) { onResult({ result }) {
switch (result.type) { switch (result.type) {
case 'success': case 'success':
@ -30,6 +31,7 @@
registerConfirmationFormData.set({ registerConfirmationFormData.set({
...$registerFormData, ...$registerFormData,
passwd: '', passwd: '',
confirm: '',
code: '' code: ''
}); });
confirmation = true; confirmation = true;
@ -38,9 +40,6 @@
(field as HTMLInputElement)?.focus(); (field as HTMLInputElement)?.focus();
}, 100); }, 100);
break; break;
case 'error':
confirmation = false;
break;
} }
} }
}); });
@ -52,7 +51,7 @@
} = registerForm; } = registerForm;
const registerConfirmationForm = superForm(data.registerConfirmationForm, { const registerConfirmationForm = superForm(data.registerConfirmationForm, {
validators: zod(registerConfirmationSchema), validators: zodClient(registerConfirmationSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent' multipleSubmits: 'prevent'
}); });
@ -62,18 +61,13 @@
enhance: registerConfirmationEnhance, enhance: registerConfirmationEnhance,
delayed: registerConfirmationDelayed delayed: registerConfirmationDelayed
} = registerConfirmationForm; } = registerConfirmationForm;
export const snapshot: Snapshot = {
capture: () => confirmation,
restore: (value) => (confirmation = value)
};
</script> </script>
<div class="container flex h-screen"> <div class="container flex h-screen">
<div class="flex w-full flex-col items-center justify-center"> <div class="flex w-full flex-col items-center justify-center">
<div class="flex w-full max-w-xs flex-col gap-4"> <div class="flex w-full max-w-xs flex-col gap-4">
{#if confirmation} {#if confirmation}
<h1 class="mx-auto text-xl font-bold">Confirmation</h1> <h2 class="mx-auto text-xl font-bold">Confirmation</h2>
<form <form
class="flex flex-col justify-center gap-2" class="flex flex-col justify-center gap-2"
method="POST" method="POST"
@ -143,6 +137,18 @@
</Form.Control> </Form.Control>
<Form.FieldErrors /> <Form.FieldErrors />
</Form.Field> </Form.Field>
<Form.Field form={registerConfirmationForm} name="confirm">
<Form.Control let:attrs>
<Form.Label>Confirmer le mot de passe</Form.Label>
<Input
{...attrs}
type="password"
bind:value={$registerConfirmationFormData.confirm}
placeholder="************"
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={registerConfirmationForm} name="code"> <Form.Field form={registerConfirmationForm} name="code">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label>Code</Form.Label> <Form.Label>Code</Form.Label>
@ -156,16 +162,12 @@
</Form.Field> </Form.Field>
</div> </div>
<Form.Button type="submit" disabled={$registerConfirmationDelayed}> <Form.Button disabled={$registerConfirmationDelayed}>
{#if $registerConfirmationDelayed} {#if $registerConfirmationDelayed}
<Loader class="mr-2 h-4 w-4 animate-spin" /> <Loader class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Continuer Continuer
</Form.Button> </Form.Button>
<!-- <Button variant="link" formaction="?/register" on:click={() => toast('Code renvoyé')}>
Renvoyer le code
</Button> -->
</form> </form>
{:else} {:else}
<h1 class="mx-auto text-xl font-bold">Inscription</h1> <h1 class="mx-auto text-xl font-bold">Inscription</h1>
@ -208,14 +210,12 @@
</Form.Control> </Form.Control>
<Form.FieldErrors /> <Form.FieldErrors />
</Form.Field> </Form.Field>
<div class="flex flex-col gap-2"> <Form.Button disabled={$registerDelayed}>
<Form.Button type="submit" disabled={$registerDelayed}> {#if $registerDelayed}
{#if $registerDelayed} <Loader className="mr-2 h-4 w-4 animate-spin" />
<Loader className="mr-2 h-4 w-4 animate-spin" /> {/if}
{/if} S'inscrire
S'inscrire </Form.Button>
</Form.Button>
</div>
</form> </form>
{/if} {/if}
<ul class="flex justify-between"> <ul class="flex justify-between">

View file

@ -6,7 +6,7 @@ import { fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { requestPasswordResetSchema, resetPasswordSchema } from '$lib/validations/auth'; import { requestPasswordResetSchema, resetPasswordSchema } from '$lib/validations/auth';
@ -23,7 +23,7 @@ export const load = (async ({ locals: { user } }) => {
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
export const actions = { export const actions: Actions = {
request: async ({ request, fetch }) => { request: async ({ request, fetch }) => {
const form = await superValidate(request, zod(requestPasswordResetSchema)); const form = await superValidate(request, zod(requestPasswordResetSchema));
@ -33,16 +33,17 @@ export const actions = {
const res = await fetch(`${API_URL}/user/fpw`, { const res = await fetch(`${API_URL}/user/fpw`, {
method: 'POST', method: 'POST',
body: JSON.stringify(form.data) body: JSON.stringify({
...form.data
})
}); });
if (!res.ok) { if (!res.ok) {
form.errors.email = ["Une erreur s'est produite ou l'email n'existe pas"]; return setError(form, 'email', "Une erreur s'est produite ou l'email n'existe pas");
return fail(400, { form });
} }
return { return {
success: true form
} }
}, },
confirmation: async ({ request, cookies, fetch }) => { confirmation: async ({ request, cookies, fetch }) => {
@ -62,11 +63,10 @@ export const actions = {
}); });
if (res.ok) { if (res.ok) {
const token = res.headers.get('Authorization')?.split('Bearer ')[1]; const token = res.headers.get('Authorization')?.split('Bearer ').pop();
if (!token) { if (!token) {
form.errors.code = ["Une erreur s'est produite"]; return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
return fail(400, { form });
} }
cookies.set('session', token, { cookies.set('session', token, {
@ -79,13 +79,9 @@ export const actions = {
} }
if (res.status === 400) { if (res.status === 400) {
form.errors.code = ['Code invalide']; return setError(form, 'code', "Le code de confirmation est incorrect");
} else {
form.errors.code = [`Une erreur s'est produite`];
} }
return fail(400, { return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
form
});
} }
} satisfies Actions; }

View file

@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { PageData, Snapshot } from './$types'; import type { PageData } from './$types';
import { Loader2 } from 'lucide-svelte'; import Loader from 'lucide-svelte/icons/loader-circle';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client'; import { superForm } from 'sveltekit-superforms/client';
import * as Form from '$lib/components/ui/form'; import * as Form from '$lib/components/ui/form';
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import { requestPasswordResetSchema } from '$lib/validations/auth'; import { requestPasswordResetSchema, resetPasswordSchema } from '$lib/validations/auth';
export let data: PageData; export let data: PageData;
@ -19,6 +19,7 @@
validators: zodClient(requestPasswordResetSchema), validators: zodClient(requestPasswordResetSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
invalidateAll: false,
onResult({ result }) { onResult({ result }) {
switch (result.type) { switch (result.type) {
case 'success': case 'success':
@ -28,6 +29,7 @@
resetPasswordFormData.set({ resetPasswordFormData.set({
...$requestPasswordResetFormData, ...$requestPasswordResetFormData,
password: '', password: '',
confirm: '',
code: '' code: ''
}); });
confirmation = true; confirmation = true;
@ -50,6 +52,7 @@
} = requestPasswordResetForm; } = requestPasswordResetForm;
const resetPasswordForm = superForm(data.resetPasswordForm, { const resetPasswordForm = superForm(data.resetPasswordForm, {
validators: zodClient(resetPasswordSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent' multipleSubmits: 'prevent'
}); });
@ -59,18 +62,13 @@
enhance: resetPasswordEnhance, enhance: resetPasswordEnhance,
delayed: resetPasswordDelayed delayed: resetPasswordDelayed
} = resetPasswordForm; } = resetPasswordForm;
export const snapshot: Snapshot = {
capture: () => confirmation,
restore: (value) => (confirmation = value)
};
</script> </script>
<div class="container flex h-screen"> <div class="container flex h-screen">
<div class="flex w-full flex-col items-center justify-center"> <div class="flex w-full flex-col items-center justify-center">
<div class="flex w-full max-w-xs flex-col gap-4"> <div class="flex w-full max-w-xs flex-col gap-4">
{#if confirmation} {#if confirmation}
<h1 class="mx-auto text-xl font-bold">Confirmation</h1> <h2 class="mx-auto text-xl font-bold">Confirmation</h2>
<form <form
class="flex flex-col justify-center gap-2" class="flex flex-col justify-center gap-2"
method="POST" method="POST"
@ -107,6 +105,18 @@
</Form.Control> </Form.Control>
<Form.FieldErrors /> <Form.FieldErrors />
</Form.Field> </Form.Field>
<Form.Field form={resetPasswordForm} name="confirm">
<Form.Control let:attrs>
<Form.Label>Confirmer le mot de passe</Form.Label>
<Input
{...attrs}
type="password"
bind:value={$resetPasswordFormData.confirm}
placeholder="************"
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={resetPasswordForm} name="code"> <Form.Field form={resetPasswordForm} name="code">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label>Code</Form.Label> <Form.Label>Code</Form.Label>
@ -118,7 +128,7 @@
<Form.Button disabled={$resetPasswordDelayed}> <Form.Button disabled={$resetPasswordDelayed}>
{#if $resetPasswordDelayed} {#if $resetPasswordDelayed}
<Loader2 class="mr-2 h-4 w-4 animate-spin" /> <Loader class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Changer le mot de passe Changer le mot de passe
</Form.Button> </Form.Button>
@ -157,7 +167,7 @@
</Form.Field> </Form.Field>
<Form.Button disabled={$requestPasswordResetDelayed}> <Form.Button disabled={$requestPasswordResetDelayed}>
{#if $requestPasswordResetDelayed} {#if $requestPasswordResetDelayed}
<Loader2 class="mr-2 h-4 w-4 animate-spin" /> <Loader class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Continuer Continuer
</Form.Button> </Form.Button>