From 9f54b459f396a36015325a24c431e47184ca43cd Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Mon, 27 Jul 2026 18:53:54 +0200 Subject: [PATCH] =?UTF-8?q?G=C3=A9n=C3=A9ralise=20Podium=20et=20computeRan?= =?UTF-8?q?ks=20au-del=C3=A0=20des=20points?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit computeRanksBy(items, comparator) remplace la logique de classement "ex-aequo partagent le rang" en la rendant générique ; computeRanks devient un simple appel avec le comparateur points desc, sans rien changer pour l'appelant existant. Podium accepte maintenant un prop renderValue (+ valueLabel optionnel) à la place du "points/gloires" en dur, pour pouvoir afficher un classement par temps (Course du Char) avec le même composant que le classement par points. --- src/app/leaderboard/leaderboard-view.tsx | 2 ++ src/components/podium.tsx | 21 ++++++++++++------- src/lib/ranking.ts | 26 +++++++++++++++--------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/app/leaderboard/leaderboard-view.tsx b/src/app/leaderboard/leaderboard-view.tsx index d9bd9b8..3468056 100644 --- a/src/app/leaderboard/leaderboard-view.tsx +++ b/src/app/leaderboard/leaderboard-view.tsx @@ -89,6 +89,8 @@ export function LeaderboardView({
p.rank <= 3)} + renderValue={(member) => member.points} + valueLabel="gloires" isJudge={isJudge} onApplyDelta={applyOptimisticDelta} /> diff --git a/src/components/podium.tsx b/src/components/podium.tsx index a2801fa..6916762 100644 --- a/src/components/podium.tsx +++ b/src/components/podium.tsx @@ -1,9 +1,10 @@ +import type { ReactNode } from "react"; import { Avatar } from "@/components/avatar"; import { JudgePointControls } from "@/components/judge-point-controls"; import { LaurelWreath } from "@/components/laurel-wreath"; -import { toRoman, type RankedProfile } from "@/lib/ranking"; +import { toRoman, type Ranked } from "@/lib/ranking"; -type PodiumProfile = { id: string; pseudo: string; avatar_url: string | null; points: number }; +type PodiumProfile = { id: string; pseudo: string; avatar_url: string | null }; const SLOT_STYLES: Record< number, @@ -14,16 +15,20 @@ const SLOT_STYLES: Record< 3: { pedestal: "h-14 sm:h-20", order: "order-3", ring: "ring-bronze", delay: "0ms" }, }; -export function Podium({ +export function Podium({ profiles, + renderValue, + valueLabel, isJudge = false, onApplyDelta, }: { - profiles: RankedProfile[]; + profiles: Ranked[]; + renderValue: (member: Ranked) => ReactNode; + valueLabel?: string; isJudge?: boolean; onApplyDelta?: (id: string, delta: number) => void; }) { - const byRank = new Map[]>(); + const byRank = new Map[]>(); for (const profile of profiles) { const group = byRank.get(profile.rank) ?? []; group.push(profile); @@ -77,9 +82,11 @@ export function Podium({ isFirst ? "text-xl sm:text-2xl" : "text-base sm:text-lg" }`} > - {member.points} + {renderValue(member)} - gloires + {valueLabel && ( + {valueLabel} + )} {isJudge && onApplyDelta && ( = T & { rank: number }; +export type Ranked = T & { rank: number }; +export type RankedProfile = Ranked; -// Classement "compétition" : les ex-aequo partagent le même rang, et le -// rang suivant saute en conséquence (1, 2, 2, 4 — pas 1, 2, 2, 3). -export function computeRanks(profiles: T[]): RankedProfile[] { - const sorted = [...profiles].sort((a, b) => b.points - a.points); - let lastPoints: number | null = null; +// Classement "compétition" générique : les ex-aequo (selon le comparateur) +// partagent le même rang, et le rang suivant saute en conséquence +// (1, 2, 2, 4 — pas 1, 2, 2, 3). +export function computeRanksBy(items: T[], compare: (a: T, b: T) => number): Ranked[] { + const sorted = [...items].sort(compare); + let lastItem: T | null = null; let lastRank = 0; - return sorted.map((profile, index) => { - if (lastPoints === null || profile.points !== lastPoints) { + return sorted.map((item, index) => { + if (lastItem === null || compare(item, lastItem) !== 0) { lastRank = index + 1; - lastPoints = profile.points; + lastItem = item; } - return { ...profile, rank: lastRank }; + return { ...item, rank: lastRank }; }); } +export function computeRanks(profiles: T[]): RankedProfile[] { + return computeRanksBy(profiles, (a, b) => b.points - a.points); +} + const ROMAN_DIGITS: [number, string][] = [ [10, "X"], [9, "IX"],