Design & UX : - Renomme "Administration" en "Admin" (nav, page, docs). - Logo original (marteau de juge stylisé navy/or) en favicon (src/app/icon.svg, remplace le favicon.ico par défaut de create-next-app) et dans la navbar (components/logo.tsx). - Navbar : menu hamburger sur mobile (les liens débordaient à partir de ~4 entrées sur petit écran). - Leaderboard : lignes en deux niveaux sur mobile (infos membre / contrôles juges) au lieu d'un seul flex-wrap qui devenait illisible. - Contrôles de points juges (JudgePointControls) : boutons et champ montant réduits en mode compact pour tenir dans les colonnes du podium sur petit écran. - Podium : scroll horizontal de secours (overflow-x-auto) si le contenu dépasse malgré tout sur très petits écrans. - Table admin : pseudo/avatar sur une ligne, rôle/statut/actions sur une autre en mobile (le flex-1 sur l'input pseudo n'avait aucun effet en layout colonne). Correctif sécurité/fonctionnel : - storage.objects n'avait que des policies INSERT et UPDATE pour le bucket avatars, pas de policy SELECT. Postgres a besoin de lire la ligne existante pour évaluer la clause USING d'un UPDATE : sans SELECT, tout remplacement de photo (upsert) échouait avec "new row violates row-level security policy", même si les policies INSERT/UPDATE étaient correctes. Ajout d'une policy SELECT publique sur le bucket avatars (cohérent avec le fait que le bucket est déjà public en lecture). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 KiB
Le Tribunal
Application web pour animer et scorer le jeu « Le Tribunal » entre amis pendant une semaine de vacances : comptes, rôles, leaderboard en direct, attribution de points par les juges et journal public des événements. Le socle est ré-thématisable par édition (un thème différent chaque année).
Fonctionnalités
- 🔐 Création de compte par email + mot de passe (email confirmé, un compte par email)
- 👤 Connexion / déconnexion, pseudo affiché partout (jamais l'email)
- 🖼️ Profil éditable : pseudo modifiable une seule fois puis figé (sauf par un juge), photo recadrée en rond, redimensionnée et compressée côté client
- 🏆 Leaderboard en direct (Supabase Realtime) : podium top 3 (or/argent/bronze, gestion des égalités), flèches ▲▼ de progression depuis le dernier « round »
- ⚖️ Rôles :
public/judge. Les juges attribuent des points (+/- avec montant personnalisé et motif, confirmation explicite avant envoi) et gèrent les membres depuis une section Admin - 📣 Le crieur : journal public et en direct de toutes les attributions de points
- 📱 Responsive, pensé mobile-first et propre en vidéoprojection
Stack technique
| Rôle | Techno |
|---|---|
| Framework | Next.js 16 (App Router) + TypeScript |
| Style | Tailwind CSS v4 |
| Auth · Base · Stockage · Temps réel | Supabase (Auth, Postgres, Storage, Realtime) |
| Recadrage photo | react-easy-crop + canvas (resize/compression client) |
| Email transactionnel | SMTP custom (ex. Resend) branché sur Supabase Auth |
| Déploiement | Vercel + Supabase (managé) |
Prérequis
- Node.js 20.19+ (Next.js 16 / React 19)
- Un compte Supabase (offre gratuite suffisante)
- Un fournisseur SMTP gratuit (ex. Resend, 100 emails/jour) — le service email intégré de Supabase a un rate limit trop bas pour un usage réel
- Un compte Vercel pour le déploiement (optionnel en local)
Installation
1. Cloner et installer
git clone ssh://git@git.alxczl.fr:7878/valentin/tribunal-app.git
cd tribunal-app
npm install
2. Créer le projet Supabase
- Créez un nouveau projet sur supabase.com.
- Dans SQL Editor, exécutez tout le contenu de
supabase/schema.sql(tablesprofiles/points_log, policies RLS, triggers, RPC, bucketavatars, Realtime). Le script est idempotent, vous pouvez le ré-exécuter sans risque après une mise à jour. - Récupérez l'URL du projet et la clé
anondans Project Settings → API. - Dans Authentication → Providers → Email, activez "Confirm email" (un vrai email par personne, condition d'unicité des comptes).
- Dans Authentication → Settings → SMTP Settings, branchez un fournisseur SMTP custom (ex. Resend) — le service email par défaut de Supabase est bridé à quelques emails/heure. Voir Email transactionnel ci-dessous.
- Une fois votre propre compte créé et confirmé via
/signup, désignez-vous juge (voir Rôles & administration).
3. Variables d'environnement
cp .env.local.example .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=votre_cle_anon
⚠️ Ne mettez jamais la clé
service_roledans le front ni dans le dépôt.
4. Lancer en local
npm run dev
L'application tourne sur http://localhost:3000.
Scripts
| Commande | Description |
|---|---|
npm run dev |
Serveur de développement |
npm run build |
Build de production |
npm run start |
Lance le build de production |
npm run lint |
Vérification du code (ESLint) |
Déploiement (Vercel)
- Importez le dépôt dans Vercel.
- Renseignez les variables d'environnement (
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY). - Déployez. Vercel rebuild automatiquement à chaque push sur la branche principale.
Structure du projet
tribunal-app/
├─ src/
│ ├─ app/
│ │ ├─ login/page.tsx # Connexion (email + mot de passe)
│ │ ├─ signup/page.tsx # Création de compte + photo recadrée
│ │ ├─ leaderboard/
│ │ │ ├─ page.tsx # Fetch initial + rôle de l'utilisateur
│ │ │ └─ leaderboard-view.tsx # Realtime, podium, liste, contrôles juges
│ │ ├─ journal/ # « Le crieur » : fil d'événements en direct
│ │ ├─ admin/ # Gestion des membres + reset du repère (juges)
│ │ ├─ profile/ # Édition pseudo (verrou) / photo, déconnexion
│ │ ├─ layout.tsx # Layout + navigation
│ │ └─ page.tsx # Redirection selon l'état de connexion
│ ├─ components/ # Avatar, AvatarPicker (recadrage), NavBar, Podium,
│ │ │ # JudgePointControls
│ ├─ lib/
│ │ ├─ ranking.ts # Classement avec égalités + calcul des flèches
│ │ ├─ image.ts # Recadrage/compression canvas côté client
│ │ └─ supabase/ # Clients Supabase (browser / server / proxy)
│ └─ proxy.ts # Protection des routes + rafraîchissement session
├─ supabase/schema.sql # Schéma complet + policies RLS + triggers + RPC (versionné)
├─ .env.local.example
└─ README.md
Authentification
La connexion se fait par email + mot de passe ; Supabase impose l'unicité de l'email, ce qui empêche la création de comptes multiples. Le pseudo est purement un nom d'affichage (leaderboard, profil, journal) : l'email n'est jamais montré aux autres membres. La confirmation d'email est obligatoire (voir SMTP).
Le profil (public.profiles) est créé automatiquement à l'inscription via un trigger sur auth.users (fonctionne même si l'email n'est pas encore confirmé).
Verrou de pseudo
Chaque membre peut changer son pseudo une seule fois depuis /profile. Dès l'enregistrement, pseudo_locked passe à true et le champ devient en lecture seule (« pseudo figé — contacte un juge pour le changer »). Un juge peut toujours modifier le pseudo de quiconque et, s'il le souhaite, redéverrouiller un compte depuis Admin.
Rôles & administration
Deux rôles : public (défaut) et judge. Les 2 premiers juges sont désignés manuellement via le SQL Editor :
select id, pseudo from public.profiles;
update public.profiles set role = 'judge' where id = '<uuid-du-membre>';
Ensuite, un juge peut promouvoir/rétrograder d'autres membres et éditer leur pseudo depuis la page Admin (visible uniquement par les juges, et vérifié côté serveur — pas seulement caché en front).
Email transactionnel
Le service email intégré de Supabase (sans SMTP custom) limite l'envoi à quelques emails par heure — largement insuffisant dès qu'on teste à plusieurs. Configurez un SMTP custom dans Authentication → Settings → SMTP Settings, par exemple avec Resend (gratuit, 100 emails/jour) :
| Champ | Valeur |
|---|---|
| Sender email | onboarding@resend.dev (domaine de test Resend) ou une adresse d'un domaine que vous avez vérifié |
| Host | smtp.resend.com |
| Port | 465 |
| Username | resend (en minuscules) |
| Password | votre clé API Resend |
Avec le domaine de test
onboarding@resend.dev, Resend ne livre qu'à l'adresse email du compte Resend lui-même. Pour envoyer à tous vos amis, vérifiez votre propre domaine dans Resend.
Points, journal et classement
- Attribution de points : depuis le leaderboard, un juge clique
+/−(montant personnalisable, motif optionnel) — le delta reste en attente (affiché en couleur) jusqu'à un clic sur Confirmer (envoie la RPCaward_points) ou Annuler. Le client n'écrit jamais directement la colonnepoints: la RPC vérifie le rôle juge côté serveur, applique le delta (négatif autorisé, pas de plancher à 0) et journalise l'opération. - Le crieur (
/journal) : fil de tous les événements (« Untel a donné +5 à Untel — motif »), visible par tous les authentifiés, alimenté par Supabase Realtime. - Podium : top 3 mis en avant (or/argent/bronze), gère proprement les égalités (rangs partagés) et les groupes de moins de 3 membres.
- Flèches de progression : ▲/▼/= comparent le rang actuel au dernier repère. Un juge fige un nouveau repère via Nouveau round dans Admin.
Sécurité
- Mots de passe hachés et gérés par Supabase Auth (jamais stockés en clair).
- Row Level Security activée sur
profilesetpoints_log. La RLS étant au niveau ligne, les colonnes sensibles (role,points,pseudo_locked, pseudo figé) sont verrouillées par un triggerBEFORE UPDATEqui ne fait jamais confiance à ce qu'envoie le client. - Les points ne sont modifiables que via la RPC
award_points(SECURITY DEFINER), jamais par une écriture directe — même un juge ne peut pas modifier la colonnepointsà la main. points_logest en lecture seule pour les clients ; seule la RPC peut y écrire.- La page
/adminvérifie le rôle côté serveur (Server Component) avant de rendre quoi que ce soit — pas de simple masquage front.
Ré-thématisation
Les couleurs sont définies comme design tokens (variables CSS), afin de pouvoir changer l'habillage d'une édition à l'autre sans toucher aux composants. Palette par défaut : navy #0F2748, or #C9A227, ivoire #F4ECD8, encre #2A2116.
Roadmap
- Éditions & thèmes — année, nom du thème, mood board, vocabulaire configurable
- Le jeu du Tribunal — le Char, batailles de cul sec, ostracisme, roulette, timer du Gardien
- Mode grand écran — vue leaderboard optimisée vidéoprojecteur
Licence
Projet personnel et privé, destiné à un usage entre amis.