Added event leaderboard
This commit is contained in:
parent
fef736f547
commit
82fbf1ecba
6 changed files with 221 additions and 2 deletions
19
app/(event)/event/[id]/page.tsx
Normal file
19
app/(event)/event/[id]/page.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import EventLeaderboard from '@/ui/events/Leaderboard';
|
||||
import { cookies } from 'next/headers';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Tableau des scores - Peer-at Code',
|
||||
description: 'Suivez la progression des élèves en direct'
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: { id: number } }) {
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const token = cookies().get('token')?.value;
|
||||
return <EventLeaderboard token={token!} id={id} />;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
import useSWR from 'swr';
|
||||
import { getScores } from '../leaderboard';
|
||||
import { getScores, getScoresEvent } from '../leaderboard';
|
||||
|
||||
export function useLeaderboard({ token }: { token: string }) {
|
||||
return useSWR('leaderboard', () => getScores({ token }));
|
||||
}
|
||||
|
||||
export function useLeaderboardEvent({ token, id }: { token: string; id: number }) {
|
||||
return useSWR(`leaderboard/${id}`, () => getScoresEvent({ token, id }));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import fetcher from './fetcher';
|
||||
import type { Group } from './players';
|
||||
import { type Group } from './groups';
|
||||
|
||||
export const getScores = async ({ token }: { token: string }): Promise<Score[]> => {
|
||||
const { data, status } = await fetcher.get(`/leaderboard`, {
|
||||
|
@ -21,6 +21,32 @@ export const getScores = async ({ token }: { token: string }): Promise<Score[]>
|
|||
return scores as Score[];
|
||||
};
|
||||
|
||||
export const getScoresEvent = async ({
|
||||
token,
|
||||
id
|
||||
}: {
|
||||
token: string;
|
||||
id: number;
|
||||
}): Promise<ScoreEvent> => {
|
||||
const { data, status } = await fetcher.get(`/leaderboard/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const scores = data;
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to fetch scores');
|
||||
}
|
||||
|
||||
if (!scores) {
|
||||
return {} as ScoreEvent;
|
||||
}
|
||||
|
||||
return scores as ScoreEvent;
|
||||
};
|
||||
|
||||
export type Score = {
|
||||
score: number;
|
||||
tries: number;
|
||||
|
@ -30,3 +56,22 @@ export type Score = {
|
|||
avatar: string;
|
||||
rank: number;
|
||||
};
|
||||
|
||||
export type ScoreEvent = {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
groups: [
|
||||
{
|
||||
name: string;
|
||||
rank: number;
|
||||
players: [
|
||||
{
|
||||
pseudo: string;
|
||||
tries: number;
|
||||
completions: number;
|
||||
score: number;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
|
|
67
ui/events/Leaderboard.tsx
Normal file
67
ui/events/Leaderboard.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
'use client';
|
||||
|
||||
import { useLeaderboardEvent } from '@/lib/hooks/use-leaderboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
// import { Timer } from '../Timer';
|
||||
import Podium from './podium/Podium';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
|
||||
|
||||
export default function EventLeaderboard({ token, id }: { token: string; id: number }) {
|
||||
const { data, isLoading } = useLeaderboardEvent({ token, id });
|
||||
|
||||
const scores = [data?.groups]
|
||||
.flat()
|
||||
.sort((a, b) => a!.rank - b!.rank)
|
||||
.map((group, place) => ({
|
||||
...group,
|
||||
place
|
||||
}));
|
||||
|
||||
return (
|
||||
<section className="flex h-full w-full flex-col space-y-4 p-4">
|
||||
{!isLoading && data && <Podium score={scores} />}
|
||||
{/* <Timer targetDate={data && data.end_date} /> */}
|
||||
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
||||
<ul className="flex flex-col space-y-2">
|
||||
{!isLoading &&
|
||||
data?.groups.map((group, key) => (
|
||||
<li key={key} className="flex justify-between space-x-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className={cn('font-semibold', SCORE_COLORS[group.rank - 1])}>
|
||||
{group.rank}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
|
||||
<span className="text-lg">{group.name}</span>
|
||||
<span className="text-sm text-muted">
|
||||
{group.players
|
||||
?.map((p) => p.pseudo)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">Essaies</span>
|
||||
<span className="text-lg text-muted">
|
||||
{group.players.reduce((a, b) => a + b.tries, 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">Score</span>
|
||||
<span className="text-lg text-muted">
|
||||
{group.players.reduce((a, b) => a + b.score, 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
}
|
20
ui/events/podium/Podium.tsx
Normal file
20
ui/events/podium/Podium.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import PodiumStep from './PodiumStep';
|
||||
|
||||
export default function Podium({ score }: { score: any }) {
|
||||
const podium = [2, 0, 1]
|
||||
.reduce((podiumOrder, position) => [...podiumOrder, score[position]] as any, [])
|
||||
.filter(Boolean);
|
||||
|
||||
console.log(podium);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mt-8 grid grid-flow-col-dense place-content-center content-end items-end justify-center justify-items-center gap-2"
|
||||
style={{ height: 250 }}
|
||||
>
|
||||
{podium.map((group: any, index: number) => (
|
||||
<PodiumStep key={index} podium={podium} group={group} index={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
64
ui/events/podium/PodiumStep.tsx
Normal file
64
ui/events/podium/PodiumStep.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { motion } from 'framer-motion';
|
||||
|
||||
export const positions: Record<number, string> = {
|
||||
0: '1 er',
|
||||
1: '2 ème',
|
||||
2: '3 ème'
|
||||
};
|
||||
|
||||
export default function PodiumStep({
|
||||
podium,
|
||||
group,
|
||||
index
|
||||
}: {
|
||||
podium: any;
|
||||
group: any;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<motion.div
|
||||
custom={index}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
visible: () => ({
|
||||
opacity: 1,
|
||||
transition: {
|
||||
delay: podium.length - group.place + 1,
|
||||
duration: 0.75
|
||||
}
|
||||
}),
|
||||
hidden: { opacity: 0 }
|
||||
}}
|
||||
className="w-16 items-center justify-center text-center"
|
||||
>
|
||||
<span className="font-semibold text-white">{group.name}</span>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
custom={index}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
visible: () => ({
|
||||
height: 200 * ((podium.length - group.place) / podium.length),
|
||||
opacity: 2,
|
||||
transition: {
|
||||
delay: podium.length - group.place,
|
||||
duration: 1.25,
|
||||
ease: 'backInOut'
|
||||
}
|
||||
}),
|
||||
hidden: { opacity: 0, height: 0 }
|
||||
}}
|
||||
className="flex w-16 cursor-pointer place-content-center rounded-t-lg bg-brand shadow-lg hover:bg-opacity-80"
|
||||
style={{
|
||||
marginBottom: -1,
|
||||
filter: `opacity(${0.1 + (podium.length - group.place) / podium.length})`
|
||||
}}
|
||||
>
|
||||
<span className="self-end font-semibold text-white">{positions[group.place]}</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Add table
Reference in a new issue