diff --git a/CLAUDE.md b/CLAUDE.md
index e37f89d..5ec13b2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -179,7 +179,7 @@ Deuxième volet de l'audit (revue complète de tout le code applicatif, pas seul
| `/` | Redirige vers `/leaderboard` si connecté, sinon `/login` |
| `/login` | Email + mot de passe |
| `/signup` | Email + mot de passe + pseudo + photo (recadrée) ; gère l'attente de confirmation email |
-| `/leaderboard` | « L'Agora » — podium top 3 + liste, Realtime, contrôles de points pour les juges, date/décompte du Tribunal en en-tête (`src/lib/tribunal-date.ts`, partagé avec `/calendrier`). Joue le rôle de page d'accueil (`/` y redirige) : une rangée de raccourcis (`quick-links.tsx`, icônes réutilisées du menu du header) vers les pages d'activité — jeux, Mur, Urne, Crieur — juste au-dessus du classement, avec une pastille façon iOS sur le raccourci du Mur (nombre de publications du jour, via `wall_notes_for_date()` — pas de lecture directe de `wall_notes`, dont la RLS ne laisserait voir que ses propres notes) |
+| `/leaderboard` | « L'Agora » — podium top 3 + liste, Realtime, contrôles de points pour les juges, date/décompte du Tribunal en en-tête (`src/lib/tribunal-date.ts`, partagé avec `/calendrier`). Joue le rôle de page d'accueil (`/` y redirige) : une rangée de raccourcis (`quick-links.tsx`, icônes réutilisées du menu du header) vers les pages d'activité — jeux, Mur, Urne, Crieur — juste au-dessus du classement, avec une pastille façon iOS sur le raccourci du Mur (nombre de publications du jour, via `wall_notes_for_date()` — pas de lecture directe de `wall_notes`, dont la RLS ne laisserait voir que ses propres notes). Juste en dessous, `game-highlights.tsx` affiche le n°1 actuel d'Icare et de la Corne d'Abondance (lecture directe de `icarus_scores`/`melon_scores`, RLS déjà ouverte à tout authentifié) — même règle que les podiums de ces deux jeux : pas de champion affiché tant que le meneur est à 0 |
| `/icare` | « Le Vol d'Icare » — Flappy Bird grec, record personnel all-time, scores figés au Tribunal, points attribués automatiquement |
| `/corne` | « La Corne d'Abondance » — jeu de fusion façon Watermelon Game (avatars aléatoires, physique `matter-js`), même mécanique de gloires que Le Vol d'Icare |
| `/char` | « Le Jeu de l'Agora » — format paysage (projeté) : Le Char (2 colonnes de 3, boutons de duel), Le Gardien du Silence (tirage aléatoire, minuteur discret) et La Roulette (tirage au sort animé) |
diff --git a/src/app/leaderboard/game-highlights.tsx b/src/app/leaderboard/game-highlights.tsx
new file mode 100644
index 0000000..3ee9be3
--- /dev/null
+++ b/src/app/leaderboard/game-highlights.tsx
@@ -0,0 +1,57 @@
+import Link from "next/link";
+import type { ComponentType } from "react";
+import { Avatar } from "@/components/avatar";
+import { IconHorn, IconWings } from "@/components/icons";
+
+export type GameLeader = { pseudo: string; avatarUrl: string | null; score: number } | null;
+
+function LeaderCard({
+ href,
+ label,
+ icon: Icon,
+ leader,
+}: {
+ href: string;
+ label: string;
+ icon: ComponentType<{ className?: string }>;
+ leader: GameLeader;
+}) {
+ return (
+
+
+
+
{label}
+ {leader ? (
+
+
+ {leader.pseudo}
+
+ ) : (
+
Personne n'a encore joué
+ )}
+
+ {leader && {leader.score}}
+
+ );
+}
+
+// Comme les podiums (voir leaderboard/icare/corne) : un meneur à 0 n'est pas
+// un vrai champion, juste un score par défaut — rien à afficher tant que
+// personne n'a vraiment marqué (leader passé à null par la page).
+export function GameHighlights({
+ icarusLeader,
+ melonLeader,
+}: {
+ icarusLeader: GameLeader;
+ melonLeader: GameLeader;
+}) {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx
index d09e68d..4b241b2 100644
--- a/src/app/leaderboard/page.tsx
+++ b/src/app/leaderboard/page.tsx
@@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import { LeaderboardView } from "./leaderboard-view";
import { QuickLinks } from "./quick-links";
+import { GameHighlights, type GameLeader } from "./game-highlights";
import { AgoraLogo } from "@/components/agora-logo";
import { formatCountdown, formatTribunalDateTime } from "@/lib/tribunal-date";
@@ -15,22 +16,37 @@ export default async function LeaderboardPage() {
redirect("/login");
}
- const [{ data: profiles, error }, { data: settings }, { data: wallNotesToday }] = await Promise.all([
- supabase
- .from("profiles")
- .select("id, pseudo, avatar_url, points, previous_rank, role")
- .order("points", { ascending: false }),
- supabase.from("settings").select("tribunal_date").eq("id", true).single(),
- // wall_notes_for_date() (jamais une lecture directe de wall_notes, dont
- // la RLS ne laisserait voir que ses propres notes) : seul moyen de
- // connaître le nombre total de publications du jour sans exposer qui a
- // publié quoi — sert juste de compteur pour la pastille du raccourci.
- supabase.rpc("wall_notes_for_date"),
- ]);
+ const [{ data: profiles, error }, { data: settings }, { data: wallNotesToday }, { data: icarusTop }, { data: melonTop }] =
+ await Promise.all([
+ supabase
+ .from("profiles")
+ .select("id, pseudo, avatar_url, points, previous_rank, role")
+ .order("points", { ascending: false }),
+ supabase.from("settings").select("tribunal_date").eq("id", true).single(),
+ // wall_notes_for_date() (jamais une lecture directe de wall_notes, dont
+ // la RLS ne laisserait voir que ses propres notes) : seul moyen de
+ // connaître le nombre total de publications du jour sans exposer qui a
+ // publié quoi — sert juste de compteur pour la pastille du raccourci.
+ supabase.rpc("wall_notes_for_date"),
+ supabase.from("icarus_scores").select("user_id, best_score").order("best_score", { ascending: false }).limit(1),
+ supabase.from("melon_scores").select("user_id, best_score").order("best_score", { ascending: false }).limit(1),
+ ]);
const isJudge = profiles?.find((p) => p.id === user.id)?.role === "judge";
const tribunalDate = settings?.tribunal_date ?? null;
+ const profileById = new Map((profiles ?? []).map((p) => [p.id, p]));
+ // Comme les podiums d'Icare/Corne : un meneur à 0 (personne n'a encore
+ // joué) n'est pas un champion, pas de couronne pour un score par défaut.
+ function buildLeader(rows: { user_id: string; best_score: number }[] | null): GameLeader {
+ const top = rows?.[0];
+ if (!top || top.best_score <= 0) return null;
+ const profile = profileById.get(top.user_id);
+ return { pseudo: profile?.pseudo ?? "Un Citoyen", avatarUrl: profile?.avatar_url ?? null, score: top.best_score };
+ }
+ const icarusLeader = buildLeader(icarusTop);
+ const melonLeader = buildLeader(melonTop);
+
return (