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"],