Groups & other refactor

This commit is contained in:
glazk0 2024-03-28 19:59:47 +01:00
parent ea13815932
commit 3a90dd2ace
No known key found for this signature in database
GPG key ID: E45BF177782B9FEB
26 changed files with 703 additions and 217 deletions

View file

@ -12,21 +12,21 @@
class={cn(
'group relative flex h-full w-full flex-col rounded border border-border bg-card transition-colors duration-150',
{
'hover:bg-card/80': chapter.show,
'opacity-50': !chapter.show
'hover:bg-card/80': chapter.show || (chapter.start && chapter.end),
'opacity-50': !chapter.show && !(chapter.start && chapter.end)
}
)}
>
{#if chapter.show}
{#if chapter.show || (chapter.start && chapter.end)}
<a
class="flex h-full w-full items-center justify-between gap-4 p-4"
href={chapter.show ? `/chapters/${chapter.id}` : '#'}
href={chapter.show || (chapter.start && chapter.end) ? `/chapters/${chapter.id}` : null}
>
<div class="flex items-center gap-2">
<span class="font-semibold">
{chapter.name}
</span>
{#if chapter.id === 1}
{#if chapter.start && chapter.end}
<Swords class="stroke-muted-foreground" />
{/if}
</div>

View file

@ -9,6 +9,9 @@
import LogOut from 'lucide-svelte/icons/log-out';
import Settings from 'lucide-svelte/icons/settings';
import Users from 'lucide-svelte/icons/users';
import Shield from 'lucide-svelte/icons/shield';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import Code from 'lucide-svelte/icons/code';
import * as Avatar from '$lib/components/ui/avatar';
import { Button } from '$lib/components/ui/button';
@ -33,6 +36,29 @@
<DropdownMenu.Label
>Salutation, <span class="text-primary">{$page.data.user?.pseudo}</span></DropdownMenu.Label
>
{#if $page.data.user?.email.endsWith('@peerat.dev')}
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<Shield class="mr-2 h-4 w-4" />
<span>Administration</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item href="/admin/logs">
<ScrollText class="mr-2 h-4 w-4" />
<span>Logs</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/admin/chapters">
<Code class="mr-2 h-4 w-4" />
<span>Chapitres</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/admin/puzzles">
<Code class="mr-2 h-4 w-4" />
<span>Puzzles</span>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
<DropdownMenu.Separator />
<DropdownMenu.Item href="/settings">
<Settings class="mr-2 h-4 w-4" />
@ -43,10 +69,10 @@
<Award class="mr-2 h-4 w-4" />
<span>Mes badges</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/teams">
<!-- <DropdownMenu.Item href="/groups">
<Users class="mr-2 h-4 w-4" />
<span>Mes équipes</span>
</DropdownMenu.Item>
</DropdownMenu.Item> -->
<DropdownMenu.Separator />
<DropdownMenu.Item href="/git" target="_blank">
<Github class="mr-2 h-4 w-4" />

View file

@ -49,6 +49,8 @@ export interface Chapter {
name: string;
puzzles: Puzzle[];
show?: boolean;
start?: string;
end?: string;
}
export interface Tag {

View file

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

View file

@ -2,6 +2,7 @@
import { navigating } from '$app/stores';
import { Loader, Navbar, Sidenav } from '$lib/components/layout';
import { Toaster } from '$lib/components/ui/sonner';
</script>
{#if $navigating}
@ -17,6 +18,7 @@
class="flex w-full flex-1 transform flex-col overflow-y-auto p-4 duration-300 ease-in-out"
>
<slot />
<Toaster position="top-right" />
</div>
</div>
</div>

View file

@ -2,6 +2,7 @@
import type { PageData } from './$types';
import Card from '$lib/components/card.svelte';
import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData;
@ -29,7 +30,7 @@
</p>
</header>
<div
class="flex items-center justify-between gap-4 rounded border border-border bg-card px-4 py-2"
class="flex items-center justify-between gap-4 rounded border border-primary bg-card px-4 py-2"
>
<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.daily.chapter.name}</span>
@ -37,13 +38,9 @@
{data.daily.puzzle.name} ({data.daily.puzzle.score ?? '?'}/{data.daily.puzzle.scoreMax})
</span>
</div>
<div class="flex items-center gap-4">
<a href="/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}">
<span class="text-lg font-semibold">
<Button href="/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}">
{data.daily.puzzle.score ? 'Voir' : 'Jouer'}
</span>
</a>
</div>
</Button>
</div>
{/if}
<div class="grid grid-cols-1 gap-4">
@ -55,7 +52,7 @@
</p>
</header>
<div
class="border bg-card h-full max-h-96 overflow-y-scroll rounded border-border p-4 shadow-md"
class="h-full max-h-96 overflow-y-scroll rounded border border-border bg-card p-4 shadow-md"
>
<ul class="flex flex-col space-y-2">
{#if user?.completionsList?.length}

View file

@ -17,7 +17,7 @@
};
</script>
<section class="flex w-full flex-col gap-4">
<section class="flex w-full flex-col gap-2">
<header class="flex items-center justify-between">
<div class="flex flex-col">
<h2 class="text-xl font-semibold">Chapitres</h2>
@ -27,7 +27,7 @@
</p>
</div>
</header>
<ul class="flex flex-col gap-4">
<ul class="flex flex-col gap-2">
{#each chapters as chapter (chapter.id)}
<Chapter {chapter} />
{/each}

View file

@ -23,10 +23,12 @@ export const load = (async ({ locals: { user }, fetch, cookies, params: { chapte
const chapter = (await res.json()) as Chapter;
if (!chapter || !chapter.show) {
if (!chapter || !chapter.show && !(chapter.start && chapter.end)) {
redirect(302, '/chapters');
}
if (chapter?.puzzles?.length) chapter.puzzles = chapter.puzzles.sort((a, b) => a.scoreMax - b.scoreMax);
return {
title: chapter.name,
chapter

View file

@ -1,30 +1,52 @@
<script lang="ts">
import type { PageData } from './$types';
import BarChart2 from 'lucide-svelte/icons/bar-chart-2';
import Users from 'lucide-svelte/icons/users';
import Puzzle from '$lib/components/puzzle.svelte';
import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData;
data.chapter.puzzles = data.chapter.puzzles.sort((a, b) => a.scoreMax - b.scoreMax);
</script>
<section class="flex w-full flex-col gap-4">
<header class="flex items-center justify-between">
<section class="flex w-full flex-col gap-2">
<header class="flex flex-col justify-between gap-2 lg:flex-row lg:items-center">
<div class="flex flex-col">
<h2 class="text-xl font-semibold">{data.chapter.name}</h2>
{#if data.chapter?.puzzles?.length}
<p class="text-muted-foreground">
Ils vous restent {data.chapter.puzzles.filter((p) => p.score).length} puzzles à résoudre sur
un total de {data.chapter.puzzles.length}
</p>
</div>
{#if data.chapter.id === 1}
<Button href="/leaderboard/{data.chapter.id}">Voir le classement</Button>
{:else}
<p class="text-muted-foreground">Le chapitre ne contient pour l'instant aucun puzzle</p>
{/if}
</div>
<div class="flex gap-2">
<Button href="/chapters/{data.chapter.id}/groups">
<Users class="mr-2 h-4 w-4" />
Voir les groupes
</Button>
{#if data.chapter.start && data.chapter.end}
<Button href="/leaderboard/{data.chapter.id}">
<BarChart2 class="mr-2 h-4 w-4" />
Voir le classement
</Button>
{/if}
</div>
</header>
<ul class="flex flex-col gap-4">
<ul class="flex flex-col gap-2">
{#if data.chapter?.puzzles?.length}
{#each data.chapter.puzzles as puzzle (puzzle.id)}
<Puzzle {puzzle} />
{:else}
<li
class="flex h-16 w-full items-center justify-center rounded border border-border bg-card"
>
<p class="text-muted-foreground">Aucun puzzle trouvé</p>
</li>
{/each}
{/if}
</ul>
</section>

View file

@ -0,0 +1,142 @@
import { API_URL } from '$env/static/private';
import type { Actions, PageServerLoad } from './$types';
import type { Chapter, Group } from '$lib/types';
import { redirect } from '@sveltejs/kit';
export const load = (async ({ locals: { user }, fetch, cookies, params: { chapterId } }) => {
if (!user) redirect(302, '/login');
const session = cookies.get('session');
let res = await fetch(`${API_URL}/chapter/${chapterId}`, {
headers: {
Authorization: `Bearer ${session}`
}
});
if (!res.ok) {
redirect(302, '/chapters');
}
const chapter = (await res.json()) as Chapter;
if (!chapter || !chapter.show && !(chapter.start && chapter.end)) {
redirect(302, '/chapters');
}
res = await fetch(`${API_URL}/groups/${chapter.id}`, {
headers:
{
Authorization: `Bearer ${session}`
}
});
if (!res.ok) {
redirect(302, `/chapters/${chapterId}`);
}
const groups = (await res.json()) as Group[];
return {
title: `${chapter.name} - Groups`,
chapter,
groups
};
}) satisfies PageServerLoad;
export const actions: Actions = {
join: async ({ fetch, params: { chapterId }, cookies, request }) => {
const data = await request.formData();
const name = data.get('name') as string;
const session = cookies.get('session');
const res = await fetch(`${API_URL}/groupJoin`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session}`
},
body: JSON.stringify({
name,
chapter: parseInt(chapterId),
})
});
if (res.ok) {
return {
success: true,
message: 'Vous avez rejoint le groupe'
}
}
if (res.status === 403) {
return {
success: false,
message: 'Vous êtes déjà dans un groupe'
};
}
if (res.status === 423) {
return {
success: false,
message: 'Vous ne pouvez plus rejoindre de groupe'
};
}
return {
success: false,
message: "Une erreur s'est produite"
};
},
leave: async ({ fetch, params: { chapterId }, cookies, request }) => {
const data = await request.formData();
const name = data.get('name') as string;
const session = cookies.get('session');
const res = await fetch(`${API_URL}/groupQuit`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session}`
},
body: JSON.stringify({
name,
chapter: parseInt(chapterId),
})
});
if (res.ok) {
return {
success: true,
message: 'Vous avez quitté le groupe'
}
}
if (res.status === 403) {
return {
success: false,
message: "Vous n'êtes pas dans ce groupe"
};
}
if (res.status === 423) {
return {
success: false,
message: "Vous ne pouvez pas quitter ce groupe maintenant"
};
}
return {
success: false,
message: "Une erreur s'est produite"
};
}
};

View file

@ -0,0 +1,136 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { ActionData, PageData } from './$types';
import Code from 'lucide-svelte/icons/code';
import Minus from 'lucide-svelte/icons/minus';
import Plus from 'lucide-svelte/icons/plus';
import Users from 'lucide-svelte/icons/users';
import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import { Loader2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
export let data: PageData;
export let form: ActionData;
let name = '';
let submitting = false;
$: filteredGroups = data.groups.filter((group) => {
const regex = new RegExp(name, 'i');
return regex.test(group.name);
});
$: hasGroup = $page.data.user?.groups.some((g) =>
data.groups.map((g) => g.name).includes(g.name)
);
$: if (form?.message) {
toast(form.message);
}
</script>
<section class="flex w-full flex-col gap-2">
<header class="flex flex-col justify-between gap-2 lg:flex-row lg:items-center">
<div class="flex flex-col">
<h2 class="text-xl font-semibold">{data.chapter.name}</h2>
<p class="text-muted-foreground">
Vous pouvez créer ou rejoindre un groupe pour participer à la complétion de ce chapitre
</p>
</div>
<div class="flex gap-2">
<Button disabled={hasGroup} on:click={() => goto(`/chapters/${data.chapter.id}/groups/new`)}>
<Users class="mr-2 h-4 w-4" />
Créer un groupe
</Button>
<Button href="/chapters/{data.chapter.id}">
<Code class="mr-2 h-4 w-4" />
Retourner aux puzzles
</Button>
</div>
</header>
<div class="flex">
<Input placeholder="Quarter Master" bind:value={name} />
</div>
<ul class="flex flex-col gap-2">
{#if filteredGroups.length === 0}
<li class="flex h-16 w-full items-center justify-center rounded border border-border bg-card">
<p class="text-muted-foreground">Aucun groupe trouvé</p>
</li>
{:else}
{#each filteredGroups as group (group.name)}
<li class="group relative flex h-full w-full flex-col rounded border border-border bg-card">
<div class="flex h-full w-full items-center justify-between gap-4 p-4">
<div class="flex items-center gap-2">
<span class="font-semibold">
{group.name}
</span>
</div>
<div>
{#if $page.data.user?.groups.some((g) => g.name === group.name)}
<form
method="post"
action="?/leave"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
submitting = true;
return async ({ result, update }) => {
switch (result.type) {
case 'success':
await update({ invalidateAll: true });
break;
}
submitting = false;
};
}}
>
<input type="hidden" name="name" value={group.name} />
<Button disabled={submitting} variant="destructive" type="submit">
{#if submitting}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Minus class="mr-2 h-4 w-4" />
{/if}
Quitter
</Button>
</form>
{/if}
{#if !$page.data.user?.groups.some((g) => data.groups
.map((g) => g.name)
.includes(g.name))}
<form
method="post"
action="?/join"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
submitting = true;
return async ({ result, update }) => {
switch (result.type) {
case 'success':
await update({ invalidateAll: true });
break;
}
submitting = false;
};
}}
>
<input type="hidden" name="name" value={group.name} />
<Button disabled={submitting} variant="outline" type="submit">
{#if submitting}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Plus class="mr-2 h-4 w-4" />
{/if}
Rejoindre
</Button>
</form>
{/if}
</div>
</div>
</li>
{/each}
{/if}
</ul>
</section>

View file

@ -0,0 +1,67 @@
import { fail, redirect, type Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import { API_URL } from "$env/static/private";
import { groupSchema } from "$lib/validations/group";
export const load: PageServerLoad = async ({ params: { chapterId }, locals: { user } }) => {
if (!user) redirect(302, '/login');
if (user.groups.find(g => g.chapter === parseInt(chapterId))) {
redirect(302, `/chapters/${chapterId}/groups`);
}
const form = await superValidate(zod(groupSchema));
return {
title: 'Nouveau groupe',
form
};
};
export const actions: Actions = {
default: async ({ locals: { user }, fetch, request, cookies, params: { chapterId } }) => {
if (!user) redirect(302, '/login');
if (!chapterId) redirect(302, '/chapters');
const session = cookies.get('session');
const form = await superValidate(request, zod(groupSchema));
if (!form.valid) {
return fail(400, { form });
}
const res = await fetch(`${API_URL}/groupCreate`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session}`
},
body: JSON.stringify({
...form.data,
chapter: parseInt(chapterId)
})
});
if (!res.ok) {
if (res.status === 403) {
form.errors.name = ["Vous êtes déjà dans un groupe"];
} else if (res.status === 423) {
form.errors.name = ["Vous ne pouvez plus créer de groupe"];
} else {
form.errors.name = ["Une erreur est survenue, veuillez réessayer plus tard"];
}
return fail(400, { form });
}
redirect(302, `/chapters/${chapterId}/groups`);
}
};

View file

@ -0,0 +1,43 @@
<script lang="ts">
import type { PageData } from './$types';
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import * as Form from '$lib/components/ui/form';
import Input from '$lib/components/ui/input/input.svelte';
import { groupSchema } from '$lib/validations/group';
import Loader from 'lucide-svelte/icons/loader-circle';
export let data: PageData;
const form = superForm(data.form, {
validators: zodClient(groupSchema),
delayMs: 500,
multipleSubmits: 'prevent'
});
const { form: formData, enhance, delayed } = form;
</script>
<section>
<div class="flex flex-col gap-4">
<h2 class="text-xl font-bold">Nouveau groupe</h2>
<form class="flex flex-col justify-center gap-2" method="POST" use:enhance>
<Form.Field {form} name="name">
<Form.Control let:attrs>
<Form.Label>Nom du groupe</Form.Label>
<Input {...attrs} bind:value={$formData.name} placeholder="Peerat sailors" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Button type="submit" disabled={$delayed}>
{#if $delayed}
<Loader class="mr-2 h-4 w-4 animate-spin" />
{/if}
Créer
</Form.Button>
</form>
</div>
</section>

View file

@ -45,6 +45,7 @@
toast.message('Réponse vide', {
description: 'Vous devez entrer une réponse'
});
submitting = false;
return cancel();
}
@ -107,7 +108,7 @@
<Input name="code_file" type="file" accept=".py,.js,.ts,.java,.rs,.c" disabled />
</div>
</div>
<Button class="w-full sm:w-44" disabled={submitting}>
<Button type="submit" class="w-full sm:w-44" disabled={submitting}>
{#if submitting}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if}

View file

@ -24,7 +24,7 @@ export const load = (async ({ locals: { user }, fetch, cookies, params: { chapte
const leaderboard = (await res.json()) as LeaderboardEvent;
if (!leaderboard || !leaderboard.groups.length) redirect(302, '/');
if (!leaderboard) redirect(302, '/');
return {
title: "Classement",

View file

@ -1,18 +1,30 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { page } from '$app/stores';
import type { PageData } from './$types';
import Code from 'lucide-svelte/icons/code';
import { cn } from '$lib/utils';
import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData;
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
</script>
<section class="flex h-full w-full flex-col gap-4">
<header class="sticky flex items-center justify-between">
<header class="flex flex-col justify-between gap-2 lg:flex-row lg:items-center">
<div class="flex flex-col">
<h2 class="text-xl font-semibold">Tableau des scores</h2>
<p class="text-muted-foreground">Suivez la progression des élèves en direct</p>
</div>
<div class="flex gap-2">
<Button href="/chapters/{$page.params.chapterId}">
<Code class="mr-2 h-4 w-4" />
Retourner au chapitre
</Button>
</div>
</header>
<main class="flex flex-col justify-between gap-4 pb-4">
<div class="overflow-x-auto">
@ -28,6 +40,13 @@
</tr>
</thead>
<tbody class="border-x border-b border-border bg-card align-middle">
{#if !data.leaderboard.groups.length}
<tr>
<td colspan="4" class="text-center text-muted-foreground"
>Aucun groupe n'a encore de score</td
>
</tr>
{:else}
{#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])}>
<td>{group.rank}</td>
@ -42,6 +61,7 @@
<td class="text-right">{group.players.reduce((a, b) => a + b.tries, 0)}</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
let routes = [
{
name: 'Général',
href: '/settings'
}
];
</script>
<div class="flex flex-col gap-4 lg:flex-row">
<div class="overflow-x-auto lg:min-w-52 lg:overflow-y-auto">
<ul class="flex gap-2 lg:flex-col">
{#each routes as { name, href } (name)}
<li>
<Button class="w-full" variant="outline" {href}>{name}</Button>
</li>
{/each}
</ul>
</div>
<div class="flex flex-1 flex-col lg:pl-4">
<slot />
</div>
</div>

View file

@ -7,15 +7,9 @@ import { superValidate } from 'sveltekit-superforms/server';
import { z } from 'zod';
const settingSchema = z.object({
firstname: z.string({
required_error: 'Prénom manquant'
}),
lastname: z.string({
required_error: 'Nom manquant'
}),
pseudo: z.string({
required_error: 'Pseudo manquant'
})
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 } }) => {

View file

@ -5,9 +5,10 @@
import { toast } from 'svelte-sonner';
import { superForm } from 'sveltekit-superforms/client';
import plausible from '$lib/stores/plausible';
import Input from '$lib/components/ui/input/input.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import plausible from '$lib/stores/plausible';
export let data: PageData;
@ -26,26 +27,14 @@
}
});
$: optedOut = $plausible;
// TODO: Handle overflow X on small screens
</script>
TODO
<div class="flex gap-4 flex-col md:flex-row h-full w-full overflow-hidden">
<div class="overflow-x-auto md:min-w-52 md:overflow-y-auto">
<ul class="flex md:flex-col gap-2">
<li>
<Button class="w-full" variant="outline" href="/settings">Général</Button>
</li>
</ul>
</div>
<div class="flex h-full w-full flex-1 flex-col overflow-auto md:pl-4">
<section>
<form class="flex flex-col gap-4" method="POST" use:enhance>
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold">Paramètres généraux</h2>
<Button variant="default" disabled={submitting}>
<h2 class="text-xl font-bold">Paramètres généraux</h2>
<Button type="submit" variant="default" disabled={submitting}>
{#if submitting}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if}
@ -104,9 +93,9 @@ TODO
class="h-4 w-4"
name="optout"
type="checkbox"
bind:value={optedOut}
on:change={() => plausible.set(!optedOut)}
checked={optedOut}
bind:value={$plausible}
on:change={() => plausible.set(!$plausible)}
checked={$plausible}
/>
</div>
@ -114,5 +103,4 @@ TODO
Nous utilisons Plausible pour analyser l'utilisation de notre site web de manière anonyme.
</p>
</form>
</div>
</div>
</section>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Toaster } from '$lib/components/ui/sonner';
</script>
<slot />
<Toaster />

View file

@ -2,7 +2,7 @@
import type { PageData } from './$types';
import { Loader2 } from 'lucide-svelte';
import { zod } from 'sveltekit-superforms/adapters';
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import * as Form from '$lib/components/ui/form';
@ -13,7 +13,7 @@
export let data: PageData;
const form = superForm(data.form, {
validators: zod(loginSchema),
validators: zodClient(loginSchema),
delayMs: 500,
multipleSubmits: 'prevent',
onResult: ({ result, formElement }) => {
@ -28,7 +28,7 @@
const { form: formData, enhance, delayed } = form;
</script>
<div class="flex h-screen container">
<div class="container flex h-screen">
<div class="flex w-full flex-col items-center justify-center">
<div class="flex w-full max-w-xs flex-col gap-4">
<h1 class="mx-auto text-xl font-bold">Connexion</h1>
@ -43,7 +43,12 @@
<Form.Field {form} name="passwd">
<Form.Control let:attrs>
<Form.Label>Mot de passe</Form.Label>
<Input {...attrs} type="password" bind:value={$formData.passwd} placeholder="********" />
<Input
{...attrs}
type="password"
bind:value={$formData.passwd}
placeholder="********"
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
@ -56,7 +61,9 @@
</form>
<ul class="flex justify-between">
<li>
<a class="font-medium text-muted-foreground hover:text-primary" href="/register"> S'inscrire </a>
<a class="font-medium text-muted-foreground hover:text-primary" href="/register">
S'inscrire
</a>
</li>
<li>
<a class="font-medium text-muted-foreground hover:text-primary" href="/reset-password">

View file

@ -35,25 +35,14 @@ export const actions = {
const res = await fetch(`${API_URL}/register`, {
method: 'POST',
body: JSON.stringify({
pseudo: form.data.pseudo,
firstname: form.data.firstname,
lastname: form.data.lastname,
email: form.data.email
})
});
if (res.ok) {
return {
form
};
success: true
}
if (res.status === 404) {
form.errors.pseudo = [`L'inscription est désactivée.`];
return fail(400, {
form
});
}
if (res.status === 400) {
@ -112,12 +101,18 @@ export const actions = {
}
if (res.status === 400) {
try {
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 });
} catch (e) {
console.error(e);
form.errors.code = ['Le code envoyé est invalide.'];
return fail(400, { form });
}
}
form.errors.code = [`Le code envoyé est invalide.`];

View file

@ -2,22 +2,23 @@
import { fade } from 'svelte/transition';
import type { PageData, Snapshot } from './$types';
import { Loader2 } from 'lucide-svelte';
import Loader from 'lucide-svelte/icons/loader-circle';
import { toast } from 'svelte-sonner';
import { zod } from 'sveltekit-superforms/adapters';
import { zod, zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import * as Form from '$lib/components/ui/form';
import Input from '$lib/components/ui/input/input.svelte';
import { registerConfirmationSchema, registerSchema } from '$lib/validations/auth';
import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData;
let confirmation = false;
const registerForm = superForm(data.registerForm, {
validators: zod(registerSchema),
validators: zodClient(registerSchema),
delayMs: 500,
multipleSubmits: 'prevent',
onResult({ result }) {
@ -27,14 +28,15 @@
description: `Un code vous à été ${confirmation ? 'renvoyé' : 'envoyé'}.`
});
registerConfirmationFormData.set({
email: $registerFormData.email,
firstname: $registerFormData.firstname,
lastname: $registerFormData.lastname,
pseudo: $registerFormData.pseudo,
...$registerFormData,
passwd: '',
code: ''
});
confirmation = true;
setTimeout(() => {
const field = document.querySelector('input[name="passwd"]');
(field as HTMLInputElement)?.focus();
}, 100);
break;
case 'error':
confirmation = false;
@ -93,21 +95,33 @@
<Form.Field form={registerConfirmationForm} name="firstname">
<Form.Control let:attrs>
<Form.Label>Prénom</Form.Label>
<Input {...attrs} bind:value={$registerConfirmationFormData.firstname} placeholder="Philip" />
<Input
{...attrs}
bind:value={$registerConfirmationFormData.firstname}
placeholder="Philip"
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={registerConfirmationForm} name="lastname">
<Form.Control let:attrs>
<Form.Label>Nom de famille</Form.Label>
<Input {...attrs} bind:value={$registerConfirmationFormData.lastname} placeholder="Barlow" />
<Input
{...attrs}
bind:value={$registerConfirmationFormData.lastname}
placeholder="Barlow"
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={registerConfirmationForm} name="pseudo">
<Form.Control let:attrs>
<Form.Label>Nom d'utilisateur</Form.Label>
<Input {...attrs} bind:value={$registerConfirmationFormData.pseudo} placeholder="Cypherwolf" />
<Input
{...attrs}
bind:value={$registerConfirmationFormData.pseudo}
placeholder="Cypherwolf"
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
@ -142,31 +156,17 @@
</Form.Field>
</div>
<Form.Button disabled={$registerConfirmationDelayed}>
<Form.Button type="submit" disabled={$registerConfirmationDelayed}>
{#if $registerConfirmationDelayed}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
<Loader class="mr-2 h-4 w-4 animate-spin" />
{/if}
Continuer
</Form.Button>
</form>
<ul class="flex justify-between">
<li>
<a class="text-muted-foreground hover:text-primary" href="/login">Se connecter</a>
</li>
<li>
<button
on:click={() => {
toast.message('Demande de confirmation', {
description: 'Un code vous à été renvoyé.'
});
}}
formaction="?/register"
class="text-muted-foreground hover:text-primary"
>
<Button variant="link" formaction="?/register" on:click={() => toast('Code renvoyé')}>
Renvoyer le code
</button>
</li>
</ul>
</Button>
</form>
{:else}
<h1 class="mx-auto text-xl font-bold">Inscription</h1>
<form
@ -208,19 +208,21 @@
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Button disabled={$registerDelayed}>
<div class="flex flex-col gap-2">
<Form.Button type="submit" disabled={$registerDelayed}>
{#if $registerDelayed}
<Loader2 className="h-4 w-4 animate-spin" />
<Loader className="mr-2 h-4 w-4 animate-spin" />
{/if}
S'inscrire
</Form.Button>
</div>
</form>
{/if}
<ul class="flex justify-between">
<li>
<a class="text-muted-foreground hover:text-primary" href="/login">Se connecter</a>
</li>
</ul>
{/if}
</div>
</div>
</div>

View file

@ -4,24 +4,29 @@
import { Loader2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import * as Form from '$lib/components/ui/form';
import Input from '$lib/components/ui/input/input.svelte';
import { requestPasswordResetSchema } from '$lib/validations/auth';
export let data: PageData;
let confirmation = false;
const requestPasswordResetForm = superForm(data.requestPasswordResetForm, {
validators: zodClient(requestPasswordResetSchema),
delayMs: 500,
multipleSubmits: 'prevent',
onResult({ result }) {
switch (result.type) {
case 'success':
toast.success('Demande de confirmation', {
toast('Demande de confirmation', {
description: `Un code vous à été envoyé à ${$requestPasswordResetFormData.email}`
});
resetPasswordFormData.set({
email: $requestPasswordResetFormData.email,
...$requestPasswordResetFormData,
password: '',
code: ''
});
@ -55,8 +60,6 @@
delayed: resetPasswordDelayed
} = resetPasswordForm;
let confirmation = false;
export const snapshot: Snapshot = {
capture: () => confirmation,
restore: (value) => (confirmation = value)

View file

@ -2,11 +2,8 @@
import '../app.css';
import { Metadata } from '$lib/components';
import { Toaster } from "$lib/components/ui/sonner";
</script>
<Metadata />
<slot />
<Toaster />