peer-at-code-web/ui/Puzzles.tsx
2023-04-18 13:29:03 +02:00

345 lines
12 KiB
TypeScript

'use client';
import { UserContext } from '@/context/user';
import { useGroups } from '@/lib/hooks/use-groups';
import { usePuzzles } from '@/lib/hooks/use-puzzles';
import type { Chapter, Puzzle } from '@/lib/puzzles';
import { cn } from '@/lib/utils';
import { useContext, useState } from 'react';
import { useForm } from 'react-hook-form';
import AppLink from './AppLink';
import Button from './Button';
import Dialog from './Dialog';
import Icon from './Icon';
import Input from './Input';
import Select from './Select';
import { useRouter } from 'next/navigation';
import { useSWRConfig } from 'swr';
export default function Puzzles({ token }: { token: string }) {
const { data: me } = useContext(UserContext);
const { data, isLoading } = usePuzzles({ token });
const [isOpen, setIsOpen] = useState<boolean[]>([]);
function handleClick(index: number) {
setIsOpen((prevState) => {
const newState = [...prevState];
newState[index] = !newState[index];
return newState;
});
}
function isInEventGroup(chapter: Chapter) {
return (
chapter.startDate &&
chapter.endDate &&
me?.groups?.some((group) => group.chapter && group.chapter === chapter.id)
);
}
return (
<>
{(!isLoading &&
data?.map((chapter) => (
<div key={chapter.id} className="flex flex-col space-y-4">
<div className="flex flex-col justify-between lg:flex-row lg:items-center">
<div className="flex items-center gap-x-2">
<h1 className="text-xl font-semibold">{chapter.name}</h1>
{!isInEventGroup(chapter) && (
<Dialog
key={chapter.id}
title={chapter.name}
open={isOpen[chapter.id]}
onOpenChange={() => handleClick(chapter.id)}
trigger={
<button className="flex items-center gap-x-2 text-sm font-semibold text-muted hover:text-brand">
<Icon name="group-line" />
Rejoindre un groupe
</button>
}
className="right-96 p-4"
>
<GroupForm chapter={chapter} token={token} />
</Dialog>
)}
</div>
{chapter.startDate && chapter.endDate ? (
<div className="flex items-center gap-x-2">
<Icon name="calendar-line" className="text-sm text-muted" />
<span className="text-sm text-muted">
{new Date(chapter.startDate).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: 'numeric'
})}{' '}
-{' '}
{new Date(chapter.endDate).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: 'numeric'
})}
</span>
</div>
) : (
<div className="h-1 w-1/4 rounded-lg bg-gray-200">
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
</div>
)}
</div>
{isInEventGroup(chapter) && (
<ul className="flex flex-col space-y-4">
{chapter.puzzles &&
chapter.puzzles
.sort((p1, p2) => {
const p1Tags = p1.tags || [];
const p2Tags = p2.tags || [];
const p1Easy = p1Tags.some((tag) => tag.name === 'easy');
const p2Easy = p2Tags.some((tag) => tag.name === 'easy');
if (p1Easy !== p2Easy) {
return p1Easy ? -1 : 1;
}
const p1Medium = p1Tags.some((tag) => tag.name === 'medium');
const p2Medium = p2Tags.some((tag) => tag.name === 'medium');
if (p1Medium !== p2Medium) {
return p1Medium ? -1 : 1;
}
const p1Hard = p1Tags.some((tag) => tag.name === 'hard');
const p2Hard = p2Tags.some((tag) => tag.name === 'hard');
if (p1Hard !== p2Hard) {
return p1Hard ? -1 : 1;
}
return p1Tags.length - p2Tags.length;
})
.map((puzzle) => (
<PuzzleProp key={puzzle.id} puzzle={puzzle} chapter={chapter} />
))}
</ul>
)}
</div>
))) || (
<div className="flex flex-col space-y-6">
{[...Array(3).keys()].map((i) => (
<div key={i} className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<span className="inline-block h-8 w-1/2 rounded-lg bg-primary-600" />
<span
className="inline-block h-1 w-1/4 animate-pulse rounded-lg bg-primary-600"
style={{
animationDelay: `${i * 0.05}s`,
animationDuration: '1s'
}}
/>
</div>
<ul className="flex flex-col space-y-4">
{[...Array(6).keys()].map((j) => (
<span
key={j}
className="inline-block h-14 animate-pulse rounded-lg bg-primary-600"
style={{
animationDelay: `${j * 0.05}s`,
animationDuration: '1s'
}}
/>
))}
</ul>
</div>
))}
</div>
)}
</>
);
}
function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
const isPuzzleAvailable = (chapter: Chapter) => {
const today = new Date();
const startDate = new Date(chapter.startDate!);
const endDate = new Date(chapter.endDate!);
return (
(chapter.startDate && chapter.endDate && today >= startDate && today <= endDate) ||
(!chapter.startDate && !chapter.endDate)
);
};
return (
<li
className={cn(
'group relative flex h-full w-full rounded-md border-2 bg-primary-700 font-code hover:bg-primary-600',
{
'border-green-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
'border-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
'border-red-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
'border-highlight-primary': !puzzle.tags?.length,
'cursor-not-allowed': !isPuzzleAvailable(chapter)
}
)}
>
{isPuzzleAvailable(chapter) ? (
<AppLink
className="flex h-full w-full items-center justify-between p-4"
key={puzzle.id}
href={`/dashboard/puzzles/${puzzle.id}`}
>
<div className="flex w-10/12 flex-col gap-2 md:w-full md:flex-row">
<span className="text-base font-semibold">{puzzle.name}</span>
{puzzle.tags?.length && (
<div className="flex gap-x-2 text-sm text-muted">
{puzzle.tags
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
.map((tag, i) => (
<span
key={i}
className={cn('inline-block rounded-md bg-primary-900 px-2 py-1')}
>
{tag.name}
</span>
))}
</div>
)}
</div>
<Icon
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
name="arrow-right-line"
/>
</AppLink>
) : (
<div className="flex h-full w-full items-center justify-between p-4 opacity-50">
<div className="flex gap-x-2">
<span className="text-base font-semibold">{puzzle.name}</span>
{puzzle.tags?.length && (
<div className="flex gap-x-2 text-sm text-muted">
{puzzle.tags
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
.map((tag, i) => (
<span
key={i}
className={cn('inline-block rounded-md bg-primary-900 px-2 py-1')}
>
{tag.name}
</span>
))}
</div>
)}
</div>
</div>
)}
</li>
);
}
type GroupData = {
name?: string;
chapter?: number;
puzzle?: number;
};
function GroupForm({ chapter, token }: { chapter: Chapter; token: string }) {
const [isJoining, setIsJoining] = useState(false);
const { data: groups } = useGroups({ token });
const { mutate } = useSWRConfig();
const router = useRouter();
const { register, handleSubmit, reset } = useForm<GroupData>({
defaultValues: {
name: undefined,
chapter: chapter.id,
puzzle: undefined
}
});
async function onSubmit(data: GroupData) {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/${isJoining ? 'groupJoin' : 'groupCreate'}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`
}
});
// TODO: handle errors
// if (res.ok) {
// if (!isJoining) {
// mutate('groups');
// } else {
// mutate('me');
// }
// router.refresh();
// }
}
return (
<div className="flex flex-col">
<div className="flex justify-between">
<button
onClick={() => {
setIsJoining(false);
reset();
}}
className={cn('rounded-lg p-2 font-semibold', {
'bg-primary-500': !isJoining
})}
>
Créer un groupe
</button>
<button
onClick={() => {
setIsJoining(true);
reset();
}}
className={cn('rounded-lg p-2 font-semibold', {
'bg-primary-500': isJoining
})}
>
Rejoindre un groupe
</button>
</div>
<div className="px-2 py-4">
<hr className="border-primary-600" />
</div>
<form
className="flex w-full flex-col justify-between"
onSubmit={handleSubmit(onSubmit)}
encType="multipart/form-data"
>
<div className="flex w-56 flex-col space-y-4">
{!isJoining ? (
<>
<div className="flex flex-col">
<Input
className="w-full"
label="Nom du groupe"
type="text"
placeholder="Terre en vue mon capitaine !"
required
{...register('name')}
/>
</div>
</>
) : (
<>
<Select
className="w-full"
label="Groupes disponibles"
required
{...register('name')}
options={
groups
?.filter((group) => group.chapter === chapter.id)
.map((group) => ({
title: group.name,
value: group.name
})) || []
}
/>
</>
)}
<Button kind="brand" type="submit">
{isJoining ? 'Rejoindre' : 'Créer'}
</Button>
</div>
</form>
</div>
);
}