Ajoute une pastille sur le raccourci du Mur de la Honte

Nombre de publications du jour, façon notification iOS, sur le raccourci /mur de /leaderboard — via wall_notes_for_date() (une lecture directe de wall_notes ne montrerait, à cause de la RLS, que ses propres notes, pas le total).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-25 19:33:29 +02:00
parent 0eaebb7a05
commit 6922d33e39
3 changed files with 33 additions and 14 deletions
+1 -1
View File
@@ -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` | | `/` | Redirige vers `/leaderboard` si connecté, sinon `/login` |
| `/login` | Email + mot de passe | | `/login` | Email + mot de passe |
| `/signup` | Email + mot de passe + pseudo + photo (recadrée) ; gère l'attente de confirmation email | | `/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 | | `/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) |
| `/icare` | « Le Vol d'Icare » — Flappy Bird grec, record personnel all-time, scores figés au Tribunal, points attribués automatiquement | | `/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 | | `/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é) | | `/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é) |
+7 -2
View File
@@ -15,12 +15,17 @@ export default async function LeaderboardPage() {
redirect("/login"); redirect("/login");
} }
const [{ data: profiles, error }, { data: settings }] = await Promise.all([ const [{ data: profiles, error }, { data: settings }, { data: wallNotesToday }] = await Promise.all([
supabase supabase
.from("profiles") .from("profiles")
.select("id, pseudo, avatar_url, points, previous_rank, role") .select("id, pseudo, avatar_url, points, previous_rank, role")
.order("points", { ascending: false }), .order("points", { ascending: false }),
supabase.from("settings").select("tribunal_date").eq("id", true).single(), 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 isJudge = profiles?.find((p) => p.id === user.id)?.role === "judge"; const isJudge = profiles?.find((p) => p.id === user.id)?.role === "judge";
@@ -37,7 +42,7 @@ export default async function LeaderboardPage() {
{tribunalDate && <p className="text-xs text-marble/50">{formatTribunalDateTime(tribunalDate)}</p>} {tribunalDate && <p className="text-xs text-marble/50">{formatTribunalDateTime(tribunalDate)}</p>}
</div> </div>
<QuickLinks /> <QuickLinks wallNoteCount={wallNotesToday?.length ?? 0} />
{error && ( {error && (
<p role="alert" className="text-sm text-oxblood"> <p role="alert" className="text-sm text-oxblood">
+25 -11
View File
@@ -15,19 +15,33 @@ const LINKS = [
{ href: "/journal", label: "Le Crieur", icon: IconScroll }, { href: "/journal", label: "Le Crieur", icon: IconScroll },
] as const; ] as const;
export function QuickLinks() { export function QuickLinks({ wallNoteCount }: { wallNoteCount: number }) {
return ( return (
<nav aria-label="Raccourcis" className="mb-6 grid grid-cols-3 gap-2 sm:grid-cols-6 sm:gap-3"> <nav aria-label="Raccourcis" className="mb-6 grid grid-cols-3 gap-2 sm:grid-cols-6 sm:gap-3">
{LINKS.map(({ href, label, icon: Icon }) => ( {LINKS.map(({ href, label, icon: Icon }) => {
<Link // Pastille "façon iPhone" (nombre de publications du jour) : pour
key={href} // l'instant seul le Mur en a une, les autres raccourcis n'ont pas
href={href} // d'équivalent "à consulter aujourd'hui" aussi direct.
className="marble-surface flex flex-col items-center gap-1.5 rounded-xl border border-gold/25 px-2 py-3 text-center shadow-sm transition hover:border-gold/50 hover:shadow-md" const badge = href === "/mur" ? wallNoteCount : 0;
> return (
<Icon className="h-6 w-6 text-gold" /> <Link
<span className="text-[0.65rem] leading-tight font-medium text-text-marble sm:text-xs">{label}</span> key={href}
</Link> href={href}
))} className="marble-surface relative flex flex-col items-center gap-1.5 rounded-xl border border-gold/25 px-2 py-3 text-center shadow-sm transition hover:border-gold/50 hover:shadow-md"
>
{badge > 0 && (
<span
className="absolute -top-1.5 -right-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-oxblood px-1 text-[0.65rem] font-bold text-marble shadow"
aria-label={`${badge} publication${badge > 1 ? "s" : ""} aujourd'hui sur le Mur de la Honte`}
>
{badge}
</span>
)}
<Icon className="h-6 w-6 text-gold" />
<span className="text-[0.65rem] leading-tight font-medium text-text-marble sm:text-xs">{label}</span>
</Link>
);
})}
</nav> </nav>
); );
} }