Design responsive, logo, et correctif RLS upload photo

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>
This commit is contained in:
Valentin ROBIN
2026-07-11 16:47:43 +02:00
parent abbd079cd2
commit e6f4463cb0
12 changed files with 171 additions and 76 deletions
+36 -33
View File
@@ -78,46 +78,49 @@ function MemberRow({ member, isSelf }: { member: Member; isSelf: boolean }) {
return (
<li className="flex flex-col gap-2 rounded-lg border border-navy/10 bg-white px-4 py-3 shadow-sm sm:flex-row sm:items-center sm:gap-4">
<Avatar pseudo={member.pseudo} avatarUrl={member.avatar_url} size="sm" />
<div className="flex items-center gap-2">
<Avatar pseudo={member.pseudo} avatarUrl={member.avatar_url} size="sm" />
<input
type="text"
value={pseudo}
onChange={(event) => 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"
/>
</div>
<input
type="text"
value={pseudo}
onChange={(event) => 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"
/>
<div className="flex flex-wrap items-center gap-2">
<select
value={role}
onChange={(event) => setRole(event.target.value as Role)}
disabled={isSelf}
className="rounded-md border border-navy/20 bg-white px-2 py-1 text-sm outline-none focus:border-gold disabled:opacity-50"
>
<option value="public">public</option>
<option value="judge">judge</option>
</select>
<select
value={role}
onChange={(event) => setRole(event.target.value as Role)}
disabled={isSelf}
className="rounded-md border border-navy/20 bg-white px-2 py-1 text-sm outline-none focus:border-gold disabled:opacity-50"
>
<option value="public">public</option>
<option value="judge">judge</option>
</select>
{pseudoLocked ? (
<button
type="button"
onClick={handleUnlock}
disabled={saving}
className="whitespace-nowrap rounded-md border border-navy/20 px-2 py-1 text-xs text-navy hover:bg-navy/5 disabled:opacity-50"
>
Déverrouiller le pseudo
</button>
) : (
<span className="whitespace-nowrap text-xs text-ink/50">pseudo libre</span>
)}
{pseudoLocked ? (
<button
type="button"
onClick={handleUnlock}
disabled={saving}
className="whitespace-nowrap rounded-md border border-navy/20 px-2 py-1 text-xs text-navy hover:bg-navy/5 disabled:opacity-50"
onClick={handleSave}
disabled={saving || !dirty}
className="whitespace-nowrap rounded-md bg-navy px-3 py-1.5 text-sm font-medium text-ivory transition hover:bg-navy/90 disabled:opacity-40 sm:ml-auto"
>
Déverrouiller le pseudo
{saving ? "…" : "Enregistrer"}
</button>
) : (
<span className="whitespace-nowrap text-xs text-ink/50">pseudo libre</span>
)}
<button
type="button"
onClick={handleSave}
disabled={saving || !dirty}
className="whitespace-nowrap rounded-md bg-navy px-3 py-1.5 text-sm font-medium text-ivory transition hover:bg-navy/90 disabled:opacity-40"
>
{saving ? "…" : "Enregistrer"}
</button>
</div>
{error && <p className="text-xs text-red-700">{error}</p>}
{success && !dirty && <p className="text-xs text-green-700">OK</p>}
+1 -1
View File
@@ -33,7 +33,7 @@ export default async function AdminPage() {
return (
<div className="mx-auto w-full max-w-3xl px-4 py-8">
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
<h1 className="text-2xl font-semibold text-navy">Administration</h1>
<h1 className="text-2xl font-semibold text-navy">Admin</h1>
<ResetRoundButton />
</div>
<AdminMembersTable members={members ?? []} currentUserId={user.id} />