Corrections UI, refonte podium, page Roulette, navigation en menu unique
Upload de photo (signup + profil) : - Remplace le contrôle natif du navigateur par un composant entièrement en français : input masqué (sr-only) déclenché par un vrai bouton (icône appareil photo + « Choisir une photo »/« Changer ma photo »), nom du fichier sélectionné affiché (tronqué) avec une croix pour le retirer, « Aucune photo sélectionnée » sinon. Header : plus jamais masqué sur mobile - "LE TRIBUNAL" et le chip utilisateur (photo + pseudo + points) restent visibles à toutes les tailles, y compris 320px — tailles de police et troncature au lieu de `hidden`. Vérifié sans débordement horizontal à 320/375/414/768px. Navigation : tout dans le menu déroulant - Retire les onglets du header (ne garde que logo + chip). Le menu déroulant du chip contient désormais toutes les pages (Le Classement, La Roulette, Le Crieur, Mon profil, Le Conseil des Archontes si juge) avec une icône chacune (components/icons.tsx), fermeture au clic extérieur et à Échap, entrée de la page courante surlignée, déconnexion séparée en oxblood. Podium : - Contrôles de points en icônes ✓/✕ rondes (40×40, olive/oxblood) sous la carte du membre, aria-label + tooltip — uniquement sur le podium, le reste du classement garde ses contrôles textuels. - Refonte visuelle : socles marbre veiné avec arête lumineuse (.pedestal-marble), chiffres romains gravés (toRoman(), .engraved), couronne de laurier + halo doré sur le 1er, accents argent/bronze sur 2e/3e, points en gros chiffres dorés, animation d'entrée décalée par socle (~600ms, respecte prefers-reduced-motion). Nouvelle page /roulette : - Tirage aléatoire uniforme calculé avant l'animation (le random ne sert qu'à choisir, l'animation ne fait que le mettre en scène). Carrousel horizontal des avatars avec décélération (cubic-bezier long), repère central doré, révélation finale (couronne + halo + zoom) avec le pseudo en Cinzel. Filtre Citoyens/tous, bouton Rejouer, dernier élu affiché, prefers-reduced-motion affiche le résultat directement. Route protégée par le proxy comme les autres pages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import Cropper, { type Area } from "react-easy-crop";
|
||||
import { cropImageToBlob } from "@/lib/image";
|
||||
import { IconCamera, IconClose } from "@/components/icons";
|
||||
|
||||
export function AvatarPicker({
|
||||
label,
|
||||
hasExistingPhoto = false,
|
||||
onCropped,
|
||||
onClear,
|
||||
}: {
|
||||
label: string;
|
||||
hasExistingPhoto?: boolean;
|
||||
onCropped: (blob: Blob) => void;
|
||||
onClear?: () => void;
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [rawImageSrc, setRawImageSrc] = useState<string | null>(null);
|
||||
const [pendingFileName, setPendingFileName] = useState<string | null>(null);
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
||||
@@ -25,6 +31,7 @@ export function AvatarPicker({
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setRawImageSrc(reader.result as string);
|
||||
setPendingFileName(file.name);
|
||||
setCrop({ x: 0, y: 0 });
|
||||
setZoom(1);
|
||||
setError(null);
|
||||
@@ -37,12 +44,23 @@ export function AvatarPicker({
|
||||
try {
|
||||
const blob = await cropImageToBlob(rawImageSrc, croppedAreaPixels);
|
||||
onCropped(blob);
|
||||
setFileName(pendingFileName);
|
||||
setRawImageSrc(null);
|
||||
setPendingFileName(null);
|
||||
} catch {
|
||||
setError("Impossible de traiter cette image, réessaie.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearSelection() {
|
||||
setFileName(null);
|
||||
onClear?.();
|
||||
}
|
||||
|
||||
function truncate(name: string): string {
|
||||
return name.length > 28 ? `${name.slice(0, 12)}…${name.slice(-10)}` : name;
|
||||
}
|
||||
|
||||
if (rawImageSrc) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
@@ -72,7 +90,10 @@ export function AvatarPicker({
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRawImageSrc(null)}
|
||||
onClick={() => {
|
||||
setRawImageSrc(null);
|
||||
setPendingFileName(null);
|
||||
}}
|
||||
className="rounded-md border border-gold/40 px-3 py-1.5 text-sm text-text-marble hover:bg-gold/10"
|
||||
>
|
||||
Annuler
|
||||
@@ -91,15 +112,41 @@ export function AvatarPicker({
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="text-sm">
|
||||
<span className="sr-only">{label}</span>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="text-sm file:mr-3 file:rounded-md file:border-0 file:bg-ink-2 file:px-3 file:py-1.5 file:text-gold-bright"
|
||||
className="sr-only"
|
||||
aria-label="Choisir un fichier image"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="flex items-center gap-2 rounded-md bg-ink-2 px-3 py-1.5 text-sm font-medium text-gold-bright hover:bg-ink"
|
||||
>
|
||||
<IconCamera className="h-4 w-4" />
|
||||
{fileName || hasExistingPhoto ? "Changer ma photo" : "Choisir une photo"}
|
||||
</button>
|
||||
|
||||
{fileName ? (
|
||||
<div className="flex items-center gap-1.5 text-xs text-text-mut">
|
||||
<span>{truncate(fileName)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearSelection}
|
||||
aria-label="Retirer la photo sélectionnée"
|
||||
className="rounded-full p-0.5 text-text-mut hover:bg-oxblood/10 hover:text-oxblood"
|
||||
>
|
||||
<IconClose className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-text-mut">Aucune photo sélectionnée</span>
|
||||
)}
|
||||
|
||||
{error && <p className="text-xs text-oxblood">{error}</p>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user