Merge pull request 'Refactor core and features' (#26) from dev into main
Reviewed-on: #26
This commit is contained in:
commit
e26876df30
54 changed files with 1012 additions and 588 deletions
690
pnpm-lock.yaml
generated
690
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -25,8 +25,13 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||
return resolve(event);
|
||||
}
|
||||
|
||||
const user: User = await res.json();
|
||||
event.locals.user = user;
|
||||
try {
|
||||
const user: User = await res.json();
|
||||
event.locals.user = user;
|
||||
} catch (error) {
|
||||
event.locals.user = undefined;
|
||||
event.cookies.delete('session', { path: '/' });
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</script>
|
||||
|
||||
<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}
|
||||
on:click={copy}
|
||||
{...$$restProps}
|
||||
|
|
|
@ -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 Metadata } from './metadata.svelte';
|
||||
export { default as Puzzle } from './puzzle.svelte';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import Award from 'lucide-svelte/icons/award';
|
||||
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 LogOut from 'lucide-svelte/icons/log-out';
|
||||
import RectangleEllipsis from 'lucide-svelte/icons/rectangle-ellipsis';
|
||||
|
@ -49,10 +49,6 @@
|
|||
<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>
|
||||
|
@ -61,10 +57,6 @@
|
|||
</DropdownMenu.Sub>
|
||||
{/if}
|
||||
<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">
|
||||
<Settings class="mr-2 h-4 w-4" />
|
||||
<span>Paramètres</span>
|
||||
|
@ -76,8 +68,8 @@
|
|||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item href="/git" target="_blank">
|
||||
<Github class="mr-2 h-4 w-4" />
|
||||
<span>GitHub</span>
|
||||
<GitBranch class="mr-2 h-4 w-4" />
|
||||
<span>Git</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item href="/discord" target="_blank">
|
||||
<LifeBuoy class="mr-2 h-4 w-4" />
|
||||
|
|
28
src/lib/components/ui/textarea/index.ts
Normal file
28
src/lib/components/ui/textarea/index.ts
Normal 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,
|
||||
};
|
31
src/lib/components/ui/textarea/textarea.svelte
Normal file
31
src/lib/components/ui/textarea/textarea.svelte
Normal 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}
|
||||
/>
|
|
@ -1,9 +1,9 @@
|
|||
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 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 LifeBuoy from "lucide-svelte/icons/life-buoy";
|
||||
|
||||
|
@ -44,7 +44,7 @@ export const navigation: NavItemWithChildren[] = [
|
|||
name: "Git",
|
||||
href: "/git",
|
||||
external: true,
|
||||
icon: Github
|
||||
icon: GitBranch
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
export const siteConfig = {
|
||||
name: 'Peer-at Code',
|
||||
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: '',
|
||||
keywords: ['peerat', 'code', 'cybersecurite', 'programmation', "apprendre en s'amusant"],
|
||||
author: 'peerat',
|
||||
links: {
|
||||
github: "https://git.peerat.dev",
|
||||
discord: "https://discord.gg/72vuHcwUkE",
|
||||
},
|
||||
themeColor: '#110F15'
|
||||
};
|
||||
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
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}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connection opened');
|
||||
if (token) {
|
||||
ws.send(JSON.stringify({ token }));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => console.log('WebSocket connection closed');
|
||||
ws.onerror = (event) => console.log('WebSocket error:', event);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log('WebSocket message:', event.data);
|
||||
const data: T = JSON.parse(event.data);
|
||||
store.addRequest(data);
|
||||
};
|
||||
|
|
6
src/params/id.ts
Normal file
6
src/params/id.ts
Normal 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
21
src/params/link.ts
Normal 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)
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -11,18 +11,18 @@ export const load = (async ({ fetch, locals: { user } }) => {
|
|||
|
||||
const res = await fetch(`${API_URL}/chapters`);
|
||||
|
||||
let chapters: Chapter[];
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
daily: null
|
||||
};
|
||||
chapters = [];
|
||||
} else {
|
||||
chapters = await res.json();
|
||||
}
|
||||
|
||||
const chapters: Chapter[] = await res.json();
|
||||
|
||||
const lastChapter = chapters.filter((chapter) => chapter.start && chapter.end).pop();
|
||||
const event = chapters.filter((chapter) => chapter.start && chapter.end).pop();
|
||||
|
||||
return {
|
||||
title: 'Dashboard',
|
||||
event: lastChapter,
|
||||
event,
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: user = data.user;
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col gap-4">
|
||||
|
@ -18,9 +16,9 @@
|
|||
<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"
|
||||
>
|
||||
<Card title="Puzzles résolus" data={user?.completions ?? 0} />
|
||||
<Card title="Badges obtenus" data={user?.badges?.length ?? 'Aucun'} />
|
||||
<Card title="Rang actuel" data={user?.rank ?? 'Non classé'} />
|
||||
<Card title="Puzzles résolus" data={data.user?.completions ?? 0} />
|
||||
<Card title="Badges obtenus" data={data.user?.badges?.length ?? 'Aucun'} />
|
||||
<Card title="Rang actuel" data={data.user?.rank ?? 'Non classé'} />
|
||||
</div>
|
||||
{#if data.event}
|
||||
<header>
|
||||
|
@ -39,27 +37,6 @@
|
|||
<Button href="/chapters/{data.event.id}/groups">Participer</Button>
|
||||
</div>
|
||||
{/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="flex flex-col gap-4">
|
||||
<header>
|
||||
|
@ -72,8 +49,8 @@
|
|||
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}
|
||||
{#each user.completionsList as completion, key}
|
||||
{#if data.user?.completionsList?.length}
|
||||
{#each data.user.completionsList as completion, key}
|
||||
<li class="flex justify-between space-x-2">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
|
|
15
src/routes/(app)/[link=link]/+page.ts
Normal file
15
src/routes/(app)/[link=link]/+page.ts
Normal 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])
|
||||
};
|
|
@ -2,7 +2,6 @@ import { redirect } from "@sveltejs/kit";
|
|||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { user }, cookies }) => {
|
||||
|
||||
if (!user) redirect(302, '/login');
|
||||
|
||||
if (!user.email.endsWith('@peerat.dev')) redirect(302, '/');
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
const stateStore = createStateStore<Log>();
|
||||
|
||||
onMount(() => {
|
||||
connectWebSocket('/admin/logs', data.session, stateStore);
|
||||
connectWebSocket('/admin/logs', stateStore, data.session);
|
||||
return () => stateStore.reset();
|
||||
});
|
||||
|
||||
const logsStore = derived(stateStore, ($stateStore) =>
|
||||
|
@ -97,6 +98,7 @@
|
|||
<span class="font-normal text-foreground">
|
||||
{log.createdAt.toLocaleString()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
20
src/routes/(app)/admin/puzzles/+page.server.ts
Normal file
20
src/routes/(app)/admin/puzzles/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
40
src/routes/(app)/admin/puzzles/+page.svelte
Normal file
40
src/routes/(app)/admin/puzzles/+page.svelte
Normal 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>
|
63
src/routes/(app)/admin/puzzles/[id]/+page.server.ts
Normal file
63
src/routes/(app)/admin/puzzles/[id]/+page.server.ts
Normal 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
|
||||
};
|
||||
}
|
||||
};
|
80
src/routes/(app)/admin/puzzles/[id]/+page.svelte
Normal file
80
src/routes/(app)/admin/puzzles/[id]/+page.svelte
Normal 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>
|
42
src/routes/(app)/admin/puzzles/[id]/schema.ts
Normal file
42
src/routes/(app)/admin/puzzles/[id]/schema.ts
Normal 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 t’accueillons ici pour un entraînement intensif à la programmation.
|
||||
Quand tu seras devenu un véritable Peer-at Codeur, tu pourras m’aider à sauver le monde.
|
||||
|
||||
Je me présente, je m’appelle « Philipz Cypher Wolf Barlow ». Je serai ton capitaine durant tout ton parcours d’apprentissage 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 d’utiliser un éditeur de texte avancé.
|
||||
|
||||
Celui que je te conseille est Open Source est disponible sur tous les types d’ordinateur, il s’appelle Geany (https://www.geany.org/).
|
||||
Commence par l’installer sur ta machine !
|
||||
|
||||
Pour discuter avec d’autres Peerats et obtenir de l’aide des plus expérimentés, rejoins notre serveur Discord (https://discord.gg/eUbSbPceh3).
|
||||
N’hésite pas y demander une petite démo de l’utilisation de Geany :)
|
||||
|
||||
Si tu t’es orienté vers la cybersécurité, tu peux réaliser les challenges en Python.
|
||||
Si tu t’es orienté vers le développement d’applications, tu peux réaliser les challenges en Java.
|
||||
|
||||
Allez moussaillon, « souquez les artimuses » et lancez Geany !
|
||||
Si tu n’as 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: 
|
||||
|
||||
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 d’aide pour cette étape, n’hésite pas à solliciter d’autres Peerats sur Discord ou dans les couloirs de ton campus.
|
||||
`
|
|
@ -2,14 +2,15 @@
|
|||
import { page } from '$app/stores';
|
||||
import Badge from '$lib/components/badge.svelte';
|
||||
|
||||
$: user = $page.data.user;
|
||||
$: badges = user?.badges?.sort((a, b) => a.level - b.level);
|
||||
$: badges = $page.data.user?.badges?.sort((a, b) => a.level - b.level);
|
||||
</script>
|
||||
|
||||
<section class="flex h-full w-full flex-col gap-4">
|
||||
<header class="flex flex-col">
|
||||
<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>
|
||||
<main class="flex flex-col justify-between gap-4">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
|
|
|
@ -6,22 +6,26 @@ import type { PageServerLoad } from './$types';
|
|||
import type { Chapter } from '$lib/types';
|
||||
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));
|
||||
|
||||
let chapters: Chapter[];
|
||||
|
||||
const res = await fetch(`${API_URL}/chapters`);
|
||||
|
||||
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 {
|
||||
title: 'Chapitres',
|
||||
chapters
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
$: chapters = data.chapters;
|
||||
|
||||
const toBeContinued: IChapter = {
|
||||
id: Math.random() * 999,
|
||||
name: 'To be continued ...',
|
||||
|
@ -28,7 +26,7 @@
|
|||
</div>
|
||||
</header>
|
||||
<ul class="flex flex-col gap-2">
|
||||
{#each chapters as chapter (chapter.id)}
|
||||
{#each data.chapters as chapter (chapter.id)}
|
||||
<Chapter {chapter} />
|
||||
{/each}
|
||||
<Chapter chapter={toBeContinued} />
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { API_URL } from '$env/static/private';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
import type { Chapter } from '$lib/types';
|
||||
|
||||
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));
|
||||
|
||||
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,
|
||||
chapter
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
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 Puzzle from '$lib/components/puzzle.svelte';
|
||||
|
@ -24,13 +24,13 @@
|
|||
{/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="/chapters/{data.chapter.id}/groups">
|
||||
<Users class="mr-2 h-4 w-4" />
|
||||
Voir les groupes
|
||||
</Button>
|
||||
<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
|
||||
</Button>
|
||||
{/if}
|
|
@ -1,45 +1,40 @@
|
|||
import { API_URL } from '$env/static/private';
|
||||
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
import type { Chapter, Group } from '$lib/types';
|
||||
|
||||
import { handleRedirect } from '$lib/utils';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
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');
|
||||
}
|
||||
if (!res.ok) redirect(302, `/chapters/${chapterId}`);
|
||||
|
||||
const chapter = (await res.json()) as Chapter;
|
||||
const chapter: Chapter = await res.json()
|
||||
|
||||
if (!chapter || !chapter.show && !(chapter.start && chapter.end)) {
|
||||
redirect(302, '/chapters');
|
||||
}
|
||||
if (!chapter || !chapter.show && !(chapter.start && chapter.end)) redirect(302, `/chapters/${chapterId}`)
|
||||
|
||||
res = await fetch(`${API_URL}/groups/${chapter.id}`);
|
||||
|
||||
if (!res.ok) {
|
||||
redirect(302, `/chapters/${chapterId}`);
|
||||
}
|
||||
if (!res.ok) redirect(302, `/chapters/${chapterId}`);
|
||||
|
||||
const groups = (await res.json()) as Group[];
|
||||
const groups: Group[] = await res.json();
|
||||
|
||||
return {
|
||||
title: `${chapter.name} - Groups`,
|
||||
title: `${chapter.name} - Groupes`,
|
||||
chapter,
|
||||
groups
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
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();
|
||||
|
||||
|
@ -79,7 +74,8 @@ export const actions: Actions = {
|
|||
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();
|
||||
|
|
@ -1,20 +1,18 @@
|
|||
import { API_URL } from "$env/static/private";
|
||||
import { fail, redirect, type Actions } from "@sveltejs/kit";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { setError, superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { handleRedirect } from "$lib/utils";
|
||||
|
||||
import { groupSchema } from "$lib/validations/group";
|
||||
|
||||
export const load: PageServerLoad = async ({ url, params: { chapterId }, locals: { user } }) => {
|
||||
|
||||
if (!user) redirect(302, handleRedirect('login', url));
|
||||
|
||||
if (user.groups.find(g => g.chapter === parseInt(chapterId))) {
|
||||
redirect(302, `/chapters/${chapterId}/groups`);
|
||||
}
|
||||
if (user.groups.find(g => g.chapter === parseInt(chapterId))) redirect(302, `/chapters/${chapterId}/groups`);
|
||||
|
||||
const form = await superValidate(zod(groupSchema));
|
||||
|
||||
|
@ -25,9 +23,10 @@ export const load: PageServerLoad = async ({ url, params: { chapterId }, locals:
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ url, locals: { user }, fetch, request, params: { chapterId } }) => {
|
||||
|
||||
if (!user) redirect(302, handleRedirect('login', url));
|
||||
default: async ({ fetch, request, params: { chapterId }, locals: { user } }) => {
|
||||
if (!user) {
|
||||
return fail(401);
|
||||
}
|
||||
|
||||
if (!chapterId) redirect(302, '/chapters');
|
||||
|
||||
|
@ -46,16 +45,13 @@ export const actions: Actions = {
|
|||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
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) {
|
||||
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 setError(form, "name", "Vous ne pouvez plus créer de groupe, la date limite est passée");
|
||||
}
|
||||
|
||||
return fail(400, { form });
|
||||
return setError(form, "name", "Une erreur est survenue, veuillez réessayer plus tard");
|
||||
}
|
||||
|
||||
redirect(302, `/chapters/${chapterId}/groups`);
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
import Code from 'lucide-svelte/icons/code';
|
||||
|
@ -8,9 +10,25 @@
|
|||
|
||||
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;
|
||||
|
||||
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>
|
||||
|
||||
<section class="flex h-full w-full flex-col gap-4">
|
||||
|
@ -28,7 +46,7 @@
|
|||
</header>
|
||||
<main class="pb-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full min-w-max table">
|
||||
<table class="table min-w-full">
|
||||
<thead
|
||||
class="border-x border-b border-t border-border bg-card/50 text-sm text-muted-foreground"
|
||||
>
|
||||
|
@ -41,14 +59,20 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="border-x border-b border-border bg-card align-middle">
|
||||
{#if !data.leaderboard.groups.length}
|
||||
{#if !$currentLeaderboard?.groups.length}
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{: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])}>
|
||||
<td>{group.rank}</td>
|
||||
<td>{group.name}</td>
|
||||
|
@ -56,7 +80,7 @@
|
|||
{#if group.players?.length}
|
||||
<span>{group.players.map((player) => player?.pseudo).join(', ')} </span>
|
||||
{:else}
|
||||
<span class="text-muted">Aucun joueur</span>
|
||||
<span class="text-muted-foreground">Aucun joueur</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-right">{group.players.reduce((a, b) => a + b.score, 0)}</td>
|
|
@ -4,7 +4,7 @@ import type { PageServerLoad } from './$types';
|
|||
|
||||
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));
|
||||
redirect(302, chapterId ? `/chapters/${chapterId}` : `/chapters`);
|
||||
}) satisfies PageServerLoad;
|
||||
redirect(302, isNaN(parseInt(chapterId)) ? `/chapters/${chapterId}` : `/chapters`);
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
import { API_URL } from '$env/static/private';
|
||||
import { error, redirect, type Actions } from '@sveltejs/kit';
|
||||
import { compile } from 'mdsvex';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
import type Puzzle from '$lib/components/puzzle.svelte';
|
||||
import type { Chapter } from '$lib/types';
|
||||
import { superValidate } from 'sveltekit-superforms';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
|
||||
import { compile } from 'mdsvex';
|
||||
|
||||
import type { Chapter, Puzzle } from '$lib/types';
|
||||
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));
|
||||
|
||||
const session = cookies.get('session');
|
||||
|
@ -22,7 +27,7 @@ export const load = (async ({ url, locals: { user }, fetch, cookies, params: { c
|
|||
redirect(302, `/chapters`);
|
||||
}
|
||||
|
||||
const chapter = (await res.json()) as Chapter;
|
||||
const chapter: Chapter = await res.json();
|
||||
|
||||
if (!chapter || !chapter.show) {
|
||||
redirect(302, `/chapters`);
|
||||
|
@ -55,20 +60,37 @@ export const load = (async ({ url, locals: { user }, fetch, cookies, params: { c
|
|||
|
||||
const content = await compile(puzzle.content);
|
||||
|
||||
puzzle.content = content?.code
|
||||
.replace(/>{@html `<code class="language-/g, '><code class="language-')
|
||||
.replace(/<\/code>`}<\/pre>/g, '</code></pre>');
|
||||
if (content)
|
||||
puzzle.content = content?.code
|
||||
.replace(/>{@html `<code class="language-/g, '><code class="language-')
|
||||
.replace(/<\/code>`}<\/pre>/g, '</code></pre>');
|
||||
|
||||
return {
|
||||
title: `${chapter.name} - ${puzzle.name}`,
|
||||
puzzle: puzzle as Puzzle,
|
||||
puzzle,
|
||||
url: `${API_URL}/puzzleResponse/${puzzleId}`,
|
||||
session
|
||||
session,
|
||||
form: await superValidate(zod(formSchema))
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
default: async ({ params }) => {
|
||||
redirect(302, `/chapters/${params.chapterId}/puzzle/${params.puzzleId}`);
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, fetch, params: { chapterId, 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;
|
||||
}
|
|
@ -1,18 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import './prism-night-owl.css';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
|
||||
import { enhance } from '$app/forms';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import Loader from 'lucide-svelte/icons/loader-circle';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import CopyCodeInjector from '$lib/components/copy-code-injector.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: puzzle = data.puzzle;
|
||||
|
@ -27,13 +29,48 @@
|
|||
<span class="text-lg text-muted-foreground">({puzzle.scoreMax} points)</span>
|
||||
</h2>
|
||||
<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>
|
||||
{@html puzzle.content}
|
||||
</CopyCodeInjector>
|
||||
</article>
|
||||
{#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
|
||||
class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"
|
||||
method="POST"
|
||||
|
@ -97,11 +134,6 @@
|
|||
<div class="flex flex-col gap-y-2">
|
||||
<label for="answer">Réponse</label>
|
||||
<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 class="flex flex-col gap-y-2">
|
||||
<label for="code_file">Fichier</label>
|
|
@ -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;
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -4,10 +4,10 @@ import { redirect } from '@sveltejs/kit';
|
|||
import type { PageServerLoad } from './$types';
|
||||
|
||||
import type { Leaderboard } from '$lib/types';
|
||||
|
||||
import { handleRedirect } from '$lib/utils';
|
||||
|
||||
export const load = (async ({ url, locals: { user }, fetch }) => {
|
||||
|
||||
if (!user) redirect(302, handleRedirect('login', url));
|
||||
|
||||
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 {
|
||||
title: 'Classement',
|
||||
|
|
|
@ -30,15 +30,21 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="border-x border-b border-border bg-card align-middle">
|
||||
{#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>
|
||||
{#if data.leaderboard.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Aucun joueur n'a encore joué</td>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { API_URL } from '$env/static/private';
|
||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
|
||||
import { handleRedirect } from '$lib/utils';
|
||||
|
||||
import { settingSchema } from '$lib/validations/auth';
|
||||
|
||||
export const load: PageServerLoad = async ({ url, locals: { user } }) => {
|
||||
|
@ -21,7 +22,6 @@ export const load: PageServerLoad = async ({ url, locals: { user } }) => {
|
|||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, fetch, locals: { user } }) => {
|
||||
|
||||
if (!user) return fail(401);
|
||||
|
||||
const form = await superValidate(request, zod(settingSchema));
|
||||
|
@ -41,6 +41,7 @@ export const actions: Actions = {
|
|||
if (res.status === 400) {
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
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 />
|
|
@ -1,7 +1,7 @@
|
|||
import { dev } from '$app/environment';
|
||||
import { API_URL } from '$env/static/private';
|
||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
|
@ -9,7 +9,6 @@ import { setError, superValidate } from 'sveltekit-superforms/server';
|
|||
import { loginSchema } from '$lib/validations/auth';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { user } }) => {
|
||||
|
||||
if (user) redirect(302, '/');
|
||||
|
||||
const form = await superValidate(zod(loginSchema));
|
||||
|
@ -54,8 +53,7 @@ export const actions: Actions = {
|
|||
|
||||
const redirectTo = searchParams.get('redirectTo');
|
||||
|
||||
if (redirectTo)
|
||||
redirect(302, `/${redirectTo.slice(1)}`);
|
||||
if (redirectTo) redirect(302, `/${redirectTo.slice(1)}`);
|
||||
|
||||
redirect(302, '/');
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@ export const GET: RequestHandler = async ({ locals: { user }, cookies }) => {
|
|||
|
||||
if (session) cookies.delete("session", { path: "/" });
|
||||
|
||||
return redirect(302, "/login");
|
||||
redirect(302, "/login");
|
||||
};
|
||||
|
|
|
@ -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 (!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");
|
||||
}
|
||||
|
||||
|
@ -95,8 +96,7 @@ export const actions: Actions = {
|
|||
|
||||
const redirectTo = searchParams.get('redirectTo');
|
||||
|
||||
if (redirectTo)
|
||||
redirect(302, `/${redirectTo.slice(1)}`);
|
||||
if (redirectTo) redirect(302, `/${redirectTo.slice(1)}`);
|
||||
|
||||
redirect(302, '/');
|
||||
}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import { dev } from '$app/environment';
|
||||
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 { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
|
||||
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 resetPasswordForm = await superValidate(zod(resetPasswordSchema));
|
||||
|
||||
|
@ -32,7 +34,7 @@ export const actions: Actions = {
|
|||
const res = await fetch(`${API_URL}/user/fpw`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...form.data
|
||||
email: form.data.email
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -60,31 +62,30 @@ export const actions: Actions = {
|
|||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const token = res.headers.get('Authorization')?.split('Bearer ').pop();
|
||||
|
||||
if (!token) {
|
||||
return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
|
||||
if (!res.ok) {
|
||||
if (res.status === 400) {
|
||||
return setError(form, 'code', "Le code de confirmation est incorrect");
|
||||
}
|
||||
|
||||
cookies.set('session', token, {
|
||||
path: '/',
|
||||
secure: !dev,
|
||||
sameSite: 'strict'
|
||||
});
|
||||
|
||||
const redirectTo = searchParams.get('redirectTo');
|
||||
|
||||
if (redirectTo)
|
||||
redirect(302, `/${redirectTo.slice(1)}`);
|
||||
|
||||
redirect(302, '/');
|
||||
return setError(form, 'code', "Une erreur est survenue, veuillez réessayer plus tard");
|
||||
}
|
||||
|
||||
if (res.status === 400) {
|
||||
return setError(form, 'code', "Le code de confirmation est incorrect");
|
||||
const token = res.headers.get('Authorization')?.split('Bearer ').pop();
|
||||
|
||||
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, '/');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import * as Form from '$lib/components/ui/form';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
|
||||
import { requestPasswordResetSchema, resetPasswordSchema } from '$lib/validations/auth';
|
||||
|
||||
export let data: PageData;
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -9,6 +9,7 @@
|
|||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
import '../app.css';
|
||||
|
||||
import { Metadata } from '$lib/components';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
|
||||
</script>
|
||||
|
||||
<Metadata />
|
||||
|
||||
<slot />
|
||||
|
||||
<Toaster />
|
||||
|
|
|
@ -59,9 +59,11 @@ const config = {
|
|||
fira: ['Fira Code', ...fontFamily.sans]
|
||||
}
|
||||
},
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true
|
||||
}
|
||||
},
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true,
|
||||
removeDeprecatedGapUtilities: true,
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
|
|
Loading…
Add table
Reference in a new issue