refactor: core features and code

This commit is contained in:
glazk0 2024-04-16 00:43:58 +02:00
parent f20407504c
commit 9aabbea973
No known key found for this signature in database
GPG key ID: E45BF177782B9FEB
54 changed files with 1012 additions and 588 deletions

690
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -25,8 +25,13 @@ export const handle: Handle = async ({ event, resolve }) => {
return resolve(event); return resolve(event);
} }
const user: User = await res.json(); try {
event.locals.user = user; const user: User = await res.json();
event.locals.user = user;
} catch (error) {
event.locals.user = undefined;
event.cookies.delete('session', { path: '/' });
}
return resolve(event); return resolve(event);
}; };

View file

@ -19,7 +19,7 @@
</script> </script>
<button <button
class="absolute right-2 top-2 rounded-md p-2 text-white" class="absolute right-2 top-2 rounded-md p-2"
bind:this={element} bind:this={element}
on:click={copy} on:click={copy}
{...$$restProps} {...$$restProps}

View file

@ -1,4 +1,6 @@
// TODO: Add more components here export { default as Badge } from './badge.svelte';
export { default as Breadcrumb } from './breadcrumb.svelte';
export { default as Chapter } from './chapter.svelte';
export { default as CopyCodeInjector } from './copy-code-injector.svelte'; export { default as CopyCodeInjector } from './copy-code-injector.svelte';
export { default as Metadata } from './metadata.svelte'; export { default as Metadata } from './metadata.svelte';
export { default as Puzzle } from './puzzle.svelte';

View file

@ -5,7 +5,7 @@
import Award from 'lucide-svelte/icons/award'; import Award from 'lucide-svelte/icons/award';
import Code from 'lucide-svelte/icons/code'; import Code from 'lucide-svelte/icons/code';
import Github from 'lucide-svelte/icons/github'; import GitBranch from 'lucide-svelte/icons/git-branch';
import LifeBuoy from 'lucide-svelte/icons/life-buoy'; import LifeBuoy from 'lucide-svelte/icons/life-buoy';
import LogOut from 'lucide-svelte/icons/log-out'; import LogOut from 'lucide-svelte/icons/log-out';
import RectangleEllipsis from 'lucide-svelte/icons/rectangle-ellipsis'; import RectangleEllipsis from 'lucide-svelte/icons/rectangle-ellipsis';
@ -49,10 +49,6 @@
<ScrollText class="mr-2 h-4 w-4" /> <ScrollText class="mr-2 h-4 w-4" />
<span>Logs</span> <span>Logs</span>
</DropdownMenu.Item> </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"> <DropdownMenu.Item href="/admin/puzzles">
<Code class="mr-2 h-4 w-4" /> <Code class="mr-2 h-4 w-4" />
<span>Puzzles</span> <span>Puzzles</span>
@ -61,10 +57,6 @@
</DropdownMenu.Sub> </DropdownMenu.Sub>
{/if} {/if}
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item href="/reset-password">
<RectangleEllipsis class="mr-2 h-4 w-4" />
<span>Réinitialiser le mot de passe</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/settings"> <DropdownMenu.Item href="/settings">
<Settings class="mr-2 h-4 w-4" /> <Settings class="mr-2 h-4 w-4" />
<span>Paramètres</span> <span>Paramètres</span>
@ -76,8 +68,8 @@
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item href="/git" target="_blank"> <DropdownMenu.Item href="/git" target="_blank">
<Github class="mr-2 h-4 w-4" /> <GitBranch class="mr-2 h-4 w-4" />
<span>GitHub</span> <span>Git</span>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item href="/discord" target="_blank"> <DropdownMenu.Item href="/discord" target="_blank">
<LifeBuoy class="mr-2 h-4 w-4" /> <LifeBuoy class="mr-2 h-4 w-4" />

View file

@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View file

@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTextareaAttributes;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
</script>
<textarea
class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
/>

View file

