feat: puzzle done
This commit is contained in:
parent
e658974929
commit
b7202656d1
3 changed files with 117 additions and 80 deletions
|
@ -1,49 +1,40 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
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>
|
||||
|
||||
<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 items-center gap-x-2">
|
||||
<h1 class="text-xl font-semibold">{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 class="flex items-center gap-2">
|
||||
<h1 class="text-xl font-semibold">{data.chapter.name}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="mt-4 flex flex-col space-y-4">
|
||||
{#each chapter.puzzles as puzzle}
|
||||
<ul class="flex flex-col gap-4">
|
||||
{#each data.chapter.puzzles as puzzle}
|
||||
<Puzzle {puzzle} />
|
||||
{/each}
|
||||
{#if data.chapter.puzzles.length > 1}
|
||||
<Puzzle puzzle={toBeContinued} />
|
||||
{/if}
|
||||
</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>
|
||||
</section>
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
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 Puzzle from '$lib/components/Puzzle.svelte';
|
||||
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 } }) => {
|
||||
await parent();
|
||||
|
||||
|
@ -51,42 +67,14 @@ export const load = (async ({ parent, fetch, cookies, params: { chapterId, puzzl
|
|||
}
|
||||
|
||||
return {
|
||||
puzzle: puzzle as Puzzle
|
||||
puzzle: puzzle as Puzzle,
|
||||
url: `${API_URL}/puzzleResponse/${puzzleId}`,
|
||||
session
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { id } = event.params;
|
||||
|
||||
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');
|
||||
default: async ({ params, request, cookies }) => {
|
||||
throw redirect(303, `/dashboard/chapters/${params.chapterId}/puzzle/${params.puzzleId}`);
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
|
|
@ -1,16 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
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 { cn } from '$lib';
|
||||
import { addToast } from '$lib/components/Toaster.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
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;
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
@ -24,7 +41,7 @@
|
|||
};
|
||||
|
||||
renderer.br = () => {
|
||||
return '<br /><br />';
|
||||
return '<br />';
|
||||
};
|
||||
|
||||
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">
|
||||
{@html marked(puzzle.content, options)}
|
||||
</div>
|
||||
{#if !puzzle.score}
|
||||
{#if !puzzle.score && !completed}
|
||||
<form
|
||||
class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"
|
||||
method="POST"
|
||||
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 flex-col gap-y-2">
|
||||
<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 class="flex flex-col gap-y-2">
|
||||
<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>
|
||||
<Button class="w-full sm:w-44" variant="brand">Valider</Button>
|
||||
</form>
|
||||
{: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">
|
||||
<p>
|
||||
Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '}
|
||||
|
@ -76,10 +135,9 @@
|
|||
Score : <span class="text-brand-accent">{puzzle.score}</span>
|
||||
</p>
|
||||
</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
|
||||
</Button> -->
|
||||
<Button href="/dashboard/chapters/{chapterId}" class="w-full sm:w-44" variant="brand">Retour aux puzzles</Button>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue