feat: puzzle done

This commit is contained in:
Théo 2023-09-17 23:04:28 +02:00
parent e658974929
commit b7202656d1
3 changed files with 117 additions and 80 deletions

View file

@ -1,49 +1,40 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import Puzzle from '$lib/components/Puzzle.svelte'; import Puzzle from '$lib/components/Puzzle.svelte';
export let data; import type { Puzzle as IPuzzle } from '$lib/types';
$: chapter = data.chapter; export let data: PageData;
data.chapter.puzzles = data.chapter.puzzles.sort((a, b) => a.scoreMax - b.scoreMax);
const toBeContinued: IPuzzle = {
id: 0,
name: 'To be continued...',
content: 'Maybe you will find the One Piece one day...',
scoreMax: 999,
show: false,
tags: [],
tries: 0,
score: 0
};
</script> </script>
<section class="flex w-full flex-col space-y-6"> <section class="flex w-full flex-col space-y-6">
<div class="flex flex-col"> <div class="flex flex-col gap-4">
<div class="flex flex-col justify-between md:flex-row md:items-center"> <div class="flex flex-col justify-between md:flex-row md:items-center">
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-2">
<h1 class="text-xl font-semibold">{chapter.name}</h1> <h1 class="text-xl font-semibold">{data.chapter.name}</h1>
<!-- {!isInEventGroup(chapter) && isBeforeStart(chapter) && (
<Dialog
key={chapter.id}
title={chapter.name}
open={isOpen[chapter.id]}
onOpenChange={() => handleClick(chapter.id)}
trigger={
<button class="flex items-center gap-x-2 text-sm font-semibold text-muted hover:text-brand">
{/* <Icon name="group-line" /> */}
Rejoindre un groupe
</button>
}
class="right-96 p-4"
>
<GroupForm chapter={chapter} token={token} />
</Dialog>
)} -->
</div> </div>
</div> </div>
<ul class="mt-4 flex flex-col space-y-4"> <ul class="flex flex-col gap-4">
{#each chapter.puzzles as puzzle} {#each data.chapter.puzzles as puzzle}
<Puzzle {puzzle} /> <Puzzle {puzzle} />
{/each} {/each}
{#if data.chapter.puzzles.length > 1}
<Puzzle puzzle={toBeContinued} />
{/if}
</ul> </ul>
<!-- {isInEventGroup(chapter) && (
<ul class="mt-4 flex flex-col space-y-4">
{filteredData &&
filteredData
.sort((a, b) => a.scoreMax - b.scoreMax)
.map((puzzle) => (
<PuzzleProp key={puzzle.id} puzzle={puzzle} chapter={chapter} />
))}
</ul>
)} -->
</div> </div>
</section> </section>

View file

@ -1,10 +1,26 @@
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, type Actions, fail } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import type Puzzle from '$lib/components/Puzzle.svelte'; import type Puzzle from '$lib/components/Puzzle.svelte';
import type { Chapter } from '$lib/types'; import type { Chapter } from '$lib/types';
import { superValidate } from 'sveltekit-superforms/server';
import { z } from 'zod';
const puzzleSchema = z.object({
// answer: z.string().trim(),
// answer need to be filled
answer: z
.string({
required_error: 'Réponse manquante'
})
.refine((val) => val.trim() !== '', {
message: 'Réponse manquante'
}),
file: z.any().optional()
});
export const load = (async ({ parent, fetch, cookies, params: { chapterId, puzzleId } }) => { export const load = (async ({ parent, fetch, cookies, params: { chapterId, puzzleId } }) => {
await parent(); await parent();
@ -51,42 +67,14 @@ export const load = (async ({ parent, fetch, cookies, params: { chapterId, puzzl
} }
return { return {
puzzle: puzzle as Puzzle puzzle: puzzle as Puzzle,
url: `${API_URL}/puzzleResponse/${puzzleId}`,
session
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
export const actions = { export const actions = {
default: async (event) => { default: async ({ params, request, cookies }) => {
const { id } = event.params; throw redirect(303, `/dashboard/chapters/${params.chapterId}/puzzle/${params.puzzleId}`);
const data = await event.request.formData();
const res = await fetch(`${API_URL}/puzzleResponse/${id}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${event.cookies.get('session')}`
},
body: data
});
return {
success: res.ok
};
// throw redirect(303, `/dashboard/puzzles/${id}`);
// if (res.ok) {
// const token = res.headers.get('Authorization')?.split(' ')[1];
// if (!token) throw new Error('No token found');
// event.cookies.set('session', token, {
// path: '/'
// });
// throw redirect(303, '/dashboard');
// }
// throw redirect(303, '/sign-in');
} }
} satisfies Actions; } satisfies Actions;

View file

@ -1,16 +1,33 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { marked, type MarkedOptions } from 'marked'; import { marked, type MarkedOptions } from 'marked';
import { cn } from '$lib';
import { addToast } from '$lib/components/Toaster.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
import Input from '$lib/components/ui/Input.svelte'; import Input from '$lib/components/ui/Input.svelte';
import { page } from '$app/stores';
export let data: PageData; export let data: PageData;
$: puzzle = data.puzzle; const { puzzle } = data;
let completed = false;
function toast(title: string, description: string) {
addToast({
data: {
title,
description
},
closeDelay: 2500
});
}
$: chapterId = $page.params.chapterId; $: chapterId = $page.params.chapterId;
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@ -24,7 +41,7 @@
}; };
renderer.br = () => { renderer.br = () => {
return '<br /><br />'; return '<br />';
}; };
renderer.codespan = (code) => { renderer.codespan = (code) => {
@ -46,27 +63,69 @@
<div class="h-screen w-full overflow-y-auto break-all font-fira text-xs sm:text-base"> <div class="h-screen w-full overflow-y-auto break-all font-fira text-xs sm:text-base">
{@html marked(puzzle.content, options)} {@html marked(puzzle.content, options)}
</div> </div>
{#if !puzzle.score} {#if !puzzle.score && !completed}
<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"
enctype="multipart/form-data" enctype="multipart/form-data"
use:enhance use:enhance={async ({ formElement, formData, action, cancel, submitter }) => {
if (formData.get('answer') === '') {
toast('Erreur', 'Vous devez entrer une réponse !');
return cancel();
}
const res = await fetch(data.url, {
method: 'POST',
body: formData,
headers: {
Authorization: `Bearer ${data.session}`
}
});
if (res.ok || res.status === 403 || res.status === 406 || res.status === 423) {
const data = res.ok || res.status === 406 ? await res.json() : null;
if (data && data.score) {
completed = true;
toast('Bonne réponse', 'Vous avez trouvé la bonne réponse!');
} else if (data && data.tries)
toast('Mauvaise réponse', `Vous avez effectué ${data.tries} tentative(s)`);
else if (res.ok && data?.success)
toast('Bonne réponse', 'Vous avez trouvé la bonne réponse!');
else if (res.status === 423)
toast('Mauvaise réponse', "Ce n'est pas la bonne réponse, réessayez!");
}
return async ({ result }) => {
if (result.type === 'redirect') {
goto(result.location, {
invalidateAll: true,
replaceState: true
});
}
};
}}
> >
<div class="flex w-full flex-col gap-2 sm:flex-row sm:gap-4"> <div class="flex w-full flex-col gap-2 sm:flex-row sm:gap-4">
<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" placeholder="CAPTAIN, LOOK !" /> <textarea
class={cn(
'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 focus-visible:ring-2 focus-visible:ring-brand 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>
<Input name="code_file" type="file" /> <Input name="code_file" type="file" accept=".py,.js,.ts,.java,.rs,.c" />
</div> </div>
</div> </div>
<Button class="w-full sm:w-44" variant="brand">Valider</Button> <Button class="w-full sm:w-44" variant="brand">Valider</Button>
</form> </form>
{:else} {:else}
<div class="flex items-center justify-between flex-col sm:flex-row gap-2"> <div class="flex flex-col items-center justify-between gap-2 sm:flex-row">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p> <p>
Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '} Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '}
@ -76,10 +135,9 @@
Score : <span class="text-brand-accent">{puzzle.score}</span> Score : <span class="text-brand-accent">{puzzle.score}</span>
</p> </p>
</div> </div>
<!-- <Button type="button" onClick={() => router.push(getURL(`/dashboard/puzzles`))}> <Button href="/dashboard/chapters/{chapterId}" class="w-full sm:w-44" variant="brand">
Retour aux puzzles Retour aux puzzles
</Button> --> </Button>
<Button href="/dashboard/chapters/{chapterId}" class="w-full sm:w-44" variant="brand">Retour aux puzzles</Button>
</div> </div>
{/if} {/if}
</div> </div>