Groups & other refactor
This commit is contained in:
parent
ea13815932
commit
3a90dd2ace
26 changed files with 703 additions and 217 deletions
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -49,6 +49,8 @@ export interface Chapter {
|
|||
name: string;
|
||||
puzzles: Puzzle[];
|
||||
show?: boolean;
|
||||
start?: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
|
|
8
src/lib/validations/group.ts
Normal file
8
src/lib/validations/group.ts
Normal 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',),
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
{data.daily.puzzle.score ? 'Voir' : 'Jouer'}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<Button href="/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}">
|
||||
{data.daily.puzzle.score ? 'Voir' : 'Jouer'}
|
||||
</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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
{#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>
|
||||
{: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>
|
||||
{#if data.chapter.id === 1}
|
||||
<Button href="/leaderboard/{data.chapter.id}">Voir le classement</Button>
|
||||
{/if}
|
||||
</header>
|
||||
<ul class="flex flex-col gap-4">
|
||||
{#each data.chapter.puzzles as puzzle (puzzle.id)}
|
||||
<Puzzle {puzzle} />
|
||||
{/each}
|
||||
<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>
|
||||
|
|
142
src/routes/(app)/chapters/[chapterId]/groups/+page.server.ts
Normal file
142
src/routes/(app)/chapters/[chapterId]/groups/+page.server.ts
Normal 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"
|
||||
};
|
||||
}
|
||||
};
|
136
src/routes/(app)/chapters/[chapterId]/groups/+page.svelte
Normal file
136
src/routes/(app)/chapters/[chapterId]/groups/+page.svelte
Normal 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>
|
|
@ -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`);
|
||||
}
|
||||
};
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,20 +40,28 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="border-x border-b border-border bg-card align-middle">
|
||||
{#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>
|
||||
<td class="text-lg">
|
||||
{#if group.players?.length}
|
||||
<span>{group.players.map((player) => player?.pseudo).join(', ')} </span>
|
||||
{:else}
|
||||
<span class="text-muted">Aucun joueur</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-right">{group.players.reduce((a, b) => a + b.score, 0)}</td>
|
||||
<td class="text-right">{group.players.reduce((a, b) => a + b.tries, 0)}</td>
|
||||
{#if !data.leaderboard.groups.length}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted-foreground"
|
||||
>Aucun groupe n'a encore de score</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
{: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>
|
||||
<td class="text-lg">
|
||||
{#if group.players?.length}
|
||||
<span>{group.players.map((player) => player?.pseudo).join(', ')} </span>
|
||||
{:else}
|
||||
<span class="text-muted">Aucun joueur</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-right">{group.players.reduce((a, b) => a + b.score, 0)}</td>
|
||||
<td class="text-right">{group.players.reduce((a, b) => a + b.tries, 0)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
25
src/routes/(app)/settings/+layout.svelte
Normal file
25
src/routes/(app)/settings/+layout.svelte
Normal 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>
|
|
@ -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 } }) => {
|
||||
|
|
|
@ -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,93 +27,80 @@
|
|||
}
|
||||
});
|
||||
|
||||
$: optedOut = $plausible;
|
||||
|
||||
// TODO: Handle overflow X on small screens
|
||||
</script>
|
||||
|
||||
TODO
|
||||
<section>
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<div class="flex items-center justify-between">
|
||||
<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}
|
||||
Modifier
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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}>
|
||||
{#if submitting}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
Modifier
|
||||
</Button>
|
||||
</div>
|
||||
<label for="email">Email</label>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="philipzcwbarlow@peerat.dev"
|
||||
value={data.user?.email}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<label for="email">Email</label>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="philipzcwbarlow@peerat.dev"
|
||||
value={data.user?.email}
|
||||
disabled
|
||||
<label for="firstname">Prénom</label>
|
||||
<Input
|
||||
name="firstname"
|
||||
type="text"
|
||||
placeholder="Philip"
|
||||
aria-invalid={$errors.firstname ? 'true' : undefined}
|
||||
bind:value={$form.firstname}
|
||||
/>
|
||||
{#if $errors.firstname}
|
||||
<span class="text-sm text-red-500">{$errors.firstname}</span>
|
||||
{/if}
|
||||
|
||||
<label for="lastname">Nom</label>
|
||||
<Input
|
||||
name="lastname"
|
||||
type="text"
|
||||
placeholder="Barlow"
|
||||
aria-invalid={$errors.lastname ? 'true' : undefined}
|
||||
bind:value={$form.lastname}
|
||||
/>
|
||||
{#if $errors.lastname}
|
||||
<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>
|
||||
|
||||
<label for="firstname">Prénom</label>
|
||||
<Input
|
||||
name="firstname"
|
||||
type="text"
|
||||
placeholder="Philip"
|
||||
aria-invalid={$errors.firstname ? 'true' : undefined}
|
||||
bind:value={$form.firstname}
|
||||
/>
|
||||
{#if $errors.firstname}
|
||||
<span class="text-sm text-red-500">{$errors.firstname}</span>
|
||||
{/if}
|
||||
|
||||
<label for="lastname">Nom</label>
|
||||
<Input
|
||||
name="lastname"
|
||||
type="text"
|
||||
placeholder="Barlow"
|
||||
aria-invalid={$errors.lastname ? 'true' : undefined}
|
||||
bind:value={$form.lastname}
|
||||
/>
|
||||
{#if $errors.lastname}
|
||||
<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={optedOut}
|
||||
on:change={() => plausible.set(!optedOut)}
|
||||
checked={optedOut}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
|
|
7
src/routes/(auth)/+layout.svelte
Normal file
7
src/routes/(auth)/+layout.svelte
Normal file
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
||||
<Toaster />
|
|
@ -2,18 +2,18 @@
|
|||
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';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
|
||||
|
||||
import { loginSchema } from '$lib/validations/auth';
|
||||
|
||||
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">
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
if (res.status === 404) {
|
||||
form.errors.pseudo = [`L'inscription est désactivée.`];
|
||||
|
||||
return fail(400, {
|
||||
form
|
||||
});
|
||||
success: true
|
||||
}
|
||||
}
|
||||
|
||||
if (res.status === 400) {
|
||||
|
@ -112,12 +101,18 @@ export const actions = {
|
|||
}
|
||||
|
||||
if (res.status === 400) {
|
||||
const { email_valid, username_valid } = await res.json();
|
||||
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é'];
|
||||
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 });
|
||||
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.`];
|
||||
|
|
|
@ -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>
|
||||
|
||||
<Button variant="link" formaction="?/register" on:click={() => toast('Code renvoyé')}>
|
||||
Renvoyer le code
|
||||
</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"
|
||||
>
|
||||
Renvoyer le code
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{: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}>
|
||||
{#if $registerDelayed}
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
S'inscrire
|
||||
</Form.Button>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Form.Button type="submit" disabled={$registerDelayed}>
|
||||
{#if $registerDelayed}
|
||||
<Loader className="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
S'inscrire
|
||||
</Form.Button>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="flex justify-between">
|
||||
<li>
|
||||
<a class="text-muted-foreground hover:text-primary" href="/login">Se connecter</a>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
<ul class="flex justify-between">
|
||||
<li>
|
||||
<a class="text-muted-foreground hover:text-primary" href="/login">Se connecter</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -2,11 +2,8 @@
|
|||
import '../app.css';
|
||||
|
||||
import { Metadata } from '$lib/components';
|
||||
import { Toaster } from "$lib/components/ui/sonner";
|
||||
</script>
|
||||
|
||||
<Metadata />
|
||||
|
||||
<slot />
|
||||
|
||||
<Toaster />
|
||||
|
|
Loading…
Add table
Reference in a new issue