diff --git a/CLAUDE.md b/CLAUDE.md index faf347f..3581a74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des ol V1, V2 et V3 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 Administration. +- **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. Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/thèmes, le jeu du Tribunal lui-même (Char, roulette, ostracisme, timer du Gardien), mode grand écran. diff --git a/README.md b/README.md index b1048d3..fb03a4c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Application web pour animer et scorer le jeu **« Le Tribunal »** entre amis pe - 👤 **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 **Administration** +- ⚖️ **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 @@ -137,7 +137,7 @@ Le profil (`public.profiles`) est créé automatiquement à l'inscription via un ### 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 **Administration**. +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 @@ -148,7 +148,7 @@ select id, pseudo from public.profiles; update public.profiles set role = 'judge' where id = ''; ``` -Ensuite, un juge peut promouvoir/rétrograder d'autres membres et éditer leur pseudo depuis la page **Administration** (visible uniquement par les juges, et vérifié côté serveur — pas seulement caché en front). +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 @@ -171,7 +171,7 @@ Le service email intégré de Supabase (sans SMTP custom) limite l'envoi à quel - **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 RPC `award_points`) ou **Annuler**. Le client n'écrit jamais directement la colonne `points` : 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 **Administration**. +- **Flèches de progression** : ▲/▼/= comparent le rang actuel au dernier repère. Un juge fige un nouveau repère via **Nouveau round** dans **Admin**. --- diff --git a/src/app/admin/admin-members-table.tsx b/src/app/admin/admin-members-table.tsx index 78b59d2..616d0d0 100644 --- a/src/app/admin/admin-members-table.tsx +++ b/src/app/admin/admin-members-table.tsx @@ -78,46 +78,49 @@ function MemberRow({ member, isSelf }: { member: Member; isSelf: boolean }) { return (
  • - +
    + + setPseudo(event.target.value)} + className="w-full flex-1 rounded-md border border-navy/20 bg-white px-2 py-1 text-sm outline-none focus:border-gold sm:w-auto" + /> +
    - setPseudo(event.target.value)} - className="flex-1 rounded-md border border-navy/20 bg-white px-2 py-1 text-sm outline-none focus:border-gold" - /> +
    + - + {pseudoLocked ? ( + + ) : ( + pseudo libre + )} - {pseudoLocked ? ( - ) : ( - pseudo libre - )} - - +
    {error &&

    {error}

    } {success && !dirty &&

    OK

    } diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c990e44..a84f0a2 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -33,7 +33,7 @@ export default async function AdminPage() { return (
    -

    Administration

    +

    Admin

    diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..828782b --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/app/leaderboard/leaderboard-view.tsx b/src/app/leaderboard/leaderboard-view.tsx index 300210a..6462149 100644 --- a/src/app/leaderboard/leaderboard-view.tsx +++ b/src/app/leaderboard/leaderboard-view.tsx @@ -99,20 +99,25 @@ export function LeaderboardView({ return (
  • - - {profile.rank} - - - {profile.pseudo} - - {profile.points} - {isJudge && ( - - )} +
    + + {profile.rank} + + + {profile.pseudo} + + {profile.points} +
    +
    + {profile.points} + {isJudge && ( + + )} +
  • ); })} diff --git a/src/components/judge-point-controls.tsx b/src/components/judge-point-controls.tsx index 95e4cb5..0577d24 100644 --- a/src/components/judge-point-controls.tsx +++ b/src/components/judge-point-controls.tsx @@ -50,12 +50,16 @@ export function JudgePointControls({ setReason(""); } + const buttonHeight = compact ? "h-6" : "h-7"; + const buttonWidth = compact ? "w-6" : "w-7"; + const inputWidth = compact ? "w-9" : "w-12"; + return ( -
    +
    + + ); + return ( ); } diff --git a/src/components/podium.tsx b/src/components/podium.tsx index d29e104..b518358 100644 --- a/src/components/podium.tsx +++ b/src/components/podium.tsx @@ -36,8 +36,8 @@ export function Podium({ } return ( -
    -
    +
    +
    {topRanks.map((rank) => { const members = byRank.get(rank)!; const style = SLOT_STYLES[rank] ?? SLOT_STYLES[3]; @@ -52,7 +52,7 @@ export function Podium({ avatarUrl={member.avatar_url} size={rank === 1 ? "lg" : "md"} /> - + {member.pseudo} {member.points} pts @@ -67,7 +67,7 @@ export function Podium({ ))}
    {rank}
    diff --git a/supabase/schema.sql b/supabase/schema.sql index b35e8a7..13259dd 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -198,7 +198,16 @@ insert into storage.buckets (id, name, public) values ('avatars', 'avatars', true) on conflict (id) do nothing; --- Bucket public => la lecture se fait via l'URL publique, pas besoin de policy SELECT. +-- Le bucket public sert la lecture via l'URL publique (hors RLS), mais une +-- policy SELECT reste nécessaire : pour un upload en upsert (remplacement +-- d'une photo existante), Postgres doit pouvoir lire la ligne existante en +-- interne pour évaluer la clause USING de l'UPDATE — sans SELECT, l'upsert +-- échoue avec "new row violates row-level security policy" même si les +-- policies INSERT/UPDATE sont correctes. +drop policy if exists "Avatars are publicly viewable" on storage.objects; +create policy "Avatars are publicly viewable" + on storage.objects for select + using (bucket_id = 'avatars'); drop policy if exists "Users can upload their own avatar" on storage.objects; create policy "Users can upload their own avatar"