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 useSWR from 'swr';
|
||||||
import { getScores } from '../leaderboard';
|
import { getScores, getScoresEvent } from '../leaderboard';
|
||||||
|
|
||||||
export function useLeaderboard({ token }: { token: string }) {
|
export function useLeaderboard({ token }: { token: string }) {
|
||||||
return useSWR('leaderboard', () => getScores({ token }));
|
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 fetcher from './fetcher';
|
||||||
import type { Group } from './players';
|
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 { data, status } = await fetcher.get(`/leaderboard`, {
|
||||||
|
@ -21,6 +21,32 @@ export const getScores = async ({ token }: { token: string }): Promise<Score[]>
|
||||||
return scores as 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 = {
|
export type Score = {
|
||||||
score: number;
|
score: number;
|
||||||
tries: number;
|
tries: number;
|
||||||
|
@ -30,3 +56,22 @@ export type Score = {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
rank: number;
|
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