Add missing stuff & changing leaderboard
This commit is contained in:
parent
f41e8d6123
commit
9825f7b3de
14 changed files with 225 additions and 165 deletions
|
@ -1,12 +1,10 @@
|
||||||
import Leaderboard from '@/ui/Leaderboard';
|
import Leaderboard from '@/ui/Leaderboard';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Tableau des scores - Peer-at Code',
|
title: 'Tableau des scores',
|
||||||
description: 'Suivez la progression des élèves en direct'
|
description: 'Suivez la progression des élèves en direct'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const token = cookies().get('token')?.value;
|
return <Leaderboard />;
|
||||||
return <Leaderboard token={token!} />;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getPuzzle } from '@/lib/puzzles';
|
import { getPuzzle } from '@/lib/puzzles';
|
||||||
|
import { getURL } from '@/lib/utils';
|
||||||
import Puzzle from '@/ui/Puzzle';
|
import Puzzle from '@/ui/Puzzle';
|
||||||
import SWRFallback from '@/ui/SWRFallback';
|
import SWRFallback from '@/ui/SWRFallback';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
@ -15,7 +16,22 @@ export async function generateMetadata({ params }: { params: { id: number } }):
|
||||||
|
|
||||||
const puzzle = await getPuzzle({ token, id });
|
const puzzle = await getPuzzle({ token, id });
|
||||||
|
|
||||||
return { title: `${puzzle.name} - Peer-at Code` };
|
if (!puzzle) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: puzzle.name,
|
||||||
|
openGraph: {
|
||||||
|
title: puzzle.name,
|
||||||
|
type: 'website',
|
||||||
|
url: getURL(`/dashboard/puzzles/${puzzle.id}`)
|
||||||
|
// IMAGES WITH OG IMAGE
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: puzzle.name
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params: { id } }: { params: { id: number } }) {
|
export default async function Page({ params: { id } }: { params: { id: number } }) {
|
||||||
|
@ -25,11 +41,7 @@ export default async function Page({ params: { id } }: { params: { id: number }
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const puzzle = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzle/${id}`, {
|
const puzzle = await getPuzzle({ token, id });
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
}).then((res) => res.json());
|
|
||||||
|
|
||||||
if (!puzzle) {
|
if (!puzzle) {
|
||||||
notFound();
|
notFound();
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
import Puzzles from '@/ui/Puzzles';
|
import Puzzles from '@/ui/Puzzles';
|
||||||
|
import SWRFallback from '@/ui/SWRFallback';
|
||||||
|
import { getPuzzles } from '@/lib/puzzles';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Puzzles - Peer-at Code'
|
title: 'Puzzles'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const token = cookieStore.get('token')?.value;
|
const token = cookieStore.get('token')!.value;
|
||||||
|
|
||||||
|
const puzzles = await getPuzzles({ token });
|
||||||
|
|
||||||
|
if (!puzzles) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
{/* <SWRFallback fallback={{ ['puzzles']: chapters }}> */}
|
<SWRFallback fallback={{ ['puzzles']: puzzles }}>
|
||||||
<Puzzles token={token!} />
|
<Puzzles token={token!} />
|
||||||
{/* </SWRFallback> */}
|
</SWRFallback>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function Page() {
|
||||||
<span>
|
<span>
|
||||||
<Console text="Peer-at Code" className="text-6xl" />
|
<Console text="Peer-at Code" className="text-6xl" />
|
||||||
</span>
|
</span>
|
||||||
<AppLink href="/dashboard/puzzles">Commencer</AppLink>
|
<AppLink href="/sign-in">Commencer l'aventure</AppLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="item-center flex h-screen w-full px-2">
|
<div className="item-center flex h-screen w-full px-2">
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
import fetcher from './fetcher';
|
|
||||||
|
|
||||||
export const getGroups = async ({ token }: { token: string }): Promise<Group[]> => {
|
export const getGroups = async ({ token }: { token: string }): Promise<Group[]> => {
|
||||||
const { data, status } = await fetcher.get(`/groups`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/groups`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const groups = data;
|
const groups = (await res.json()) as Group[];
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch groups');
|
throw new Error('Failed to fetch groups');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groups) {
|
if (!groups) {
|
||||||
return [] as Group[];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups as Group[];
|
return groups;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Group = {
|
export type Group = {
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import fetcher from './fetcher';
|
|
||||||
import { type Group } from './groups';
|
import { type Group } from './groups';
|
||||||
|
|
||||||
export const getScores = async ({ token }: { token: string }): Promise<Score[]> => {
|
export const getScores = async ({ token }: { token: string }): Promise<Score[]> => {
|
||||||
const { data, status } = await fetcher.get(`/leaderboard`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/leaderboard`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const scores = data;
|
const scores = (await res.json()) as Score[];
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch scores');
|
throw new Error('Failed to fetch scores');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scores) {
|
if (!scores) {
|
||||||
return [] as Score[];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return scores as Score[];
|
return scores;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getScoresEvent = async ({
|
export const getScoresEvent = async ({
|
||||||
|
@ -28,16 +27,16 @@ export const getScoresEvent = async ({
|
||||||
token: string;
|
token: string;
|
||||||
id: number;
|
id: number;
|
||||||
}): Promise<ScoreEvent> => {
|
}): Promise<ScoreEvent> => {
|
||||||
const { data, status } = await fetcher.get(`/leaderboard/${id}`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/leaderboard/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const scores = data;
|
const scores = (await res.json()) as ScoreEvent;
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch scores');
|
throw new Error('Failed to fetch event scores');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scores) {
|
if (!scores) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import fetcher from './fetcher';
|
|
||||||
import { type Group } from './groups';
|
import { type Group } from './groups';
|
||||||
|
|
||||||
export const getPlayer = async ({
|
export const getPlayer = async ({
|
||||||
|
@ -8,15 +7,15 @@ export const getPlayer = async ({
|
||||||
token: string;
|
token: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
}): Promise<Player | null> => {
|
}): Promise<Player | null> => {
|
||||||
const { data, status } = await fetcher.get(`/player/${username}`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/player/${username}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const player = data;
|
const player = (await res.json()) as Player;
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch player');
|
throw new Error('Failed to fetch player');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +23,7 @@ export const getPlayer = async ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return player as Player;
|
return player;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import fetcher from './fetcher';
|
|
||||||
|
|
||||||
export const getChapters = async ({ token }: { token: string }): Promise<Chapter[]> => {
|
export const getChapters = async ({ token }: { token: string }): Promise<Chapter[]> => {
|
||||||
const { data, status } = await fetcher.get(`/chapters`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chapters`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const chapters = data;
|
const chapters = (await res.json()) as Chapter[];
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch puzzles');
|
throw new Error('Failed to fetch puzzles');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +15,7 @@ export const getChapters = async ({ token }: { token: string }): Promise<Chapter
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapters as Chapter[];
|
return chapters;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getChapter = async ({
|
export const getChapter = async ({
|
||||||
|
@ -27,15 +25,15 @@ export const getChapter = async ({
|
||||||
token: string;
|
token: string;
|
||||||
id: number;
|
id: number;
|
||||||
}): Promise<Chapter | null> => {
|
}): Promise<Chapter | null> => {
|
||||||
const { data, status } = await fetcher.get(`/chapter/${id}`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chapter/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const chapter = data;
|
const chapter = (await res.json()) as Chapter;
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch puzzles');
|
throw new Error('Failed to fetch puzzles');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,40 +41,50 @@ export const getChapter = async ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapter as Chapter;
|
return chapter;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPuzzles = async ({ token }: { token: string }): Promise<Chapter[]> => {
|
export const getPuzzles = async ({ token }: { token: string }): Promise<Chapter[]> => {
|
||||||
const chapters = await getChapters({ token });
|
const chapters = await getChapters({ token });
|
||||||
|
|
||||||
for (let i = 0; i < chapters.length; i++) {
|
if (!chapters) {
|
||||||
const chapter = chapters[i];
|
return [];
|
||||||
const chapterData = await getChapter({ token, id: chapter.id });
|
|
||||||
if (!chapterData) continue;
|
|
||||||
chapters[i].puzzles = chapterData.puzzles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapters as Chapter[];
|
for (let i = 0; i < chapters.length; i++) {
|
||||||
|
let chapter = chapters[i];
|
||||||
|
chapter = (await getChapter({ token, id: chapter.id })) as Chapter;
|
||||||
|
if (!chapter) continue;
|
||||||
|
chapters[i].puzzles = chapter.puzzles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPuzzle = async ({ token, id }: { token: string; id: number }): Promise<Puzzle> => {
|
export const getPuzzle = async ({
|
||||||
const { data, status } = await fetcher.get(`/puzzle/${id}`, {
|
token,
|
||||||
|
id
|
||||||
|
}: {
|
||||||
|
token: string;
|
||||||
|
id: number;
|
||||||
|
}): Promise<Puzzle | null> => {
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzle/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const puzzle = data;
|
const puzzle = (await res.json()) as Puzzle;
|
||||||
|
|
||||||
if (status !== 200) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch puzzle');
|
throw new Error('Failed to fetch puzzle');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!puzzle) {
|
if (!puzzle) {
|
||||||
return {} as Puzzle;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return puzzle as Puzzle;
|
return puzzle;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Puzzle = {
|
export type Puzzle = {
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default function Console({ text, className }: { text: string; className?:
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [typingIndex, setTypingIndex] = useState(0);
|
const [typingIndex, setTypingIndex] = useState(0);
|
||||||
|
|
||||||
const typingDelay = 400; // The delay between each character being typed, in milliseconds
|
const typingDelay = 200; // The delay between each character being typed, in milliseconds
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLeaderboard } from '@/lib/hooks/use-leaderboard';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import AvatarComponent from './Avatar';
|
|
||||||
import Select from './Select';
|
|
||||||
import { type ScoreEvent } from '@/lib/leaderboard';
|
|
||||||
import useSWRSubscription, { type SWRSubscription } from 'swr/subscription';
|
import useSWRSubscription, { type SWRSubscription } from 'swr/subscription';
|
||||||
import { useGroups } from '@/lib/hooks/use-groups';
|
|
||||||
|
import { type ScoreEvent } from '@/lib/leaderboard';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import Podium from '@/ui/events/podium/Podium';
|
||||||
|
|
||||||
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
|
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
|
||||||
|
|
||||||
export default function Leaderboard({ token }: { token: string }) {
|
export default function Leaderboard() {
|
||||||
// const { data, isLoading } = useLeaderboard({ token });
|
// TODO CHANGER CECI
|
||||||
|
|
||||||
const CHAPITRE_EVENT = 3;
|
const CHAPITRE_EVENT = 3;
|
||||||
|
|
||||||
const subscription: SWRSubscription<string, ScoreEvent, Error> = (key, { next }) => {
|
const subscription: SWRSubscription<string, ScoreEvent, Error> = (key, { next }) => {
|
||||||
|
@ -31,34 +27,17 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data } = useSWRSubscription(
|
const { data } = useSWRSubscription(
|
||||||
`wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard${CHAPITRE_EVENT}`,
|
`wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${CHAPITRE_EVENT}`,
|
||||||
subscription
|
subscription
|
||||||
);
|
);
|
||||||
|
|
||||||
const [filter, setFilter] = useState('');
|
const scores = [data?.groups]
|
||||||
|
.flat()
|
||||||
let options = [] as { value: string; title: string }[];
|
.sort((a, b) => a!.rank - b!.rank)
|
||||||
|
.map((group, place) => ({
|
||||||
const { data: groups } = useGroups({ token });
|
...group,
|
||||||
|
place
|
||||||
console.log(groups);
|
}));
|
||||||
|
|
||||||
if (groups) {
|
|
||||||
options = groups
|
|
||||||
.filter((group) => group.chapter === null)
|
|
||||||
.map((group) => ({ value: group.name, title: group.name }))
|
|
||||||
.filter((group, index, self) => self.findIndex((g) => g.value === group.value) === index)
|
|
||||||
.sort((a, b) => a.title.localeCompare(b.title));
|
|
||||||
|
|
||||||
options.unshift({ value: '', title: 'Tous' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
if (filter) {
|
|
||||||
return data?.groups.filter((group) => group.name === filter);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}, [data, filter]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full w-full flex-col space-y-4">
|
<section className="flex h-full w-full flex-col space-y-4">
|
||||||
|
@ -67,23 +46,9 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
<h1 className="text-xl font-semibold">Tableau des scores</h1>
|
<h1 className="text-xl font-semibold">Tableau des scores</h1>
|
||||||
<p className="text-muted">Suivez la progression des élèves en direct</p>
|
<p className="text-muted">Suivez la progression des élèves en direct</p>
|
||||||
</div>
|
</div>
|
||||||
{/* {(filteredData && (
|
|
||||||
<Select
|
|
||||||
className="w-32"
|
|
||||||
options={options || []}
|
|
||||||
value={filter}
|
|
||||||
onChange={(event) => setFilter(event.target.value)}
|
|
||||||
/>
|
|
||||||
)) || (
|
|
||||||
<span
|
|
||||||
className="inline-block h-12 w-32 animate-pulse rounded-lg bg-primary-600"
|
|
||||||
style={{
|
|
||||||
animationDuration: '1s'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</header>
|
</header>
|
||||||
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
||||||
|
{data && <Podium score={scores} />}
|
||||||
<ul className="flex flex-col space-y-2">
|
<ul className="flex flex-col space-y-2">
|
||||||
{data?.groups.map((group, key) => (
|
{data?.groups.map((group, key) => (
|
||||||
<li key={key} className="flex justify-between space-x-2">
|
<li key={key} className="flex justify-between space-x-2">
|
||||||
|
|
|
@ -75,7 +75,10 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between space-y-4">
|
<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}</h1>
|
<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>
|
||||||
<div className="flex h-screen w-full overflow-y-auto">
|
<div className="flex h-screen w-full overflow-y-auto">
|
||||||
<ToHTML className="font-code text-xs sm:text-base" data={puzzle.content} />
|
<ToHTML className="font-code text-xs sm:text-base" data={puzzle.content} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,8 +123,7 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
|
||||||
Tentatives : <span className="text-brand-accent">{puzzle.tries}</span>
|
Tentatives : <span className="text-brand-accent">{puzzle.tries}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Score : <span className="text-brand-accent">{puzzle.score}</span> (Score maximum:{' '}
|
Score : <span className="text-brand-accent">{puzzle.score}</span>
|
||||||
{puzzle.scoreMax})
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AppLink href="/dashboard/puzzles">
|
<AppLink href="/dashboard/puzzles">
|
||||||
|
|
110
ui/Puzzles.tsx
110
ui/Puzzles.tsx
|
@ -1,25 +1,29 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { UserContext } from '@/context/user';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useGroups } from '@/lib/hooks/use-groups';
|
import { useContext, useMemo, useState } from 'react';
|
||||||
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 { useForm } from 'react-hook-form';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
import AppLink from './AppLink';
|
import AppLink from './AppLink';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Dialog from './Dialog';
|
import Dialog from './Dialog';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useSWRConfig } from 'swr';
|
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';
|
||||||
|
|
||||||
export default function Puzzles({ token }: { token: string }) {
|
export default function Puzzles({ token }: { token: string }) {
|
||||||
const { data: me } = useContext(UserContext);
|
const { data: me } = useContext(UserContext);
|
||||||
const { data, isLoading } = usePuzzles({ token });
|
const { data, isLoading } = usePuzzles({ token });
|
||||||
const [isOpen, setIsOpen] = useState<boolean[]>([]);
|
const [isOpen, setIsOpen] = useState<boolean[]>([]);
|
||||||
|
const [filter, setFilter] = useState<string>('');
|
||||||
|
const [filterChapter, setFilterChapter] = useState<number>();
|
||||||
|
|
||||||
function handleClick(index: number) {
|
function handleClick(index: number) {
|
||||||
setIsOpen((prevState) => {
|
setIsOpen((prevState) => {
|
||||||
|
@ -37,12 +41,23 @@ export default function Puzzles({ token }: { token: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (filter && filterChapter) {
|
||||||
|
console.log(filter);
|
||||||
|
return data
|
||||||
|
?.find((chapter) => chapter.id === filterChapter)
|
||||||
|
?.puzzles.filter((puzzle) => puzzle!.tags!.some((tag) => tag.name === filter))
|
||||||
|
.map((puzzle) => puzzle);
|
||||||
|
}
|
||||||
|
return data?.find((chapter) => chapter.id === filterChapter)?.puzzles;
|
||||||
|
}, [data, filter, filterChapter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(!isLoading &&
|
{(!isLoading &&
|
||||||
data?.map((chapter) => (
|
data?.map((chapter) => (
|
||||||
<div key={chapter.id} className="flex flex-col space-y-4">
|
<div key={chapter.id} className="flex flex-col">
|
||||||
<div className="flex flex-col justify-between lg:flex-row lg:items-center">
|
<div className="flex flex-col justify-between md:flex-row md:items-center">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h1 className="text-xl font-semibold">{chapter.name}</h1>
|
<h1 className="text-xl font-semibold">{chapter.name}</h1>
|
||||||
{!isInEventGroup(chapter) && (
|
{!isInEventGroup(chapter) && (
|
||||||
|
@ -63,6 +78,7 @@ export default function Puzzles({ token }: { token: string }) {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
{chapter.startDate && chapter.endDate ? (
|
{chapter.startDate && chapter.endDate ? (
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Icon name="calendar-line" className="text-sm text-muted" />
|
<Icon name="calendar-line" className="text-sm text-muted" />
|
||||||
|
@ -87,11 +103,23 @@ export default function Puzzles({ token }: { token: string }) {
|
||||||
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
|
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="mt-1 flex justify-end">
|
||||||
|
{isInEventGroup(chapter) && (
|
||||||
|
<FilterChapter
|
||||||
|
chapters={data}
|
||||||
|
chapter={chapter}
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
setFilterChapter={setFilterChapter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isInEventGroup(chapter) && (
|
{isInEventGroup(chapter) && (
|
||||||
<ul className="flex flex-col space-y-4">
|
<ul className="mt-4 flex flex-col space-y-4">
|
||||||
{chapter.puzzles &&
|
{filteredData &&
|
||||||
chapter.puzzles
|
filteredData
|
||||||
.sort((p1, p2) => {
|
.sort((p1, p2) => {
|
||||||
const p1Tags = p1.tags || [];
|
const p1Tags = p1.tags || [];
|
||||||
const p2Tags = p2.tags || [];
|
const p2Tags = p2.tags || [];
|
||||||
|
@ -181,17 +209,19 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
||||||
key={puzzle.id}
|
key={puzzle.id}
|
||||||
href={`/dashboard/puzzles/${puzzle.id}`}
|
href={`/dashboard/puzzles/${puzzle.id}`}
|
||||||
>
|
>
|
||||||
<div className="flex w-10/12 flex-col gap-2 md:w-full md:flex-row">
|
<div className="flex w-10/12 flex-col gap-2 lg:w-full lg:flex-row">
|
||||||
<span className="text-base font-semibold">{puzzle.name}</span>
|
<span className="text-base font-semibold">
|
||||||
|
{puzzle.name}{' '}
|
||||||
|
<span className="text-sm text-highlight-secondary">
|
||||||
|
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
{puzzle.tags?.length && (
|
{puzzle.tags?.length && (
|
||||||
<div className="flex gap-x-2 text-sm text-muted">
|
<div className="flex gap-x-2 text-sm text-muted">
|
||||||
{puzzle.tags
|
{puzzle.tags
|
||||||
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
|
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
|
||||||
.map((tag, i) => (
|
.map((tag, i) => (
|
||||||
<span
|
<span key={i} className="inline-block rounded-md bg-primary-900 px-2 py-1">
|
||||||
key={i}
|
|
||||||
className={cn('inline-block rounded-md bg-primary-900 px-2 py-1')}
|
|
||||||
>
|
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
@ -206,7 +236,9 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-between p-4 opacity-50">
|
<div className="flex h-full w-full items-center justify-between p-4 opacity-50">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<span className="text-base font-semibold">{puzzle.name}</span>
|
<span className="text-base font-semibold">
|
||||||
|
{puzzle.name} ({puzzle.scoreMax})
|
||||||
|
</span>
|
||||||
{puzzle.tags?.length && (
|
{puzzle.tags?.length && (
|
||||||
<div className="flex gap-x-2 text-sm text-muted">
|
<div className="flex gap-x-2 text-sm text-muted">
|
||||||
{puzzle.tags
|
{puzzle.tags
|
||||||
|
@ -228,6 +260,44 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilterChapter({
|
||||||
|
chapters,
|
||||||
|
chapter,
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
setFilterChapter
|
||||||
|
}: {
|
||||||
|
chapters: Chapter[];
|
||||||
|
chapter: Chapter;
|
||||||
|
filter: string;
|
||||||
|
setFilter: (filter: string) => void;
|
||||||
|
setFilterChapter: (chapter: number) => void;
|
||||||
|
}) {
|
||||||
|
let options = [] as { title: string; value: string }[];
|
||||||
|
|
||||||
|
options = chapters
|
||||||
|
.find((c) => c.id === chapter.id)
|
||||||
|
?.puzzles?.map((p) => p.tags)
|
||||||
|
.flat()
|
||||||
|
.filter((tag, index, self) => self.findIndex((t) => t!.name === tag!.name) === index)
|
||||||
|
.map((t) => {
|
||||||
|
return { title: t!.name, value: t!.name };
|
||||||
|
}) as { title: string; value: string }[];
|
||||||
|
|
||||||
|
options?.unshift({ title: 'Tout les puzzles', value: '' });
|
||||||
|
|
||||||
|
setFilterChapter(chapter.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="w-44"
|
||||||
|
options={options || []}
|
||||||
|
value={filter}
|
||||||
|
onChange={(event) => setFilter(event.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type GroupData = {
|
type GroupData = {
|
||||||
name?: string;
|
name?: string;
|
||||||
chapter?: number;
|
chapter?: number;
|
||||||
|
|
|
@ -2,27 +2,27 @@
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import Mardown from 'react-markdown';
|
import Mardown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import remarkBreaks from 'remark-breaks';
|
import remarkBreaks from 'remark-breaks';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
export default function ToHTML({ data, className }: { data: string; className?: string }) {
|
export default function ToHTML({ data, className }: { data: string; className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={cn(className)}>
|
<div className={cn(className)}>
|
||||||
<Mardown
|
<Mardown
|
||||||
components={{
|
components={{
|
||||||
a: ({ ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
{...props}
|
{...props}
|
||||||
className="text-brand-accent hover:text-brand hover:underline"
|
className="inline text-brand-accent hover:text-brand hover:underline"
|
||||||
// MAKE thIS SHIT DOWNLOAD
|
// MAKE thIS SHIT DOWNLOADABLE
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
code: ({ ...props }) => (
|
code: ({ node, ...props }) => (
|
||||||
<code
|
<code
|
||||||
{...props}
|
{...props}
|
||||||
className="cursor-pointer select-none text-transparent hover:text-highlight-secondary"
|
className="cursor-default select-none text-transparent transition-colors delay-150 hover:text-highlight-secondary"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default function PodiumStep({
|
||||||
visible: () => ({
|
visible: () => ({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
delay: podium.length - group.place + 1,
|
delay: podium.length - group.place + 0.5,
|
||||||
duration: 0.75
|
duration: 0.75
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -44,7 +44,7 @@ export default function PodiumStep({
|
||||||
height: 200 * ((podium.length - group.place) / podium.length),
|
height: 200 * ((podium.length - group.place) / podium.length),
|
||||||
opacity: 2,
|
opacity: 2,
|
||||||
transition: {
|
transition: {
|
||||||
delay: podium.length - group.place,
|
delay: podium.length - group.place - 0.5,
|
||||||
duration: 1.25,
|
duration: 1.25,
|
||||||
ease: 'backInOut'
|
ease: 'backInOut'
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue