5cb19caec2
Boutons de validation des points : - Les icônes ✓/✕ étaient trop grandes (40px) et dupliquées entre le podium et le reste du classement. Factorisées dans un composant partagé PointsConfirmControls (32px, icône 16px, olive/oxblood, aria-label + tooltip), utilisé à l'identique par JudgePointControls sur le podium ET dans la liste. Nouvelle page /calendrier — "Le Calendrier des Dieux" : - Modèle de données : days (date, dieu, domaine), events (titre, type, heure, lieu, created_by forcé par trigger), settings (ligne unique, date du Tribunal). RLS : lecture ouverte à tout authentifié, écriture (insert/update/delete) réservée au rôle judge par policies dédiées — jamais un simple masquage des boutons côté client. - 8 préréglages de divinités (Dionysos, Arès, Athéna, Aphrodite, Hermès, Poséidon, Hadès, Zeus) avec domaine, couleur d'accent et emblème SVG ; un Archonte peut aussi saisir une divinité libre. - Bannière avec compte à rebours avant la date du Tribunal, éditable par les Archontes. Journées en cartes (jour courant mis en évidence, jours passés estompés, jour du Tribunal en accent oxblood), liste d'événements avec icône par type (activité/défi/épreuve/tribunal). Mode édition (ajout/modification/suppression avec confirmation) réservé aux Archontes. Realtime sur les trois tables. - Ajouté au menu déroulant du header, route protégée par le proxy. Rendu des icônes dynamiques (GodEmblem, EventTypeIcon) via branchement JSX explicite plutôt que variable de composant résolue à l'exécution, pour respecter react-hooks/static-components. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
12 KiB
Markdown
149 lines
12 KiB
Markdown
@AGENTS.md
|
|
|
|
# Le Tribunal
|
|
|
|
Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des olympiades pendant une semaine de vacances : comptes, rôles, leaderboard en direct, attribution de points par des juges, journal public des événements. Habillée sur le thème d'un tribunal de la Grèce antique (voir section 6).
|
|
|
|
## 1. État actuel
|
|
|
|
V1 à V5 sont livrées :
|
|
- **V1** : comptes, profils, leaderboard statique.
|
|
- **V2** : authentification par email réel (plus de pseudo+email interne), rôles `public`/`judge`, verrou de pseudo (modifiable une fois), section Admin.
|
|
- **V3** : attribution de points par les juges (RPC sécurisée, +/- avec confirmation explicite), journal public « le crieur », podium top 3, flèches de progression, recadrage/compression photo côté client.
|
|
- **V4** : direction artistique Grèce antique (marbre/or/mer de nuit, polices Cinzel/Cormorant Garamond/Manrope, vocabulaire Archontes/Citoyens/Agora/Crieur), header global avec chip utilisateur.
|
|
- **V5** : navigation entièrement repliée dans le menu déroulant du chip (header épuré : logo + chip seulement), page **La Roulette** (tirage au sort animé), page **Le Calendrier des Dieux** (agenda de la semaine, panthéon, date du Tribunal), boutons de validation des points unifiés (`PointsConfirmControls`) entre podium et classement.
|
|
|
|
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, le jeu du Tribunal lui-même (Char, ostracisme, timer du Gardien), mode grand écran.
|
|
|
|
---
|
|
|
|
## 2. Stack technique
|
|
|
|
- **Framework** : Next.js 16 (App Router) + TypeScript
|
|
- **Style** : Tailwind CSS v4 (design tokens en variables CSS, voir `globals.css`)
|
|
- **Auth + Base + Stockage + Temps réel** : Supabase (Auth, Postgres, Storage, Realtime)
|
|
- **Recadrage photo** : `react-easy-crop` + canvas natif (resize/compression client, jamais d'upload de l'image brute)
|
|
- **Déploiement** : Vercel (front) + Supabase (managé) — pas encore déployé, tourne en local pour l'instant
|
|
|
|
Next.js 16 a renommé `middleware.ts` en `proxy.ts` (fichier `src/proxy.ts`, fonction `proxy`). `src/lib/supabase/middleware.ts` est un nom de fichier utilitaire interne, sans rapport avec cette convention.
|
|
|
|
---
|
|
|
|
## 3. Modèle de données (`supabase/schema.sql`, source de vérité)
|
|
|
|
Le script est **idempotent** : toujours le ré-exécuter en entier après une modification, jamais de migration incrémentale séparée.
|
|
|
|
### `public.profiles` (liée à `auth.users`, créée automatiquement par un trigger sur `auth.users`)
|
|
|
|
| Colonne | Notes |
|
|
|-----------------|-------------------------------------------------------------------|
|
|
| `id` | = `auth.users.id` |
|
|
| `pseudo` | affiché partout, unique (insensible à la casse) |
|
|
| `avatar_url` | URL publique Storage (`{user_id}.webp`) |
|
|
| `points` | jamais modifiable directement — uniquement via RPC `award_points` |
|
|
| `role` | `public` (défaut) ou `judge` |
|
|
| `pseudo_locked` | passe à `true` automatiquement au 1er changement de pseudo |
|
|
| `previous_rank` | repère de classement, mis à jour par `reset_rank_reference()` |
|
|
|
|
### `public.points_log` (journal, lecture seule côté client)
|
|
|
|
`id, target_id, judge_id, delta, reason, created_at` — écrit uniquement par la RPC `award_points`.
|
|
|
|
### `public.days` / `public.events` / `public.settings` (Calendrier des Dieux)
|
|
|
|
- `days` : `id, date (unique), god_name, god_domain, description, created_at`.
|
|
- `events` : `id, day_id (fk days), title, description, type ('activite'|'defi'|'epreuve'|'tribunal'), start_time, location, created_by (fk profiles, forcé par trigger), created_at`.
|
|
- `settings` : ligne unique (`id boolean primary key default true`, contrainte `check (id)`) — `tribunal_date`.
|
|
- RLS : `SELECT` ouvert à tout authentifié ; `INSERT`/`UPDATE`/`DELETE` réservés au rôle `judge`, vérifié par policy (`exists (select 1 from profiles where id = auth.uid() and role = 'judge')`) — pas de trigger `BEFORE UPDATE` ici car il n'y a pas de colonne à protéger *dans une ligne par ailleurs modifiable par tous* (contrairement à `profiles`) : toute la table est verrouillée en écriture aux juges.
|
|
|
|
### Colonnes sensibles : RLS + trigger, jamais confiance au client
|
|
|
|
La RLS est **au niveau ligne** : elle ne peut pas exprimer « cette colonne seulement si tel rôle ». Le verrouillage fin des colonnes (`role`, `points`, `pseudo_locked`, pseudo figé, `previous_rank`) passe par le trigger `enforce_profile_update()` :
|
|
- `points` : bloqué pour **tout le monde**, y compris les juges, en écriture directe. Seule la RPC `award_points()` (SECURITY DEFINER) peut la modifier, via un flag de session (`app.bypass_points_lock`) qui autorise explicitement CETTE écriture précise.
|
|
- `role`, `previous_rank` : modifiables uniquement par un `judge`.
|
|
- `pseudo` : modifiable une fois par le propriétaire (auto-lock ensuite), ou à tout moment par un `judge`.
|
|
- `auth.uid() is null` (SQL Editor, migrations) bypass le trigger : ces accès sont déjà fiables par construction.
|
|
|
|
Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accès aux colonnes) est la convention du projet — le reproduire pour toute nouvelle colonne sensible plutôt que d'inventer autre chose.
|
|
|
|
### RPC exposées
|
|
|
|
- `is_pseudo_taken(p_pseudo)` — anon + authenticated, ne renvoie qu'un booléen (l'anon ne peut pas lire `profiles`).
|
|
- `award_points(p_target_id, p_delta, p_reason)` — authenticated, vérifie `role = 'judge'` côté serveur, jamais côté client.
|
|
- `reset_rank_reference()` — authenticated, vérifie `role = 'judge'`, fige le classement courant dans `previous_rank`.
|
|
|
|
---
|
|
|
|
## 4. Authentification
|
|
|
|
Email + mot de passe (Supabase Auth impose l'unicité de l'email). Confirmation d'email **activée** — nécessite un SMTP custom configuré côté Supabase (le service intégré est trop limité en volume, voir README). Le pseudo est un simple nom d'affichage stocké dans `profiles`, jamais l'email n'est montré aux autres membres.
|
|
|
|
---
|
|
|
|
## 5. Pages & navigation
|
|
|
|
| Route | Contenu |
|
|
|----------------|---------------------------------------------------------------------------|
|
|
| `/` | 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 |
|
|
| `/roulette` | « La Roulette » — tirage au sort animé parmi les membres (ou Citoyens uniquement) |
|
|
| `/calendrier` | « Le Calendrier des Dieux » — agenda de la semaine, panthéon, date du Tribunal, édition réservée aux juges |
|
|
| `/journal` | « Le Crieur » — fil live des décrets (attributions de points), lecture pour tous |
|
|
| `/profile` | Pseudo (verrouillable), photo, déconnexion |
|
|
| `/admin` | « Le Conseil des Archontes » — juges uniquement, **vérifié côté serveur** (Server Component) — gestion des membres, reset du repère de classement |
|
|
|
|
`/leaderboard`, `/profile`, `/admin`, `/journal`, `/roulette`, `/calendrier` sont protégées par `src/proxy.ts` (redirection `/login` si non connecté). L'accès juge-only de `/admin` n'est **pas** géré par le proxy (il n'a pas facilement le rôle) — c'est la page elle-même qui vérifie et redirige ; même principe pour les boutons d'édition du Calendrier (RLS + vérification serveur, jamais un simple masquage front).
|
|
|
|
Le header global (`components/header.tsx`) est rendu dans `layout.tsx` pour tout utilisateur connecté : logo (couronne de laurier + « Le Tribunal », jamais masqué même à 320px) à gauche, chip utilisateur (avatar, pseudo, points, jamais masqués) à droite. **Aucun lien de navigation dans le header lui-même** — le chip ouvre un menu déroulant qui contient toutes les pages (avec icônes), fermeture au clic extérieur/Échap, entrée courante surlignée.
|
|
|
|
---
|
|
|
|
## 6. Look & feel — Grèce antique
|
|
|
|
Thème **fixe et codé en dur** (pas de système de re-thématisation par édition — voir roadmap). Un tribunal égéen de nuit : marbre, or, mer sombre, sceaux de cire.
|
|
|
|
Tokens couleur (`globals.css`, exposés comme utilities Tailwind via `@theme inline`) :
|
|
- `--ink #0A1B33` / `--ink-2 #0F2748` : fond de l'app (dégradés radiaux mer de nuit), header, boutons primaires.
|
|
- `--marble #F4ECD8` / `--marble-2 #E9DEC2` : surfaces claires (`.marble-surface`, classe utilitaire avec léger dégradé/grain).
|
|
- `--gold #C9A227` / `--gold-bright #E7C560` : accents, bordures, 1ʳᵉ place. **Jamais** pour du texte courant sur `marble` (contraste insuffisant) — utiliser `--text-marble` à la place.
|
|
- `--oxblood #A5342A` : blâmes (points négatifs), sceaux, erreurs.
|
|
- `--olive #5E6B3B` : honneurs (points positifs), succès.
|
|
- `--sea #2E6E7E` : accent secondaire.
|
|
- `--silver #C7CDD6` / `--bronze #B08D57` : accents 2ᵉ/3ᵉ place du podium.
|
|
- `--text-marble #2A2116` / `--text-mut #7A6A4C` : texte sur surfaces claires.
|
|
|
|
`components/points-confirm-controls.tsx` : boutons ✓/✕ compacts (32px, icône 16px) de validation/annulation des points en attente — utilisés à l'identique par le podium et le reste du classement (`JudgePointControls`). Ne pas dupliquer cette UI ailleurs, toujours passer par ce composant.
|
|
|
|
Polices (`next/font/google`, variables CSS, exposées comme `font-heading`/`font-serif`/`font-sans`) : **Cinzel** (titres, noms, chiffres de score — majuscules, `tracking-[0.1em]`), **Cormorant Garamond** (citations/sous-titres, italique), **Manrope** (interface, corps de texte).
|
|
|
|
Ornements en SVG/CSS inline (jamais d'image bitmap) :
|
|
- `components/laurel-wreath.tsx` — couronne de laurier calculée par trigonométrie (deux arcs symétriques de feuilles), utilisée en logo, sur le rang 1 du podium, etc. Le favicon (`src/app/icon.svg`) est une version statique des mêmes coordonnées.
|
|
- `.meander-divider` (classe globale) — frise à méandre grecque en `background-image` SVG data-URI, séparateur discret.
|
|
- `components/diamond-divider.tsx` — séparateur ◆ avec filets dégradés.
|
|
- `.marble-surface` (classe globale) — texture marbre via dégradés (pas d'image de bruit).
|
|
|
|
Convention de carte : `marble-surface rounded-2xl border border-gold/40 shadow-xl` (formulaires, podium) ou `rounded-lg border-gold/25` pour les lignes de liste. Bouton primaire : `bg-ink-2 text-gold-bright font-heading uppercase hover:bg-ink`. Bouton secondaire : `border border-gold/30 text-text-marble hover:bg-gold/10`.
|
|
|
|
Vocabulaire diégétique (toujours accompagné du terme fonctionnel dans le code/les commentaires, pour que ça reste maintenable) : juges/admins → **Archontes**, membres → **Citoyens**, leaderboard → **L'Agora**, journal des points → **Le Crieur** (décrets), points positifs/négatifs → **Honneurs**/**Blâmes**, points → **gloires**, section admin → **Le Conseil des Archontes**.
|
|
|
|
---
|
|
|
|
## 7. Conventions
|
|
|
|
- Toujours passer par les policies RLS + trigger ; ne jamais exposer la `service_role key` côté client.
|
|
- Toute nouvelle colonne sensible sur `profiles` (ou une future table) suit le pattern RLS+trigger de la section 3, pas un `grant`/`revoke` par colonne (qui ne peut pas distinguer les rôles applicatifs).
|
|
- Aperçus locaux d'image avant upload (crop, fichier choisi) = `blob:`/`data:` URL → toujours `unoptimized` sur `next/image` ou une balise `<img>` classique (`next/image` ne sait pas résoudre ces schémas côté serveur).
|
|
- Commandes : `npm run dev`, `npm run build`, `npm run lint`.
|
|
- Le schéma SQL (`supabase/schema.sql`) est la seule source de vérité pour la base — le tenir à jour à chaque changement de données/sécurité, et le garder idempotent.
|
|
|
|
---
|
|
|
|
## 8. Roadmap (pas encore codé)
|
|
|
|
- **Éditions** : table `editions` (année, mood board) ; les points deviendront rattachables à une édition donnée. Le thème visuel (Grèce antique) reste fixe, ce n'est pas un système de re-thématisation par édition.
|
|
- **Le jeu du Tribunal** : le Char (zone de pénalité), batailles de cul sec, ostracisme, timer du Gardien. (La Roulette est livrée en V5.)
|
|
- **Mode grand écran** : vue leaderboard optimisée pour vidéoprojecteur.
|
|
- **Déploiement** : pas encore fait (Vercel + Supabase managé prévus, voir README).
|