From 187cdec0e2c0b36d8784665ba077c0c6f6992915 Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Tue, 25 Aug 2026 19:39:38 +0200 Subject: [PATCH] =?UTF-8?q?Affiche=20le=20n=C2=B01=20d'Icare=20et=20de=20l?= =?UTF-8?q?a=20Corne=20d'Abondance=20sur=20/leaderboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelle section (game-highlights.tsx) juste sous les raccourcis : avatar, pseudo et score du meneur actuel de chaque mini-jeu, cliquable vers le jeu correspondant. Même règle que les podiums existants : rien affiché tant que le meneur est à 0. Score en text-sea (pas gold-bright, peu lisible sur fond marbre — même choix déjà fait pour Icare/Corne/l'Urne). Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 +- src/app/leaderboard/game-highlights.tsx | 57 +++++++++++++++++++++++++ src/app/leaderboard/page.tsx | 42 ++++++++++++------ 3 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 src/app/leaderboard/game-highlights.tsx 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 (
@@ -44,6 +60,8 @@ export default async function LeaderboardPage() { + + {error && (

Impossible de charger le classement pour le moment.