Files
tribunal-app/src/components/judge-point-controls.tsx
T
Valentin ROBIN e6f4463cb0 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>
2026-07-11 16:47:43 +02:00

123 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabase/client";
export function JudgePointControls({
memberId,
onApplyDelta,
compact = false,
}: {
memberId: string;
onApplyDelta: (id: string, delta: number) => void;
compact?: boolean;
}) {
const [amount, setAmount] = useState("1");
const [reason, setReason] = useState("");
const [pending, setPending] = useState(0);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const parsedAmount = Math.max(1, Math.abs(Number.parseInt(amount, 10) || 1));
function handleCancel() {
setPending(0);
setReason("");
setError(null);
}
async function handleConfirm() {
if (pending === 0) return;
setSubmitting(true);
setError(null);
const supabase = createClient();
const { error: rpcError } = await supabase.rpc("award_points", {
p_target_id: memberId,
p_delta: pending,
p_reason: reason.trim() || null,
});
setSubmitting(false);
if (rpcError) {
setError("Échec, réessaie.");
return;
}
onApplyDelta(memberId, pending);
setPending(0);
setReason("");
}
const buttonHeight = compact ? "h-6" : "h-7";
const buttonWidth = compact ? "w-6" : "w-7";
const inputWidth = compact ? "w-9" : "w-12";
return (
<div className="flex flex-wrap items-center gap-1 sm:gap-1.5">
<button
type="button"
onClick={() => setPending((p) => p - parsedAmount)}
className={`${buttonHeight} ${buttonWidth} shrink-0 rounded-md border border-navy/20 text-sm font-bold text-navy hover:bg-navy/5`}
aria-label={`Retirer ${parsedAmount} points`}
>
</button>
<input
type="number"
min={1}
value={amount}
onChange={(event) => setAmount(event.target.value)}
className={`${buttonHeight} ${inputWidth} rounded-md border border-navy/20 px-1 text-center text-xs`}
/>
<button
type="button"
onClick={() => setPending((p) => p + parsedAmount)}
className={`${buttonHeight} ${buttonWidth} shrink-0 rounded-md border border-navy/20 text-sm font-bold text-navy hover:bg-navy/5`}
aria-label={`Ajouter ${parsedAmount} points`}
>
+
</button>
{!compact && pending !== 0 && (
<input
type="text"
value={reason}
onChange={(event) => setReason(event.target.value)}
placeholder="motif (optionnel)"
className="h-7 w-28 rounded-md border border-navy/20 px-2 text-xs sm:w-36"
/>
)}
{pending !== 0 && (
<>
<span
className={`text-sm font-semibold ${pending > 0 ? "text-green-700" : "text-red-700"}`}
>
{pending > 0 ? `+${pending}` : pending}
</span>
<button
type="button"
onClick={handleConfirm}
disabled={submitting}
className="rounded-md bg-navy px-2 py-1 text-xs font-medium text-ivory hover:bg-navy/90 disabled:opacity-50"
>
{submitting ? "…" : "Confirmer"}
</button>
<button
type="button"
onClick={handleCancel}
disabled={submitting}
className="rounded-md border border-navy/20 px-2 py-1 text-xs text-navy hover:bg-navy/5 disabled:opacity-50"
>
Annuler
</button>
</>
)}
{error && <span className="text-xs text-red-700">{error}</span>}
</div>
);
}