Added event leaderboard

This commit is contained in:
Théo 2023-04-11 11:22:21 +02:00
parent fef736f547
commit 82fbf1ecba
6 changed files with 221 additions and 2 deletions

View 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} />;
}

View file

@ -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 }));
}

View file

@ -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
View 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>
);
}

View 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>
);
}

View 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>
);
}