186 lines
5.8 KiB
TypeScript
186 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import cookies from 'js-cookie';
|
|
import { notFound, useRouter } from 'next/navigation';
|
|
import { useContext, useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { useSWRConfig } from 'swr';
|
|
import * as z from 'zod';
|
|
|
|
import { usePuzzle } from '@/lib/hooks/use-puzzles';
|
|
import { getURL } from '@/lib/utils';
|
|
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/Form';
|
|
import { Input } from '@/components/ui/Input';
|
|
import ToHTML from '@/components/ui/ToHTML';
|
|
|
|
import { UserContext } from '@/context/user';
|
|
import { type Puzzle } from '@/lib/puzzles';
|
|
import { Loader2 } from 'lucide-react';
|
|
import { Separator } from '@/components/ui/Separator';
|
|
|
|
type Granted = {
|
|
tries: number | null;
|
|
score?: number | null;
|
|
message?: string | null;
|
|
success?: boolean | null;
|
|
};
|
|
|
|
export default function Puzzle({ token, id }: { token: string; id: number }) {
|
|
const { data: me } = useContext(UserContext);
|
|
const { data: puzzle, isLoading } = usePuzzle({ token, id });
|
|
const router = useRouter();
|
|
|
|
if (!puzzle && isLoading) {
|
|
return <></>;
|
|
}
|
|
|
|
if (!puzzle) {
|
|
notFound();
|
|
}
|
|
|
|
// TODO : add a check to see if the user is in the group of the puzzle
|
|
if (me?.groups.length === 0) {
|
|
router.push('/dashboard/puzzles');
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full w-full flex-col justify-between space-y-4">
|
|
<h1 className="text-2xl font-bold sm:text-3xl md:text-4xl">
|
|
{puzzle.name}{' '}
|
|
<span className="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
|
</h1>
|
|
<Separator />
|
|
<div className="flex h-screen w-full overflow-y-auto">
|
|
<ToHTML className="font-code text-xs sm:text-base" data={puzzle.content} />
|
|
</div>
|
|
{!puzzle.score ? (
|
|
<InputForm puzzle={puzzle} />
|
|
) : (
|
|
<div className="flex items-center justify-between">
|
|
<div className="items-center gap-x-2">
|
|
<p>
|
|
Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '}
|
|
<span className="text-brand-accent">{puzzle.tries}</span>
|
|
</p>
|
|
<p>
|
|
Score : <span className="text-brand-accent">{puzzle.score}</span>
|
|
</p>
|
|
</div>
|
|
<Button type="button" onClick={() => router.push(getURL(`/dashboard/puzzles`))}>
|
|
Retour aux puzzles
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const InputFormSchema = z.object({
|
|
answer: z.string().nonempty().trim(),
|
|
code_file: z.any().optional()
|
|
});
|
|
|
|
function InputForm({ puzzle }: { puzzle?: Puzzle }) {
|
|
const form = useForm<z.infer<typeof InputFormSchema>>({
|
|
resolver: zodResolver(InputFormSchema),
|
|
defaultValues: {
|
|
answer: '',
|
|
code_file: undefined
|
|
}
|
|
});
|
|
|
|
const { mutate } = useSWRConfig();
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [granted, setGranted] = useState<Granted | null>(null);
|
|
|
|
async function onSubmit(data: z.infer<typeof InputFormSchema>) {
|
|
setIsLoading(true);
|
|
|
|
const formData = new FormData();
|
|
|
|
if (data.code_file) {
|
|
formData.append('code_file', data.code_file[0]);
|
|
}
|
|
|
|
formData.append('answer', data.answer);
|
|
|
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzleResponse/${puzzle!.id}`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
Authorization: `Bearer ${cookies.get('token')}}`
|
|
}
|
|
});
|
|
|
|
if (res.ok || res.status === 403 || res.status === 406 || res.status === 423) {
|
|
const data = res.ok || res.status === 406 ? ((await res.json()) as Granted) : null;
|
|
if (data && data.score) {
|
|
mutate(`puzzles/${puzzle?.id}`);
|
|
} else if (data && data.tries) setGranted(data);
|
|
else if (res.ok && data?.success)
|
|
setGranted({ tries: null, score: null, message: 'Réponse correcte' });
|
|
else if (res.status === 423)
|
|
setGranted({ tries: null, score: null, message: 'Réponse incorrecte' });
|
|
else if (res.status === 403) mutate(`puzzles/${puzzle?.id}`);
|
|
}
|
|
|
|
setIsLoading(false);
|
|
|
|
form.reset();
|
|
}
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="flex w-full flex-col items-end justify-between gap-4 sm:flex-row"
|
|
encType="multipart/form-data"
|
|
>
|
|
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="answer"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel htmlFor="answer">Réponse</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="CAPTAIN, LOOK !" autoComplete="off" {...field} />
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="code_file"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel htmlFor="code_file">Fichier</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} type="file" />
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
{granted && (
|
|
<div className="flex flex-col items-center justify-center gap-2">
|
|
<p className="text-brand-accent">{granted.message}</p>
|
|
{granted.tries && (
|
|
<p className="text-brand-accent">
|
|
Il vous reste {granted.tries} tentative{granted.tries > 1 ? 's' : ''}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
<Button disabled={isLoading} className="w-full sm:w-44" variant="brand">
|
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
Envoyer
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
);
|
|
}
|