Compare commits

..

No commits in common. "49f2c09648b5d86467daacc12de83ebaedda18c1" and "c72d70b7729dc9001dab0ba2e868e2671335c005" have entirely different histories.

20 changed files with 304 additions and 253 deletions

View file

@ -3,17 +3,13 @@
import * as Breadcrumb from '$lib/components/ui/breadcrumb'; import * as Breadcrumb from '$lib/components/ui/breadcrumb';
export let breadcrumb: { name: string; href: string }[] = []; $: segments = $page.url.pathname.slice(1).split('/');
$: breadcrumb = segments.map((segment, index) => {
$: page.subscribe(({ url: { pathname } }) => { return {
breadcrumb = pathname name: segment.charAt(0).toUpperCase() + segment.slice(1),
.split('/') href: '/' + segments.slice(0, index + 1).join('/')
.slice(1) };
.map((segment, index, segments) => ({ }) as { name: string; href: string }[];
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,7 +30,6 @@
<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,10 +6,12 @@
<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">
<NavbarUser />
<MobileNav /> <MobileNav />
<Breadcrumb />
</div>
<div class="flex items-center">
<NavbarUser />
</div> </div>
</div> </div>
</nav> </nav>

View file

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

View file

@ -1,6 +1,8 @@
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,7 +34,15 @@
> >
<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>
<span class="text-muted-foreground"> Participer en équipe de 1 à 4 joueurs </span> {#if data.event.start && data.event.end}
<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="/chapters/{data.chapter.id}/leaderboard"> <Button href="/leaderboard/{data.chapter.id}">
<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

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

View file

@ -15,12 +15,10 @@
<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="pb-4"> <main class="flex flex-col justify-between gap-4 pb-4">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table w-full min-w-max"> <table class="w-full min-w-max table-auto">
<thead <thead class="border-x border-b text-muted-foreground border-t border-border bg-card/50 text-sm">
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

@ -1,10 +1,12 @@
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';
export const load: PageServerLoad = async ({ locals: { user }, fetch, cookies, params: { chapterId } }) => { import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user }, fetch, cookies, params: { chapterId } }) => {
if (!user) redirect(302, '/login'); if (!user) redirect(302, '/login');
@ -18,18 +20,14 @@ export const load: PageServerLoad = async ({ locals: { user }, fetch, cookies, p
} }
}); });
if (!res.ok) return { if (!res.ok) redirect(302, '/');
leaderboard: []
}
const leaderboard = (await res.json()) as LeaderboardEvent; const leaderboard = (await res.json()) as LeaderboardEvent;
if (!leaderboard) return { if (!leaderboard) redirect(302, '/');
leaderboard: []
}
return { return {
title: "Classement", title: "Classement",
leaderboard leaderboard
}; };
}; }) satisfies PageServerLoad;

View file

@ -26,15 +26,14 @@
</Button> </Button>
</div> </div>
</header> </header>
<main class="pb-4"> <main class="flex flex-col justify-between gap-4 pb-4">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full min-w-max table"> <table class="w-full min-w-max table-auto">
<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>
@ -43,15 +42,14 @@
<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="5" class="text-center text-muted-foreground"> <td colspan="4" class="text-center text-muted-foreground"
Aucun groupe n'a encore de score >Aucun groupe n'a encore de score</td
</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

@ -2,11 +2,17 @@ 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 { setError, superValidate } from 'sveltekit-superforms/server'; import { superValidate } from 'sveltekit-superforms/server';
import { z } from 'zod';
export const load: PageServerLoad = async ({ locals: { user } }) => { const settingSchema = z.object({
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));
@ -15,13 +21,10 @@ export const load: PageServerLoad = 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));
@ -35,20 +38,21 @@ export const actions: Actions = {
headers: { headers: {
Authorization: `Bearer ${session}` Authorization: `Bearer ${session}`
}, },
body: JSON.stringify({ body: JSON.stringify(form.data)
...form.data
})
}); });
if (!res.ok) { if (res.ok) {
if (res.status === 400) { return {
return setError(form, "pseudo", "Ce pseudo est déjà utilisé"); success: true
} };
return setError(form, "pseudo", "Une erreur est survenue lors de la sauvegarde des paramètres");
} }
return { if (res.status === 400) {
form form.errors.pseudo = ['Le pseudo est déjà utilisé'];
};
return fail(400, { form });
}
return fail(500, { form });
} }
}; } satisfies Actions;

View file

@ -1,76 +1,106 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores';
import type { PageData } from './$types'; import type { PageData } from './$types';
import Loader from 'lucide-svelte/icons/loader-circle'; import { Loader2 } from 'lucide-svelte';
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 * as Form from '$lib/components/ui/form'; import Button from '$lib/components/ui/button/button.svelte';
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 { settingSchema } from '$lib/validations/auth'; import plausible from '$lib/stores/plausible';
export let data: PageData; export let data: PageData;
const form = superForm(data.form, { let submitting = false;
validators: zodClient(settingSchema),
delayMs: 500, const { form, errors, enhance } = superForm(data.form, {
multipleSubmits: 'prevent', onSubmit() {
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-2" method="POST" use:enhance> <form class="flex flex-col gap-4" 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>
<Form.Button disabled={$delayed}> <Button type="submit" variant="default" disabled={submitting}>
{#if $delayed} {#if submitting}
<Loader class="mr-2 h-4 w-4 animate-spin" /> <Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Modifier Modifier
</Form.Button> </Button>
</div> </div>
<Label>Email</Label>
<label for="email">Email</label>
<Input <Input
name="email"
type="email" type="email"
value={$page.data.user?.email} placeholder="philipzcwbarlow@peerat.dev"
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>
<Form.Field {form} name="firstname"> <label for="firstname">Prénom</label>
<Form.Control let:attrs> <Input
<Form.Label>Prénom</Form.Label> name="firstname"
<Input {...attrs} bind:value={$formData.firstname} placeholder="Philip" /> type="text"
</Form.Control> placeholder="Philip"
<Form.FieldErrors /> aria-invalid={$errors.firstname ? 'true' : undefined}
</Form.Field> bind:value={$form.firstname}
<Form.Field {form} name="lastname"> />
<Form.Control let:attrs> {#if $errors.firstname}
<Form.Label>Nom de famille</Form.Label> <span class="text-sm text-red-500">{$errors.firstname}</span>
<Input {...attrs} bind:value={$formData.lastname} placeholder="Barlow" /> {/if}
</Form.Control>
<Form.FieldErrors /> <label for="lastname">Nom</label>
</Form.Field> <Input
<Form.Field {form} name="pseudo"> name="lastname"
<Form.Control let:attrs> type="text"
<Form.Label>Nom d'utilisateur</Form.Label> placeholder="Barlow"
<Input {...attrs} bind:value={$formData.pseudo} placeholder="Cypherwolf" /> aria-invalid={$errors.lastname ? 'true' : undefined}
</Form.Control> bind:value={$form.lastname}
<Form.Description>Ce nom sera visible par les autres utilisateurs.</Form.Description> />
<Form.FieldErrors /> {#if $errors.lastname}
</Form.Field> <span class="text-sm text-red-500">{$errors.lastname}</span>
{/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 { setError, superValidate } from 'sveltekit-superforms/server'; import { superValidate } from 'sveltekit-superforms/server';
import { loginSchema } from '$lib/validations/auth'; import { loginSchema } from '$lib/validations/auth';
@ -37,13 +37,19 @@ 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('Bearer ').pop(); const token = res.headers.get('Authorization')?.split(' ').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 Loader from 'lucide-svelte/icons/loader-circle'; import { Loader2 } from 'lucide-svelte';
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">
<h2 class="mx-auto text-xl font-bold">Connexion</h2> <h1 class="mx-auto text-xl font-bold">Connexion</h1>
<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}
<Loader class="mr-2 h-4 w-4 animate-spin" /> <Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Se connecter Se connecter
</Form.Button> </Form.Button>

View file

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

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 } from './$types'; import type { PageData, Snapshot } 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 { zodClient } from 'sveltekit-superforms/adapters'; import { zod, 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 { enhance } from '$app/forms'; import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData; export let data: PageData;
@ -21,7 +21,6 @@
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':
@ -31,7 +30,6 @@
registerConfirmationFormData.set({ registerConfirmationFormData.set({
...$registerFormData, ...$registerFormData,
passwd: '', passwd: '',
confirm: '',
code: '' code: ''
}); });
confirmation = true; confirmation = true;
@ -40,6 +38,9 @@
(field as HTMLInputElement)?.focus(); (field as HTMLInputElement)?.focus();
}, 100); }, 100);
break; break;
case 'error':
confirmation = false;
break;
} }
} }
}); });
@ -51,7 +52,7 @@
} = registerForm; } = registerForm;
const registerConfirmationForm = superForm(data.registerConfirmationForm, { const registerConfirmationForm = superForm(data.registerConfirmationForm, {
validators: zodClient(registerConfirmationSchema), validators: zod(registerConfirmationSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent' multipleSubmits: 'prevent'
}); });
@ -61,13 +62,18 @@
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}
<h2 class="mx-auto text-xl font-bold">Confirmation</h2> <h1 class="mx-auto text-xl font-bold">Confirmation</h1>
<form <form
class="flex flex-col justify-center gap-2" class="flex flex-col justify-center gap-2"
method="POST" method="POST"
@ -137,18 +143,6 @@
</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>
@ -162,12 +156,16 @@
</Form.Field> </Form.Field>
</div> </div>
<Form.Button disabled={$registerConfirmationDelayed}> <Form.Button type="submit" 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>
@ -210,12 +208,14 @@
</Form.Control> </Form.Control>
<Form.FieldErrors /> <Form.FieldErrors />
</Form.Field> </Form.Field>
<Form.Button disabled={$registerDelayed}> <div class="flex flex-col gap-2">
{#if $registerDelayed} <Form.Button type="submit" disabled={$registerDelayed}>
<Loader className="mr-2 h-4 w-4 animate-spin" /> {#if $registerDelayed}
{/if} <Loader className="mr-2 h-4 w-4 animate-spin" />
S'inscrire {/if}
</Form.Button> S'inscrire
</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 { setError, superValidate } from 'sveltekit-superforms/server'; import { 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: Actions = { export const actions = {
request: async ({ request, fetch }) => { request: async ({ request, fetch }) => {
const form = await superValidate(request, zod(requestPasswordResetSchema)); const form = await superValidate(request, zod(requestPasswordResetSchema));
@ -33,17 +33,16 @@ export const actions: Actions = {
const res = await fetch(`${API_URL}/user/fpw`, { const res = await fetch(`${API_URL}/user/fpw`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify(form.data)
...form.data
})
}); });
if (!res.ok) { if (!res.ok) {
return setError(form, 'email', "Une erreur s'est produite ou l'email n'existe pas"); form.errors.email = ["Une erreur s'est produite ou l'email n'existe pas"];
return fail(400, { form });
} }
return { return {
form success: true
} }
}, },
confirmation: async ({ request, cookies, fetch }) => { confirmation: async ({ request, cookies, fetch }) => {
@ -63,10 +62,11 @@ export const actions: Actions = {
}); });
if (res.ok) { if (res.ok) {
const token = res.headers.get('Authorization')?.split('Bearer ').pop(); const token = res.headers.get('Authorization')?.split('Bearer ')[1];
if (!token) { if (!token) {
return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard"); form.errors.code = ["Une erreur s'est produite"];
return fail(400, { form });
} }
cookies.set('session', token, { cookies.set('session', token, {
@ -79,9 +79,13 @@ export const actions: Actions = {
} }
if (res.status === 400) { if (res.status === 400) {
return setError(form, 'code', "Le code de confirmation est incorrect"); form.errors.code = ['Code invalide'];
} else {
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
});
} }
} } 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 } from './$types'; import type { PageData, Snapshot } from './$types';
import Loader from 'lucide-svelte/icons/loader-circle'; import { Loader2 } from 'lucide-svelte';
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, resetPasswordSchema } from '$lib/validations/auth'; import { requestPasswordResetSchema } from '$lib/validations/auth';
export let data: PageData; export let data: PageData;
@ -19,7 +19,6 @@
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':
@ -29,7 +28,6 @@
resetPasswordFormData.set({ resetPasswordFormData.set({
...$requestPasswordResetFormData, ...$requestPasswordResetFormData,
password: '', password: '',
confirm: '',
code: '' code: ''
}); });
confirmation = true; confirmation = true;
@ -52,7 +50,6 @@
} = requestPasswordResetForm; } = requestPasswordResetForm;
const resetPasswordForm = superForm(data.resetPasswordForm, { const resetPasswordForm = superForm(data.resetPasswordForm, {
validators: zodClient(resetPasswordSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent' multipleSubmits: 'prevent'
}); });
@ -62,13 +59,18 @@
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}
<h2 class="mx-auto text-xl font-bold">Confirmation</h2> <h1 class="mx-auto text-xl font-bold">Confirmation</h1>
<form <form
class="flex flex-col justify-center gap-2" class="flex flex-col justify-center gap-2"
method="POST" method="POST"
@ -105,18 +107,6 @@
</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>
@ -128,7 +118,7 @@
<Form.Button disabled={$resetPasswordDelayed}> <Form.Button disabled={$resetPasswordDelayed}>
{#if $resetPasswordDelayed} {#if $resetPasswordDelayed}
<Loader class="mr-2 h-4 w-4 animate-spin" /> <Loader2 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>
@ -167,7 +157,7 @@
</Form.Field> </Form.Field>
<Form.Button disabled={$requestPasswordResetDelayed}> <Form.Button disabled={$requestPasswordResetDelayed}>
{#if $requestPasswordResetDelayed} {#if $requestPasswordResetDelayed}
<Loader class="mr-2 h-4 w-4 animate-spin" /> <Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if} {/if}
Continuer Continuer
</Form.Button> </Form.Button>