@ -1,9 +1,9 @@
import type { ComponentType } from "svelte"; import type { ComponentType } from "svelte";
import type { Icon } from "lucide-svelte"; import { type Icon } from "lucide-svelte";
import BarChart2 from "lucide-svelte/icons/bar-chart-2"; import BarChart2 from "lucide-svelte/icons/bar-chart-2";
import Code from "lucide-svelte/icons/code"; import Code from "lucide-svelte/icons/code";
import Github from "lucide-svelte/icons/github"; import GitBranch from "lucide-svelte/icons/git-branch";
import LayoutDashboard from "lucide-svelte/icons/layout-dashboard"; import LayoutDashboard from "lucide-svelte/icons/layout-dashboard";
import LifeBuoy from "lucide-svelte/icons/life-buoy"; import LifeBuoy from "lucide-svelte/icons/life-buoy";
@ -44,7 +44,7 @@ export const navigation: NavItemWithChildren[] = [
name: "Git", name: "Git",
href: "/git", href: "/git",
external: true, external: true,
icon: Github icon: GitBranch
}, },
{ {
name: "Discord", name: "Discord",

View file

@ -1,14 +1,10 @@
export const siteConfig = { export const siteConfig = {
name: 'Peer-at Code', name: 'Peer-at Code',
url: 'https://app.peerat.dev', url: 'https://app.peerat.dev',
description: 'Apprendre la programmation et la cybersécurité en s\'amusant.', description: "Apprendre la programmation et la cybersécurité en s'amusant.",
imageUrl: '', imageUrl: '',
keywords: ['peerat', 'code', 'cybersecurite', 'programmation', "apprendre en s'amusant"], keywords: ['peerat', 'code', 'cybersecurite', 'programmation', "apprendre en s'amusant"],
author: 'peerat', author: 'peerat',
links: {
github: "https://git.peerat.dev",
discord: "https://discord.gg/72vuHcwUkE",
},
themeColor: '#110F15' themeColor: '#110F15'
}; };

View file

@ -1,20 +1,15 @@
import type { StateStore } from "./state"; import type { StateStore } from "./state";
export const connectWebSocket = <T>(path: string, token: string | undefined, store: StateStore<T>) => { export const connectWebSocket = <T>(path: string, store: StateStore<T>, token: string | undefined = undefined) => {
const ws = new WebSocket(`wss://api.peerat.dev${path}`); const ws = new WebSocket(`wss://api.peerat.dev${path}`);
ws.onopen = () => { ws.onopen = () => {
console.log('WebSocket connection opened');
if (token) { if (token) {
ws.send(JSON.stringify({ token })); ws.send(JSON.stringify({ token }));
} }
}; };
ws.onclose = () => console.log('WebSocket connection closed');
ws.onerror = (event) => console.log('WebSocket error:', event);
ws.onmessage = (event) => { ws.onmessage = (event) => {
console.log('WebSocket message:', event.data);
const data: T = JSON.parse(event.data); const data: T = JSON.parse(event.data);
store.addRequest(data); store.addRequest(data);
}; };

6
src/params/id.ts Normal file
View file

@ -0,0 +1,6 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
const regex = /\d+/
return regex.test(param);
};

21
src/params/link.ts Normal file
View file

@ -0,0 +1,21 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const links = [
'git',
'discord'
];
type Link = (typeof links)[number];
export const linkMap: Record<Link, string> = {
git: "https://git.peerat.dev",
discord: "https://discord.gg/72vuHcwUkE",
}
export const isLink = (param: string): param is Link => {
return links.includes(param as Link);
}
export const match: ParamMatcher = (param: string) => {
return links.includes(param.toLowerCase() as Link)
};

View file

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

View file

@ -11,18 +11,18 @@ export const load = (async ({ fetch, locals: { user } }) => {
const res = await fetch(`${API_URL}/chapters`); const res = await fetch(`${API_URL}/chapters`);
let chapters: Chapter[];
if (!res.ok) { if (!res.ok) {
return { chapters = [];
daily: null } else {
}; chapters = await res.json();
} }
const chapters: Chapter[] = await res.json(); const event = chapters.filter((chapter) => chapter.start && chapter.end).pop();
const lastChapter = chapters.filter((chapter) => chapter.start && chapter.end).pop();
return { return {
title: 'Dashboard', title: 'Dashboard',
event: lastChapter, event,
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View file

@ -5,8 +5,6 @@
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData; export let data: PageData;
$: user = data.user;
</script> </script>
<section class="flex w-full flex-col gap-4"> <section class="flex w-full flex-col gap-4">
@ -18,9 +16,9 @@
<div <div
class="flex w-full flex-col justify-between gap-4 space-x-0 md:flex md:flex-row md:space-x-6 md:space-y-0" class="flex w-full flex-col justify-between gap-4 space-x-0 md:flex md:flex-row md:space-x-6 md:space-y-0"
> >
<Card title="Puzzles résolus" data={user?.completions ?? 0} /> <Card title="Puzzles résolus" data={data.user?.completions ?? 0} />
<Card title="Badges obtenus" data={user?.badges?.length ?? 'Aucun'} /> <Card title="Badges obtenus" data={data.user?.badges?.length ?? 'Aucun'} />
<Card title="Rang actuel" data={user?.rank ?? 'Non classé'} /> <Card title="Rang actuel" data={data.user?.rank ?? 'Non classé'} />
</div> </div>
{#if data.event} {#if data.event}
<header> <header>
@ -39,27 +37,6 @@
<Button href="/chapters/{data.event.id}/groups">Participer</Button> <Button href="/chapters/{data.event.id}/groups">Participer</Button>
</div> </div>
{/if} {/if}
<!-- {#if data.daily && data.daily.puzzle}
<header>
<h1 class="text-lg font-semibold">Puzzle du jour</h1>
<p class="text-muted-foreground">
Essayer de résoudre le puzzle du jour pour gagner des points supplémentaires
</p>
</header>
<div
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>
<span class="text-muted-foreground">
{data.daily.puzzle.name} ({data.daily.puzzle.score ?? '?'}/{data.daily.puzzle.scoreMax})
</span>
</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"> <div class="grid grid-cols-1 gap-4">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<header> <header>
@ -72,8 +49,8 @@
class="h-full max-h-96 overflow-y-scroll rounded border border-border bg-card 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"> <ul class="flex flex-col space-y-2">
{#if user?.completionsList?.length} {#if data.user?.completionsList?.length}
{#each user.completionsList as completion, key} {#each data.user.completionsList as completion, key}
<li class="flex justify-between space-x-2"> <li class="flex justify-between space-x-2">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">

View file

@ -0,0 +1,15 @@
import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
import { isLink, linkMap } from "../../../params/link";
export const load: PageLoad = async ({ params: { link } }) => {
link = link.toLowerCase();
if (!isLink(link))
redirect(302, '/')
redirect(302, linkMap[link])
};

View file

@ -2,7 +2,6 @@ import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals: { user }, cookies }) => { export const load: PageServerLoad = async ({ locals: { user }, cookies }) => {
if (!user) redirect(302, '/login'); if (!user) redirect(302, '/login');
if (!user.email.endsWith('@peerat.dev')) redirect(302, '/'); if (!user.email.endsWith('@peerat.dev')) redirect(302, '/');

View file

@ -24,7 +24,8 @@
const stateStore = createStateStore<Log>(); const stateStore = createStateStore<Log>();
onMount(() => { onMount(() => {
connectWebSocket('/admin/logs', data.session, stateStore); connectWebSocket('/admin/logs', stateStore, data.session);
return () => stateStore.reset();
}); });
const logsStore = derived(stateStore, ($stateStore) => const logsStore = derived(stateStore, ($stateStore) =>
@ -97,6 +98,7 @@
<span class="font-normal text-foreground"> <span class="font-normal text-foreground">
{log.createdAt.toLocaleString()} {log.createdAt.toLocaleString()}
</span> </span>
</p>
</div> </div>
</div> </div>
{/each} {/each}

View file

@ -0,0 +1,20 @@
import { API_URL } from "$env/static/private";
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import type { Puzzle } from "$lib/types";
export const load: PageServerLoad = async ({ locals: { user }, fetch }) => {
if (!user) redirect(302, '/login');
if (!user.email.endsWith('@peerat.dev')) redirect(302, '/');
const res = await fetch(`${API_URL}/admin/puzzles/`);
if (!res.ok) redirect(302, '/');
const puzzles: Puzzle[] = await res.json();
return {
puzzles
};
};

View file

@ -0,0 +1,40 @@
<script lang="ts">
import type { PageData } from './$types';
import { Input } from '$lib/components/ui/input';
export let data: PageData;
let name: string;
$: filteredPuzzles = data.puzzles.filter((puzzle) => {
const regex = new RegExp(name, 'i');
return regex.test(puzzle.name);
});
</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">Liste de puzzle</h2>
</div>
</header>
<div class="flex">
<Input bind:value={name} placeholder="Goerfra the puzzle lord" />
</div>
<ul class="flex flex-col gap-2">
{#if filteredPuzzles?.length}
{#each filteredPuzzles as puzzle (puzzle.id)}
<a href="/admin/puzzles/{puzzle.id}">
{puzzle.name}
</a>
{: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,63 @@
import { API_URL } from "$env/static/private";
import { redirect, type Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { fail, superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import type { Puzzle } from "$lib/types";
import { formSchema } from "./schema";
export const load: PageServerLoad = async ({ locals: { user }, fetch, params: { id } }) => {
if (!user) redirect(302, '/login');
if (!user.email.endsWith('@peerat.dev')) redirect(302, '/');
const res = await fetch(`${API_URL}/admin/puzzle/${id}`);
if (!res.ok) redirect(302, '/admin/puzzles');
const puzzle: Puzzle = await res.json()
const form = await superValidate(puzzle, zod(formSchema));
return {
puzzle,
form,
};
};
export const actions: Actions = {
default: async ({ request, locals: { user }, fetch, params: { id } }) => {
if (!user) return fail(401);
if (!user.email.endsWith('@peerat.dev')) return fail(401);
const form = await superValidate(request, zod(formSchema));
if (!form.valid) {
return fail(400, { form });
}
const res = await fetch(`${API_URL}/admin/puzzle/${id}`, {
method: "PUT",
body: JSON.stringify({
name: form.data.name,
content: form.data.content,
soluce: form.data.soluce,
scoreMax: parseInt(form.data.score_max),
chapter: parseInt(form.data.chapter)
})
});
if (!res.ok) {
return fail(500);
}
return {
form
};
}
};

View file

@ -0,0 +1,80 @@
<script lang="ts">
import type { PageData } from './$types';
import Loader from 'lucide-svelte/icons/loader-circle';
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 Textarea from '$lib/components/ui/textarea/textarea.svelte';
import { formSchema } from './schema';
import Button from '$lib/components/ui/button/button.svelte';
export let data: PageData;
const form = superForm(data.form, {
validators: zodClient(formSchema),
delayMs: 500,
multipleSubmits: 'prevent'
});
const { form: formData, enhance, delayed } = form;
</script>
<section>
<form class="flex flex-col gap-2" method="POST" use:enhance>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold">Modifier le puzzle</h2>
<div class="flex gap-2">
{#if $formData.chapter}
<Button href="/chapters/{$formData.chapter}/puzzle/{data.puzzle.id}">
Voir le puzzle
</Button>
{/if}
<Form.Button disabled={$delayed}>
{#if $delayed}
<Loader class="mr-2 h-4 w-4 animate-spin" />
{/if}
Modifier
</Form.Button>
</div>
</div>
<Form.Field {form} name="name">
<Form.Control let:attrs>
<Form.Label>Nom</Form.Label>
<Input {...attrs} bind:value={$formData.name} placeholder="Philip" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="content">
<Form.Control let:attrs>
<Form.Label>Contenu</Form.Label>
<Textarea {...attrs} bind:value={$formData.content} placeholder="Barlow" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="soluce">
<Form.Control let:attrs>
<Form.Label>Solution</Form.Label>
<Input {...attrs} bind:value={$formData.soluce} placeholder="Cypherwolf" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="score_max">
<Form.Control let:attrs>
<Form.Label>Score maximum</Form.Label>
<Input type="number" {...attrs} bind:value={$formData.score_max} placeholder="Cypherwolf" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="chapter">
<Form.Control let:attrs>
<Form.Label>Chapitre</Form.Label>
<Input type="number" {...attrs} bind:value={$formData.chapter} placeholder="Cypherwolf" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</form>
</section>

View file

@ -0,0 +1,42 @@
import { z } from "zod";
export const formSchema = z.object({
name: z.string().min(1, { message: "Le nom du puzzle est requis" }),
content: z.string().min(1, { message: "Le contenu du puzzle est requis" }),
soluce: z.string().min(1, { message: "La solution du puzzle est requise" }),
score_max: z.string().regex(/^\d+$/, { message: "Le score maximum doit être un nombre" }),
chapter: z.string().regex(/^\d+$/, { message: "Le chapitre doit être un nombre" }),
});
export type FormSchema = typeof formSchema;
const text = `
Bienvenue dans notre vaisseau de cyber-formation le « Rasper-ship », moussaillon.
Nous taccueillons ici pour un entraînement intensif à la programmation.
Quand tu seras devenu un véritable Peer-at Codeur, tu pourras maider à sauver le monde.
Je me présente, je mappelle « Philipz Cypher Wolf Barlow ». Je serai ton capitaine durant tout ton parcours dapprentissage du code.
Premièrement, nous allons nous occuper de ton équipement.
Pour développer, ton arme principale sera un IDE (Integrated Development Editor) mais dans un premier temps je te propose dutiliser un éditeur de texte avancé.
Celui que je te conseille est Open Source est disponible sur tous les types dordinateur, il sappelle Geany (https://www.geany.org/).
Commence par linstaller sur ta machine !
Pour discuter avec dautres Peerats et obtenir de laide des plus expérimentés, rejoins notre serveur Discord (https://discord.gg/eUbSbPceh3).
Nhésite pas y demander une petite démo de lutilisation de Geany :)
Si tu tes orienté vers la cybersécurité, tu peux réaliser les challenges en Python.
Si tu tes orienté vers le développement dapplications, tu peux réaliser les challenges en Java.
Allez moussaillon, « souquez les artimuses » et lancez Geany !
Si tu nas jamais codé, tu peux trouver, dans la cale, des explications sur son fonctionnement.
Commence par créer un fichier Abordage.java ou Abordage.py en fonction du langage que tu veux utiliser.
Voici ce que tu peux écrire dans ce fichier: ![À l'abordage !](https://cdn.peerat.dev/w1-0.png)
Félicitation, tu viens décrire le code source de ton premier programme :)
(Compile) et Exécute ton programme. Copie ci-dessous le résultat affiché dans la console.
Si tu as besoin daide pour cette étape, nhésite pas à solliciter dautres Peerats sur Discord ou dans les couloirs de ton campus.
`

View file

@ -2,14 +2,15 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Badge from '$lib/components/badge.svelte'; import Badge from '$lib/components/badge.svelte';
$: user = $page.data.user; $: badges = $page.data.user?.badges?.sort((a, b) => a.level - b.level);
$: badges = user?.badges?.sort((a, b) => a.level - b.level);
</script> </script>
<section class="flex h-full w-full flex-col gap-4"> <section class="flex h-full w-full flex-col gap-4">
<header class="flex flex-col"> <header class="flex flex-col">
<h1 class="text-xl font-semibold">Mes badges</h1> <h1 class="text-xl font-semibold">Mes badges</h1>
<p class="text-muted-foreground">Vos badges sont affichés ici, vous pouvez les partager avec vos amis</p> <p class="text-muted-foreground">
Vos badges sont affichés ici, vous pouvez les partager avec vos amis
</p>
</header> </header>
<main class="flex flex-col justify-between gap-4"> <main class="flex flex-col justify-between gap-4">
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">

View file

@ -6,22 +6,26 @@ import type { PageServerLoad } from './$types';
import type { Chapter } from '$lib/types'; import type { Chapter } from '$lib/types';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
export const load = (async ({ url, locals: { user }, fetch }) => { export const load: PageServerLoad = async ({ url, locals: { user }, fetch }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
let chapters: Chapter[];
const res = await fetch(`${API_URL}/chapters`); const res = await fetch(`${API_URL}/chapters`);
if (!res.ok) { if (!res.ok) {
return { chapters = [];
chapters: [] } else {
}; chapters = await res.json();
} }
const chapters = (await res.json()) as Chapter[]; if (chapters.length) chapters
.sort((a) => {
return (a.start && a.end) ? -1 : 1;
})
return { return {
title: 'Chapitres', title: 'Chapitres',
chapters chapters
}; };
}) satisfies PageServerLoad; }

View file

@ -7,8 +7,6 @@
export let data: PageData; export let data: PageData;
$: chapters = data.chapters;
const toBeContinued: IChapter = { const toBeContinued: IChapter = {
id: Math.random() * 999, id: Math.random() * 999,
name: 'To be continued ...', name: 'To be continued ...',
@ -28,7 +26,7 @@
</div> </div>
</header> </header>
<ul class="flex flex-col gap-2"> <ul class="flex flex-col gap-2">
{#each chapters as chapter (chapter.id)} {#each data.chapters as chapter (chapter.id)}
<Chapter {chapter} /> <Chapter {chapter} />
{/each} {/each}
<Chapter chapter={toBeContinued} /> <Chapter chapter={toBeContinued} />

View file

@ -1,13 +1,13 @@
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import type { Chapter } from '$lib/types'; import type { Chapter } from '$lib/types';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
import { redirect } from '@sveltejs/kit';
export const load = (async ({ url, locals: { user }, fetch, params: { chapterId } }) => {
export const load: PageServerLoad = async ({ url, locals: { user }, fetch, params: { chapterId } }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
const res = await fetch(`${API_URL}/chapter/${chapterId}`); const res = await fetch(`${API_URL}/chapter/${chapterId}`);
@ -28,4 +28,4 @@ export const load = (async ({ url, locals: { user }, fetch, params: { chapterId
title: chapter.name, title: chapter.name,
chapter chapter
}; };
}) satisfies PageServerLoad; }

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import BarChart2 from 'lucide-svelte/icons/bar-chart-2'; import BarChart from 'lucide-svelte/icons/bar-chart-2';
import Users from 'lucide-svelte/icons/users'; import Users from 'lucide-svelte/icons/users';
import Puzzle from '$lib/components/puzzle.svelte'; import Puzzle from '$lib/components/puzzle.svelte';
@ -24,13 +24,13 @@
{/if} {/if}
</div> </div>
<div class="flex gap-2"> <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} {#if data.chapter.start && data.chapter.end}
<Button href="/chapters/{data.chapter.id}/groups">
<Users class="mr-2 h-4 w-4" />
Voir les groupes
</Button>
<Button href="/chapters/{data.chapter.id}/leaderboard"> <Button href="/chapters/{data.chapter.id}/leaderboard">
<BarChart2 class="mr-2 h-4 w-4" /> <BarChart class="mr-2 h-4 w-4" />
Voir le classement Voir le classement
</Button> </Button>
{/if} {/if}

View file

@ -1,45 +1,40 @@
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import type { Chapter, Group } from '$lib/types'; import type { Chapter, Group } from '$lib/types';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ url, locals: { user }, fetch, params: { chapterId } }) => { export const load: PageServerLoad = async ({ url, locals: { user }, fetch, params: { chapterId } }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
let res = await fetch(`${API_URL}/chapter/${chapterId}`); let res = await fetch(`${API_URL}/chapter/${chapterId}`);
if (!res.ok) { if (!res.ok) redirect(302, `/chapters/${chapterId}`);
redirect(302, '/chapters');
}
const chapter = (await res.json()) as Chapter; const chapter: Chapter = await res.json()
if (!chapter || !chapter.show && !(chapter.start && chapter.end)) { if (!chapter || !chapter.show && !(chapter.start && chapter.end)) redirect(302, `/chapters/${chapterId}`)
redirect(302, '/chapters');
}
res = await fetch(`${API_URL}/groups/${chapter.id}`); res = await fetch(`${API_URL}/groups/${chapter.id}`);
if (!res.ok) { if (!res.ok) redirect(302, `/chapters/${chapterId}`);
redirect(302, `/chapters/${chapterId}`);
}
const groups = (await res.json()) as Group[]; const groups: Group[] = await res.json();
return { return {
title: `${chapter.name} - Groups`, title: `${chapter.name} - Groupes`,
chapter, chapter,
groups groups
}; };
}; };
export const actions: Actions = { export const actions: Actions = {
join: async ({ fetch, params: { chapterId }, request }) => { join: async ({ fetch, params: { chapterId }, request, locals: { user } }) => {
if (!user) {
return fail(401);
}
const data = await request.formData(); const data = await request.formData();
@ -79,7 +74,8 @@ export const actions: Actions = {
message: "Une erreur s'est produite" message: "Une erreur s'est produite"
}; };
}, },
leave: async ({ fetch, params: { chapterId }, request }) => { leave: async ({ fetch, params: { chapterId }, request, locals: { user } }) => {
if (!user) fail(401);
const data = await request.formData(); const data = await request.formData();

View file

@ -1,20 +1,18 @@
import { API_URL } from "$env/static/private"; import { API_URL } from "$env/static/private";
import { fail, redirect, type Actions } from "@sveltejs/kit"; import { fail, redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms"; import { setError, superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters"; import { zod } from "sveltekit-superforms/adapters";
import { handleRedirect } from "$lib/utils"; import { handleRedirect } from "$lib/utils";
import { groupSchema } from "$lib/validations/group"; import { groupSchema } from "$lib/validations/group";
export const load: PageServerLoad = async ({ url, params: { chapterId }, locals: { user } }) => { export const load: PageServerLoad = async ({ url, params: { chapterId }, locals: { user } }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
if (user.groups.find(g => g.chapter === parseInt(chapterId))) { if (user.groups.find(g => g.chapter === parseInt(chapterId))) redirect(302, `/chapters/${chapterId}/groups`);
redirect(302, `/chapters/${chapterId}/groups`);
}
const form = await superValidate(zod(groupSchema)); const form = await superValidate(zod(groupSchema));
@ -25,9 +23,10 @@ export const load: PageServerLoad = async ({ url, params: { chapterId }, locals:
}; };
export const actions: Actions = { export const actions: Actions = {
default: async ({ url, locals: { user }, fetch, request, params: { chapterId } }) => { default: async ({ fetch, request, params: { chapterId }, locals: { user } }) => {
if (!user) {
if (!user) redirect(302, handleRedirect('login', url)); return fail(401);
}
if (!chapterId) redirect(302, '/chapters'); if (!chapterId) redirect(302, '/chapters');
@ -46,16 +45,13 @@ export const actions: Actions = {
}); });
if (!res.ok) { if (!res.ok) {
if (res.status === 403) { if (res.status === 403) {
form.errors.name = ["Vous êtes déjà dans un groupe"]; return setError(form, "name", "Vous êtes déjà dans un groupe");
} else if (res.status === 423) { } else if (res.status === 423) {
form.errors.name = ["Vous ne pouvez plus créer de groupe"]; return setError(form, "name", "Vous ne pouvez plus créer de groupe, la date limite est passée");
} else {
form.errors.name = ["Une erreur est survenue, veuillez réessayer plus tard"];
} }
return fail(400, { form }); return setError(form, "name", "Une erreur est survenue, veuillez réessayer plus tard");
} }
redirect(302, `/chapters/${chapterId}/groups`); redirect(302, `/chapters/${chapterId}/groups`);

View file

@ -0,0 +1,32 @@
import { API_URL } from '$env/static/private';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import type { Chapter, LeaderboardEvent } from '$lib/types';
import { handleRedirect } from '$lib/utils';
export const load: PageServerLoad = async ({ url, locals: { user }, fetch, params: { chapterId } }) => {
if (!user) redirect(302, handleRedirect('login', url));
let res = await fetch(`${API_URL}/chapter/${chapterId}`)
if (!res.ok) redirect(302, `/chapters/${chapterId}`);
const chapter: Chapter = await res.json();
if (!(chapter.start && chapter.end)) redirect(302, `/chapters/${chapter.id}`);
res = await fetch(`${API_URL}/leaderboard/${chapter.id}`);
let leaderboard: LeaderboardEvent | undefined = undefined;
if (!res.ok) leaderboard = undefined;
leaderboard = await res.json();
return {
title: `${chapter.name} - Classement`,
leaderboard
};
};

View file

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte';
import { derived } from 'svelte/store';
import type { PageData } from './$types'; import type { PageData } from './$types';
import Code from 'lucide-svelte/icons/code'; import Code from 'lucide-svelte/icons/code';
@ -8,9 +10,25 @@
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
import type { LeaderboardEvent } from '$lib/types';
import { createStateStore } from '$lib/stores/state';
import { connectWebSocket } from '$lib/stores/websocket';
export let data: PageData; export let data: PageData;
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400']; const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
const stateStore = createStateStore<LeaderboardEvent>();
if (data.leaderboard) stateStore.addRequest(data.leaderboard);
onMount(() => {
connectWebSocket(`/rleaderboard/${$page.params.chapterId}`, stateStore);
return () => stateStore.reset();
});
$: currentLeaderboard = derived(stateStore, ($stateStore) => $stateStore.requests.pop());
</script> </script>
<section class="flex h-full w-full flex-col gap-4"> <section class="flex h-full w-full flex-col gap-4">
@ -28,7 +46,7 @@
</header> </header>
<main class="pb-4"> <main class="pb-4">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full min-w-max table"> <table class="table min-w-full">
<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"
> >
@ -41,14 +59,20 @@
</tr> </tr>
</thead> </thead>
<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 !$currentLeaderboard?.groups.length}
<tr> <tr>
<td colspan="5" class="text-center text-muted-foreground"> <td colspan="5" class="text-center text-muted-foreground">
Aucun groupe n'a encore de score Aucun groupe n'a encore été créé
</td>
</tr>
{:else if !$currentLeaderboard.groups.filter( (g) => g.players.reduce((a, b) => a + b.score, 0) ).length}
<tr>
<td colspan="5" class="text-center text-muted-foreground">
Aucune équipe 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 $currentLeaderboard.groups as group (group.name)}
<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>{group.name}</td>
@ -56,7 +80,7 @@
{#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>
{:else} {:else}
<span class="text-muted">Aucun joueur</span> <span class="text-muted-foreground">Aucun joueur</span>
{/if} {/if}
</td> </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.score, 0)}</td>

View file

@ -4,7 +4,7 @@ import type { PageServerLoad } from './$types';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
export const load = (async ({ url, locals: { user }, params: { chapterId } }) => { export const load: PageServerLoad = async ({ url, locals: { user }, params: { chapterId } }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
redirect(302, chapterId ? `/chapters/${chapterId}` : `/chapters`); redirect(302, isNaN(parseInt(chapterId)) ? `/chapters/${chapterId}` : `/chapters`);
}) satisfies PageServerLoad; }

View file

@ -1,13 +1,18 @@
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import { error, redirect, type Actions } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { compile } from 'mdsvex'; import type { Actions, PageServerLoad } from './$types';
import type { PageServerLoad } from './$types';
import type Puzzle from '$lib/components/puzzle.svelte'; import { superValidate } from 'sveltekit-superforms';
import type { Chapter } from '$lib/types'; import { zod } from 'sveltekit-superforms/adapters';
import { compile } from 'mdsvex';
import type { Chapter, Puzzle } from '$lib/types';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
export const load = (async ({ url, locals: { user }, fetch, cookies, params: { chapterId, puzzleId } }) => { import { formSchema } from './schema';
export const load: PageServerLoad = async ({ url, locals: { user }, fetch, cookies, params: { chapterId, puzzleId } }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
const session = cookies.get('session'); const session = cookies.get('session');
@ -22,7 +27,7 @@ export const load = (async ({ url, locals: { user }, fetch, cookies, params: { c
redirect(302, `/chapters`); redirect(302, `/chapters`);
} }
const chapter = (await res.json()) as Chapter; const chapter: Chapter = await res.json();
if (!chapter || !chapter.show) { if (!chapter || !chapter.show) {
redirect(302, `/chapters`); redirect(302, `/chapters`);
@ -55,20 +60,37 @@ export const load = (async ({ url, locals: { user }, fetch, cookies, params: { c
const content = await compile(puzzle.content); const content = await compile(puzzle.content);
puzzle.content = content?.code if (content)
.replace(/>{@html `<code class="language-/g, '><code class="language-') puzzle.content = content?.code
.replace(/<\/code>`}<\/pre>/g, '</code></pre>'); .replace(/>{@html `<code class="language-/g, '><code class="language-')
.replace(/<\/code>`}<\/pre>/g, '</code></pre>');
return { return {
title: `${chapter.name} - ${puzzle.name}`, title: `${chapter.name} - ${puzzle.name}`,
puzzle: puzzle as Puzzle, puzzle,
url: `${API_URL}/puzzleResponse/${puzzleId}`, url: `${API_URL}/puzzleResponse/${puzzleId}`,
session session,
form: await superValidate(zod(formSchema))
}; };
}) satisfies PageServerLoad; }
export const actions = { export const actions: Actions = {
default: async ({ params }) => { default: async ({ request, fetch, params: { chapterId, puzzleId } }) => {
redirect(302, `/chapters/${params.chapterId}/puzzle/${params.puzzleId}`);
const formData = await request.formData();
console.log(formData)
const res = await fetch(`${API_URL}/puzzleResponse/${puzzleId}`, {
method: 'POST',
body: formData
});
if (!res.ok) {
console.log(res)
return;
}
redirect(302, `/chapters/${chapterId}/puzzle/${puzzleId}`);
} }
} satisfies Actions; }

View file

@ -1,18 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import './prism-night-owl.css'; import './prism-night-owl.css';
import type { PageData } from './$types';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Loader from 'lucide-svelte/icons/loader-circle'; import Loader from 'lucide-svelte/icons/loader-circle';
import { toast } from 'svelte-sonner';
import CopyCodeInjector from '$lib/components/copy-code-injector.svelte'; import CopyCodeInjector from '$lib/components/copy-code-injector.svelte';
import Button from '$lib/components/ui/button/button.svelte'; 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 { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
export let data: PageData; export let data: PageData;
$: puzzle = data.puzzle; $: puzzle = data.puzzle;
@ -27,13 +29,48 @@
<span class="text-lg text-muted-foreground">({puzzle.scoreMax} points)</span> <span class="text-lg text-muted-foreground">({puzzle.scoreMax} points)</span>
</h2> </h2>
<article <article
class="prose-invert prose-a:text-primary prose-pre:rounded h-screen max-w-none overflow-y-auto break-normal font-fira" class="prose-invert h-screen max-w-none overflow-y-auto break-normal font-fira prose-a:text-primary prose-pre:rounded"
> >
<CopyCodeInjector> <CopyCodeInjector>
{@html puzzle.content} {@html puzzle.content}
</CopyCodeInjector> </CopyCodeInjector>
</article> </article>
{#if !puzzle.score} {#if !puzzle.score}
<!-- <form
class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"
method="POST"
enctype="multipart/form-data"
use:enhance
>
<div class="flex w-full flex-col gap-2 sm:flex-row sm:gap-4">
<Form.Field {form} name="answer">
<Form.Control let:attrs>
<Form.Label>Réponse</Form.Label>
<Input {...attrs} bind:value={$formData.answer} placeholder="CAPTAIN, LOOK!" />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="code_file">
<Form.Control let:attrs>
<Form.Label>Code source</Form.Label>
<Input
{...attrs}
bind:value={$formData.code_file}
type="file"
placeholder=""
disabled
/>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</div>
<Button type="submit" class="w-full sm:w-44" disabled={$delayed}>
{#if $delayed}
<Loader class="mr-2 h-4 w-4 animate-spin" />
{/if}
Valider
</Button>
</form> -->
<form <form
class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row" class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"
method="POST" method="POST"
@ -97,11 +134,6 @@
<div class="flex flex-col gap-y-2"> <div class="flex flex-col gap-y-2">
<label for="answer">Réponse</label> <label for="answer">Réponse</label>
<Input name="answer" type="text" placeholder="CAPTAIN, LOOK !" disabled={submitting} /> <Input name="answer" type="text" placeholder="CAPTAIN, LOOK !" disabled={submitting} />
<!-- <textarea
class="flex h-10 w-full rounded-md border border-primary-600 bg-highlight-primary px-3 py-2 text-sm ring-offset-highlight-primary file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted focus:bg-primary-800 focus-visible:outline-none text-primary:ring-2 focus-visible:text-primaryg8brand focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
name="answer"
placeholder="CAPTAIN, LOOK !"
/> -->
</div> </div>
<div class="flex flex-col gap-y-2"> <div class="flex flex-col gap-y-2">
<label for="code_file">Fichier</label> <label for="code_file">Fichier</label>

View file

@ -0,0 +1,10 @@
import { z } from "zod";
export const formSchema = z.object({
answer: z.string().min(1, { message: "Veuillez entrer une réponse." }),
code_file: z.instanceof(File, { message: "Veuillez envoyer votre code source." })
.refine((file) => file.size < 100_000, 'Votre fichier doit faire maximum 100 kB.')
.array()
});
export type FormSchema = typeof formSchema;

View file

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

View file

@ -4,10 +4,10 @@ import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import type { Leaderboard } from '$lib/types'; import type { Leaderboard } from '$lib/types';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
export const load = (async ({ url, locals: { user }, fetch }) => { export const load = (async ({ url, locals: { user }, fetch }) => {
if (!user) redirect(302, handleRedirect('login', url)); if (!user) redirect(302, handleRedirect('login', url));
const res = await fetch(`${API_URL}/leaderboard`); const res = await fetch(`${API_URL}/leaderboard`);
@ -18,7 +18,7 @@ export const load = (async ({ url, locals: { user }, fetch }) => {
}; };
} }
const leaderboard = (await res.json()) as Leaderboard[]; const leaderboard: Leaderboard[] = await res.json();
return { return {
title: 'Classement', title: 'Classement',

View file

@ -30,15 +30,21 @@
</tr> </tr>
</thead> </thead>
<tbody class="border-x border-b border-border bg-card align-middle"> <tbody class="border-x border-b border-border bg-card align-middle">
{#each data.leaderboard as player (player)} {#if data.leaderboard.length === 0}
<tr class={cn([SCORE_COLORS[player.rank - 1]])}> <tr>
<td>{player.rank}</td> <td colspan="5" class="text-center">Aucun joueur n'a encore joué</td>
<td class="text-lg">{player.pseudo}</td>
<td class="text-right">{player.score}</td>
<td class="text-right">{player.completions}</td>
<td class="text-right">{player.tries}</td>
</tr> </tr>
{/each} {:else}
{#each data.leaderboard as player (player)}
<tr class={cn([SCORE_COLORS[player.rank - 1]])}>
<td>{player.rank}</td>
<td class="text-lg">{player.pseudo}</td>
<td class="text-right">{player.score}</td>
<td class="text-right">{player.completions}</td>
<td class="text-right">{player.tries}</td>
</tr>
{/each}
{/if}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -1,11 +1,12 @@
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import { fail, redirect, type Actions } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { handleRedirect } from '$lib/utils'; import { handleRedirect } from '$lib/utils';
import { settingSchema } from '$lib/validations/auth'; import { settingSchema } from '$lib/validations/auth';
export const load: PageServerLoad = async ({ url, locals: { user } }) => { export const load: PageServerLoad = async ({ url, locals: { user } }) => {
@ -21,7 +22,6 @@ export const load: PageServerLoad = async ({ url, locals: { user } }) => {
export const actions: Actions = { export const actions: Actions = {
default: async ({ request, fetch, locals: { user } }) => { default: async ({ request, fetch, locals: { user } }) => {
if (!user) return fail(401); if (!user) return fail(401);
const form = await superValidate(request, zod(settingSchema)); const form = await superValidate(request, zod(settingSchema));
@ -41,6 +41,7 @@ export const actions: Actions = {
if (res.status === 400) { if (res.status === 400) {
return setError(form, "pseudo", "Ce pseudo est déjà utilisé"); return setError(form, "pseudo", "Ce pseudo est déjà utilisé");
} }
return setError(form, "pseudo", "Une erreur est survenue lors de la sauvegarde des paramètres"); return setError(form, "pseudo", "Une erreur est survenue lors de la sauvegarde des paramètres");
} }

View file

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

View file

@ -1,7 +1,7 @@
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import { fail, redirect, type Actions } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
@ -9,7 +9,6 @@ import { setError, superValidate } from 'sveltekit-superforms/server';
import { loginSchema } from '$lib/validations/auth'; import { loginSchema } from '$lib/validations/auth';
export const load: PageServerLoad = async ({ locals: { user } }) => { export const load: PageServerLoad = async ({ locals: { user } }) => {
if (user) redirect(302, '/'); if (user) redirect(302, '/');
const form = await superValidate(zod(loginSchema)); const form = await superValidate(zod(loginSchema));
@ -54,8 +53,7 @@ export const actions: Actions = {
const redirectTo = searchParams.get('redirectTo'); const redirectTo = searchParams.get('redirectTo');
if (redirectTo) if (redirectTo) redirect(302, `/${redirectTo.slice(1)}`);
redirect(302, `/${redirectTo.slice(1)}`);
redirect(302, '/'); redirect(302, '/');
} }

View file

@ -8,5 +8,5 @@ export const GET: RequestHandler = async ({ locals: { user }, cookies }) => {
if (session) cookies.delete("session", { path: "/" }); if (session) cookies.delete("session", { path: "/" });
return redirect(302, "/login"); redirect(302, "/login");
}; };

View file

@ -78,6 +78,7 @@ export const actions: Actions = {
if (!email_valid) return setError(form, 'email', 'Un compte avec cette adresse email existe déjà'); if (!email_valid) return setError(form, 'email', 'Un compte avec cette adresse email existe déjà');
if (!username_valid) return setError(form, 'pseudo', "Ce nom d'utilisateur est déjà utilisé"); if (!username_valid) return setError(form, 'pseudo', "Ce nom d'utilisateur est déjà utilisé");
} }
return setError(form, 'code', "Une erreur est survenue lors de la confirmation"); return setError(form, 'code', "Une erreur est survenue lors de la confirmation");
} }
@ -95,8 +96,7 @@ export const actions: Actions = {
const redirectTo = searchParams.get('redirectTo'); const redirectTo = searchParams.get('redirectTo');
if (redirectTo) if (redirectTo) redirect(302, `/${redirectTo.slice(1)}`);
redirect(302, `/${redirectTo.slice(1)}`);
redirect(302, '/'); redirect(302, '/');
} }

View file

@ -1,16 +1,18 @@
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { API_URL } from '$env/static/private'; import { API_URL } from '$env/static/private';
import { fail, redirect, type Actions } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { requestPasswordResetSchema, resetPasswordSchema } from '$lib/validations/auth'; import { requestPasswordResetSchema, resetPasswordSchema } from '$lib/validations/auth';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async ({ locals: { user } }) => {
if (user) redirect(302, '/');
const requestPasswordResetForm = await superValidate(zod(requestPasswordResetSchema)); const requestPasswordResetForm = await superValidate(zod(requestPasswordResetSchema));
const resetPasswordForm = await superValidate(zod(resetPasswordSchema)); const resetPasswordForm = await superValidate(zod(resetPasswordSchema));
@ -32,7 +34,7 @@ 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 email: form.data.email
}) })
}); });
@ -60,31 +62,30 @@ export const actions: Actions = {
}) })
}); });
if (res.ok) { if (!res.ok) {
const token = res.headers.get('Authorization')?.split('Bearer ').pop(); if (res.status === 400) {
return setError(form, 'code', "Le code de confirmation est incorrect");
if (!token) {
return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
} }
cookies.set('session', token, { return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
path: '/',
secure: !dev,
sameSite: 'strict'
});
const redirectTo = searchParams.get('redirectTo');
if (redirectTo)
redirect(302, `/${redirectTo.slice(1)}`);
redirect(302, '/');
} }
if (res.status === 400) { const token = res.headers.get('Authorization')?.split('Bearer ').pop();
return setError(form, 'code', "Le code de confirmation est incorrect");
if (!token) {
return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
} }
return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard"); cookies.set('session', token, {
path: '/',
secure: !dev,
sameSite: 'strict'
});
const redirectTo = searchParams.get('redirectTo');
if (redirectTo) redirect(302, `/${redirectTo.slice(1)}`);
redirect(302, '/');
} }
} }

View file

@ -9,6 +9,7 @@
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, resetPasswordSchema } from '$lib/validations/auth';
export let data: PageData; export let data: PageData;

View file

@ -1,8 +0,0 @@
import { redirect } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { siteConfig } from "$lib/config";
export const GET: RequestHandler = async () => {
redirect(303, siteConfig.links.discord);
};

View file

@ -1,8 +0,0 @@
import { redirect } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { siteConfig } from "$lib/config";
export const GET: RequestHandler = async () => {
redirect(303, siteConfig.links.github);
};

View file

@ -9,6 +9,7 @@
<h1 class="text-6xl font-bold text-red-500">Oops!</h1> <h1 class="text-6xl font-bold text-red-500">Oops!</h1>
<p class="mt-4 text-xl">Apparement tu as navigué en eau trouble...</p> <p class="mt-4 text-xl">Apparement tu as navigué en eau trouble...</p>
<Button class="mt-4" href="/">Retour au port</Button> <Button class="mt-4" href="/">Retour au port</Button>
<p class="mt-4 text-xs text-muted-foreground">{$page.error?.message}</p>
<p class="mt-4 text-xs text-muted-foreground">{$page.error?.errorId}</p> <p class="mt-4 text-xs text-muted-foreground">{$page.error?.errorId}</p>
</div> </div>
</div> </div>

View file

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

View file

@ -59,9 +59,11 @@ const config = {
fira: ['Fira Code', ...fontFamily.sans] fira: ['Fira Code', ...fontFamily.sans]
} }
}, },
future: { },
hoverOnlyWhenSupported: true future: {
} hoverOnlyWhenSupported: true,
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
}, },
plugins: [ plugins: [
require('@tailwindcss/typography'), require('@tailwindcss/typography'),