Refonte visuelle : direction artistique Grèce antique
Remplace le thème générique (tokens navy/gold/ivory re-thématisables par édition) par une direction artistique fixe et codée en dur : tribunal égéen de nuit — marbre, or, mer sombre, sceaux de cire. Fondations (globals.css, layout.tsx) : - Polices next/font/google : Cinzel (titres, tracking large), Cormorant Garamond (citations italiques), Manrope (interface). - Nouveaux tokens couleur : ink/ink-2 (fond mer de nuit), marble/marble-2 (surfaces claires), gold/gold-bright (accents), oxblood (blâmes/erreurs), olive (honneurs/succès), sea, text-marble/text-mut (texte sur marbre — jamais de doré en texte courant, contraste insuffisant). - Classes utilitaires : .marble-surface (texture par dégradés, pas d'image de bruit), .meander-divider (frise à méandre grecque en SVG data-URI). Composants d'ornement (aucune image bitmap) : - components/laurel-wreath.tsx : couronne de laurier calculée par trigonométrie (deux arcs symétriques de feuilles), réutilisée en logo, favicon (icon.svg régénéré avec les mêmes coordonnées) et couronne du rang 1 du podium. - components/diamond-divider.tsx : séparateur ◆ avec filets dégradés. Header global (components/header.tsx, remplace nav-bar.tsx) : - Logo laurier + "LE TRIBUNAL" à gauche (cliquable vers le leaderboard). - Chip utilisateur à droite : avatar (bordure dorée), pseudo, badge de rôle (Archonte/Citoyen), points de gloire, menu déroulant (Mon profil, Le Conseil des Archontes si juge, Se déconnecter). - Onglets L'Agora / Le Crieur, frise à méandre en séparateur. - Responsive : nom du site et pseudo masqués sous le breakpoint sm, l'avatar reste toujours visible. Vocabulaire diégétique (toujours avec le terme fonctionnel à côté) : juges → Archontes, membres → Citoyens, leaderboard → L'Agora, journal → Le Crieur (décrets avec sceau de cire oxblood), points positifs/négatifs → Honneurs/Blâmes, points → gloires, section admin → Le Conseil des Archontes. Avatar : bordure dorée, couleur de fallback dérivée d'un hash du pseudo (au lieu d'une couleur fixe). Toutes les pages (login, signup, leaderboard/podium, journal, admin, profil) sont passées sur les nouveaux tokens/polices/vocabulaire. README.md et CLAUDE.md : section "Ré-thématisation" remplacée par "Direction artistique" documentant la palette, les polices et les ornements ; toute mention du système de re-thème par édition retirée. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,14 +77,14 @@ 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">
|
||||
<li className="marble-surface flex flex-col gap-2 rounded-lg border border-gold/25 px-4 py-3 shadow-sm sm:flex-row sm:items-center sm:gap-4">
|
||||
<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"
|
||||
className="w-full flex-1 rounded-md border border-gold/30 bg-white/50 px-2 py-1 text-sm text-text-marble outline-none focus:border-gold sm:w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -93,10 +93,10 @@ function MemberRow({ member, isSelf }: { member: Member; isSelf: boolean }) {
|
||||
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"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1 text-sm text-text-marble outline-none focus:border-gold disabled:opacity-50"
|
||||
>
|
||||
<option value="public">public</option>
|
||||
<option value="judge">judge</option>
|
||||
<option value="public">Citoyen</option>
|
||||
<option value="judge">Archonte</option>
|
||||
</select>
|
||||
|
||||
{pseudoLocked ? (
|
||||
@@ -104,26 +104,26 @@ function MemberRow({ member, isSelf }: { member: Member; isSelf: boolean }) {
|
||||
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"
|
||||
className="whitespace-nowrap rounded-md border border-gold/30 px-2 py-1 text-xs text-text-marble hover:bg-gold/10 disabled:opacity-50"
|
||||
>
|
||||
Déverrouiller le pseudo
|
||||
</button>
|
||||
) : (
|
||||
<span className="whitespace-nowrap text-xs text-ink/50">pseudo libre</span>
|
||||
<span className="whitespace-nowrap text-xs text-text-mut">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 sm:ml-auto"
|
||||
className="whitespace-nowrap rounded-md bg-ink-2 px-3 py-1.5 text-sm font-medium text-gold-bright transition hover:bg-ink disabled:opacity-40 sm:ml-auto"
|
||||
>
|
||||
{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>}
|
||||
{error && <p className="text-xs text-oxblood">{error}</p>}
|
||||
{success && !dirty && <p className="text-xs text-olive">OK</p>}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export function AdminMembersTable({
|
||||
currentUserId: string;
|
||||
}) {
|
||||
if (members.length === 0) {
|
||||
return <p className="text-sm text-ink/70">Aucun membre inscrit.</p>;
|
||||
return <p className="text-sm text-marble/60">Aucun Citoyen inscrit.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,12 @@ 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">Admin</h1>
|
||||
<div>
|
||||
<h1 className="font-heading text-2xl tracking-[0.1em] text-gold-bright uppercase">
|
||||
Le Conseil des Archontes
|
||||
</h1>
|
||||
<p className="font-serif text-sm text-marble/60 italic">Administration des Citoyens</p>
|
||||
</div>
|
||||
<ResetRoundButton />
|
||||
</div>
|
||||
<AdminMembersTable members={members ?? []} currentUserId={user.id} />
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ResetRoundButton() {
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={state === "loading"}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-1.5 text-sm font-medium text-navy shadow-sm transition hover:bg-navy/5 disabled:opacity-50"
|
||||
className="marble-surface rounded-md border border-gold/40 px-3 py-1.5 text-sm font-medium text-text-marble shadow-sm transition hover:bg-gold/10 disabled:opacity-50"
|
||||
>
|
||||
{state === "loading" ? "…" : state === "done" ? "Repère mis à jour ✓" : "Nouveau round (réinitialiser le repère)"}
|
||||
</button>
|
||||
|
||||
+54
-15
@@ -1,40 +1,79 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--color-navy: #0f2748;
|
||||
--color-gold: #c9a227;
|
||||
--color-ivory: #f4ecd8;
|
||||
--color-ink: #2a2116;
|
||||
--ink: #0a1b33;
|
||||
--ink-2: #0f2748;
|
||||
--marble: #f4ecd8;
|
||||
--marble-2: #e9dec2;
|
||||
--gold: #c9a227;
|
||||
--gold-bright: #e7c560;
|
||||
--oxblood: #a5342a;
|
||||
--sea: #2e6e7e;
|
||||
--olive: #5e6b3b;
|
||||
--text-marble: #2a2116;
|
||||
--text-mut: #7a6a4c;
|
||||
|
||||
--background: var(--color-ivory);
|
||||
--foreground: var(--color-ink);
|
||||
--background: var(--ink);
|
||||
--foreground: var(--marble);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-navy: var(--color-navy);
|
||||
--color-gold: var(--color-gold);
|
||||
--color-ivory: var(--color-ivory);
|
||||
--color-ink: var(--color-ink);
|
||||
--color-ink: var(--ink);
|
||||
--color-ink-2: var(--ink-2);
|
||||
--color-marble: var(--marble);
|
||||
--color-marble-2: var(--marble-2);
|
||||
--color-gold: var(--gold);
|
||||
--color-gold-bright: var(--gold-bright);
|
||||
--color-oxblood: var(--oxblood);
|
||||
--color-sea: var(--sea);
|
||||
--color-olive: var(--olive);
|
||||
--color-text-marble: var(--text-marble);
|
||||
--color-text-mut: var(--text-mut);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
|
||||
--font-heading: var(--font-cinzel);
|
||||
--font-serif: var(--font-cormorant);
|
||||
--font-sans: var(--font-manrope);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 20% -10%, color-mix(in srgb, var(--ink-2) 75%, transparent) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 70% 60% at 100% 110%, color-mix(in srgb, var(--sea) 30%, transparent) 0%, transparent 65%),
|
||||
var(--ink);
|
||||
background-attachment: fixed;
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-gold);
|
||||
outline: 2px solid var(--gold-bright);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Carte "marbre" : surface claire avec léger grain, bordure dorée fine. */
|
||||
.marble-surface {
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, color-mix(in srgb, var(--marble-2) 65%, transparent) 0%, transparent 42%),
|
||||
radial-gradient(circle at 82% 88%, color-mix(in srgb, var(--marble-2) 55%, transparent) 0%, transparent 48%),
|
||||
linear-gradient(150deg, var(--marble) 0%, var(--marble) 55%, var(--marble-2) 100%);
|
||||
color: var(--text-marble);
|
||||
}
|
||||
|
||||
/* Frise à méandre (motif grec), séparateur horizontal discret. */
|
||||
.meander-divider {
|
||||
height: 10px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='10' viewBox='0 0 32 10'%3E%3Cpath d='M0 8 H6 V2 H16 V8 H22 V2 H32' fill='none' stroke='%23C9A227' stroke-width='1.3'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat-x;
|
||||
background-position: center;
|
||||
background-size: 32px 10px;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@keyframes points-flash {
|
||||
0% {
|
||||
background-color: color-mix(in srgb, var(--color-gold) 55%, transparent);
|
||||
background-color: color-mix(in srgb, var(--gold-bright) 55%, transparent);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
|
||||
+17
-7
@@ -1,8 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
|
||||
<rect x="58" y="74" width="30" height="12" rx="3" fill="#0F2748" />
|
||||
<rect x="54" y="70" width="38" height="6" rx="3" fill="#0F2748" opacity="0.7" />
|
||||
<g transform="rotate(45 50 46)">
|
||||
<rect x="30" y="10" width="18" height="46" rx="6" fill="#C9A227" />
|
||||
<rect x="18" y="8" width="42" height="20" rx="6" fill="#0F2748" />
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#C9A227">
|
||||
<rect x="0" y="0" width="100" height="100" rx="20" fill="#0A1B33" />
|
||||
<ellipse cx="44.18" cy="85.39" rx="7.00" ry="2.80" transform="rotate(-168.00 44.18 85.39)"/>
|
||||
<ellipse cx="31.70" cy="81.14" rx="6.71" ry="2.68" transform="rotate(-141.67 31.70 81.14)"/>
|
||||
<ellipse cx="21.98" cy="71.26" rx="6.42" ry="2.57" transform="rotate(-115.33 21.98 71.26)"/>
|
||||
<ellipse cx="17.50" cy="57.43" rx="6.13" ry="2.45" transform="rotate(-89.00 17.50 57.43)"/>
|
||||
<ellipse cx="19.80" cy="42.39" rx="5.83" ry="2.33" transform="rotate(-62.67 19.80 42.39)"/>
|
||||
<ellipse cx="28.97" cy="29.40" rx="5.54" ry="2.22" transform="rotate(-36.33 28.97 29.40)"/>
|
||||
<ellipse cx="43.58" cy="21.56" rx="5.25" ry="2.10" transform="rotate(-10.00 43.58 21.56)"/>
|
||||
<ellipse cx="55.82" cy="85.39" rx="7.00" ry="2.80" transform="rotate(-168.00 55.82 85.39)"/>
|
||||
<ellipse cx="68.30" cy="81.14" rx="6.71" ry="2.68" transform="rotate(-141.67 68.30 81.14)"/>
|
||||
<ellipse cx="78.02" cy="71.26" rx="6.42" ry="2.57" transform="rotate(-115.33 78.02 71.26)"/>
|
||||
<ellipse cx="82.50" cy="57.43" rx="6.13" ry="2.45" transform="rotate(-89.00 82.50 57.43)"/>
|
||||
<ellipse cx="80.20" cy="42.39" rx="5.83" ry="2.33" transform="rotate(-62.67 80.20 42.39)"/>
|
||||
<ellipse cx="71.03" cy="29.40" rx="5.54" ry="2.22" transform="rotate(-36.33 71.03 29.40)"/>
|
||||
<ellipse cx="56.42" cy="21.56" rx="5.25" ry="2.10" transform="rotate(-10.00 56.42 21.56)"/>
|
||||
<circle cx="50" cy="85" r="2.4"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 420 B After Width: | Height: | Size: 1.5 KiB |
@@ -49,27 +49,33 @@ export function JournalView({
|
||||
}, []);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return <p className="text-sm text-ink/70">Aucun événement pour l'instant.</p>;
|
||||
return <p className="text-sm text-marble/60">Aucun décret pour l'instant.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ol className="flex flex-col gap-2">
|
||||
{entries.map((entry) => {
|
||||
const judgePseudo = pseudoById[entry.judge_id] ?? "Un juge";
|
||||
const targetPseudo = pseudoById[entry.target_id] ?? "un membre";
|
||||
const judgePseudo = pseudoById[entry.judge_id] ?? "Un Archonte";
|
||||
const targetPseudo = pseudoById[entry.target_id] ?? "un Citoyen";
|
||||
const sign = entry.delta > 0 ? `+${entry.delta}` : `${entry.delta}`;
|
||||
return (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="rounded-lg border border-navy/10 bg-white px-4 py-3 text-sm shadow-sm"
|
||||
className="marble-surface flex gap-3 rounded-lg border border-gold/25 px-4 py-3 text-sm shadow-sm"
|
||||
>
|
||||
<span className="font-medium text-navy">{judgePseudo}</span> a donné{" "}
|
||||
<span className={entry.delta >= 0 ? "font-semibold text-green-700" : "font-semibold text-red-700"}>
|
||||
{sign}
|
||||
</span>{" "}
|
||||
à <span className="font-medium text-navy">{targetPseudo}</span>
|
||||
{entry.reason && <span className="text-ink/70"> — {entry.reason}</span>}
|
||||
<div className="mt-1 text-xs text-ink/50">{formatTime(entry.created_at)}</div>
|
||||
<span
|
||||
className="mt-1 h-3 w-3 shrink-0 rounded-full bg-oxblood shadow-[0_0_0_2px_rgba(165,52,42,0.25)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-text-marble">{judgePseudo}</span> a proclamé{" "}
|
||||
<span className={entry.delta >= 0 ? "font-semibold text-olive" : "font-semibold text-oxblood"}>
|
||||
{sign}
|
||||
</span>{" "}
|
||||
à <span className="font-medium text-text-marble">{targetPseudo}</span>
|
||||
{entry.reason && <span className="text-text-mut"> — {entry.reason}</span>}
|
||||
<div className="mt-1 text-xs text-text-mut">{formatTime(entry.created_at)}</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -23,8 +23,14 @@ export default async function JournalPage() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-2xl px-4 py-8">
|
||||
<h1 className="mb-2 text-2xl font-semibold text-navy">Le crieur</h1>
|
||||
<p className="mb-6 text-sm text-ink/60">Journal public de toutes les attributions de points.</p>
|
||||
<div className="mb-6 text-center">
|
||||
<h1 className="font-heading text-2xl tracking-[0.1em] text-gold-bright uppercase">
|
||||
Le Crieur
|
||||
</h1>
|
||||
<p className="font-serif text-sm text-marble/60 italic">
|
||||
Le fil des décrets — chaque honneur, chaque blâme, proclamé publiquement.
|
||||
</p>
|
||||
</div>
|
||||
<JournalView initialEntries={entries ?? []} pseudoById={pseudoById} />
|
||||
</div>
|
||||
);
|
||||
|
||||
+30
-14
@@ -1,22 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Cinzel, Cormorant_Garamond, Manrope } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import { NavBar } from "@/components/nav-bar";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const cinzel = Cinzel({
|
||||
variable: "--font-cinzel",
|
||||
subsets: ["latin"],
|
||||
weight: ["500", "600", "700"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const cormorant = Cormorant_Garamond({
|
||||
variable: "--font-cormorant",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600"],
|
||||
style: ["italic", "normal"],
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
variable: "--font-manrope",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Le Tribunal",
|
||||
description: "Comptes et leaderboard pour le jeu du Tribunal",
|
||||
description: "Comptes, rôles et classement pour le jeu du Tribunal",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
@@ -29,23 +37,31 @@ export default async function RootLayout({
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
let isJudge = false;
|
||||
let profile: { pseudo: string; avatar_url: string | null; role: string; points: number } | null =
|
||||
null;
|
||||
if (user) {
|
||||
const { data: profile } = await supabase
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("role")
|
||||
.select("pseudo, avatar_url, role, points")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
isJudge = profile?.role === "judge";
|
||||
profile = data;
|
||||
}
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="fr"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
className={`${cinzel.variable} ${cormorant.variable} ${manrope.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col bg-ivory text-ink">
|
||||
{user && <NavBar isJudge={isJudge} />}
|
||||
<body className="min-h-full flex flex-col">
|
||||
{user && profile && (
|
||||
<Header
|
||||
pseudo={profile.pseudo}
|
||||
avatarUrl={profile.avatar_url}
|
||||
isJudge={profile.role === "judge"}
|
||||
points={profile.points}
|
||||
/>
|
||||
)}
|
||||
<main className="flex flex-1 flex-col">{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,13 +17,13 @@ type Profile = {
|
||||
|
||||
function ProgressBadge({ direction, amount }: { direction: string; amount: number }) {
|
||||
if (direction === "up") {
|
||||
return <span className="text-xs font-semibold text-green-700">▲{amount}</span>;
|
||||
return <span className="text-xs font-semibold text-olive">▲{amount}</span>;
|
||||
}
|
||||
if (direction === "down") {
|
||||
return <span className="text-xs font-semibold text-red-700">▼{amount}</span>;
|
||||
return <span className="text-xs font-semibold text-oxblood">▼{amount}</span>;
|
||||
}
|
||||
if (direction === "same") {
|
||||
return <span className="text-xs text-ink/40">=</span>;
|
||||
return <span className="text-xs text-text-mut">=</span>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export function LeaderboardView({
|
||||
const rest = ranked.filter((p) => p.rank > 3);
|
||||
|
||||
if (profiles.length === 0) {
|
||||
return <p className="text-sm text-ink/70">Personne n'est encore inscrit.</p>;
|
||||
return <p className="text-sm text-marble/60">Personne n'est encore inscrit.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -99,21 +99,23 @@ export function LeaderboardView({
|
||||
return (
|
||||
<li
|
||||
key={profile.id}
|
||||
className={`flex flex-col gap-2 rounded-lg border border-navy/10 bg-white px-4 py-3 shadow-sm transition-colors sm:flex-row sm:items-center sm:gap-3 ${
|
||||
className={`marble-surface flex flex-col gap-2 rounded-lg border border-gold/25 px-4 py-3 shadow-sm transition-colors sm:flex-row sm:items-center sm:gap-3 ${
|
||||
flashingId === profile.id ? "animate-points-flash" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 shrink-0 text-center font-semibold text-navy/60">
|
||||
<span className="w-6 shrink-0 text-center font-heading text-text-mut">
|
||||
{profile.rank}
|
||||
</span>
|
||||
<Avatar pseudo={profile.pseudo} avatarUrl={profile.avatar_url} size="sm" />
|
||||
<span className="flex-1 truncate font-medium sm:flex-initial">{profile.pseudo}</span>
|
||||
<ProgressBadge direction={progress.direction} amount={progress.amount} />
|
||||
<span className="font-semibold text-navy sm:hidden">{profile.points}</span>
|
||||
<span className="font-semibold text-text-marble sm:hidden">{profile.points}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<span className="hidden font-semibold text-navy sm:inline">{profile.points}</span>
|
||||
<span className="hidden font-semibold text-text-marble sm:inline">
|
||||
{profile.points}
|
||||
</span>
|
||||
{isJudge && (
|
||||
<JudgePointControls memberId={profile.id} onApplyDelta={applyOptimisticDelta} />
|
||||
)}
|
||||
|
||||
@@ -21,11 +21,16 @@ export default async function LeaderboardPage() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-2xl px-4 py-8">
|
||||
<h1 className="mb-6 text-2xl font-semibold text-navy">Leaderboard</h1>
|
||||
<div className="mb-6 text-center">
|
||||
<h1 className="font-heading text-2xl tracking-[0.1em] text-gold-bright uppercase">
|
||||
L'Agora
|
||||
</h1>
|
||||
<p className="font-serif text-sm text-marble/60 italic">Le classement des Citoyens</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-sm text-red-700">
|
||||
Impossible de charger le leaderboard pour le moment.
|
||||
<p role="alert" className="text-sm text-oxblood">
|
||||
Impossible de charger le classement pour le moment.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
+19
-12
@@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { LaurelWreath } from "@/components/laurel-wreath";
|
||||
|
||||
function mapSignInError(message: string): string {
|
||||
if (message.toLowerCase().includes("email not confirmed")) {
|
||||
@@ -40,14 +41,20 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-sm rounded-xl border border-navy/10 bg-white p-6 shadow-sm sm:p-8">
|
||||
<h1 className="mb-6 text-center text-2xl font-semibold text-navy">
|
||||
Connexion
|
||||
</h1>
|
||||
<div className="marble-surface w-full max-w-sm rounded-2xl border border-gold/40 p-6 shadow-xl sm:p-8">
|
||||
<div className="mb-4 flex flex-col items-center gap-2">
|
||||
<LaurelWreath className="h-10 w-10" color="#A5342A" />
|
||||
<h1 className="font-heading text-xl tracking-[0.1em] text-text-marble uppercase">
|
||||
Connexion
|
||||
</h1>
|
||||
<p className="font-serif text-sm text-text-mut italic">
|
||||
« La balance ne ment jamais aux Citoyens vertueux. »
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
<label htmlFor="email" className="text-sm font-medium text-text-marble">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
@@ -57,12 +64,12 @@ export default function LoginPage() {
|
||||
required
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-2 text-ink outline-none focus:border-gold"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-3 py-2 text-text-marble outline-none focus:border-gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="password" className="text-sm font-medium">
|
||||
<label htmlFor="password" className="text-sm font-medium text-text-marble">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
@@ -72,12 +79,12 @@ export default function LoginPage() {
|
||||
required
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-2 text-ink outline-none focus:border-gold"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-3 py-2 text-text-marble outline-none focus:border-gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-sm text-red-700">
|
||||
<p role="alert" className="text-sm text-oxblood">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
@@ -85,15 +92,15 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="mt-2 rounded-md bg-navy px-4 py-2 font-medium text-ivory transition hover:bg-navy/90 disabled:opacity-50"
|
||||
className="mt-2 rounded-md bg-ink-2 px-4 py-2 font-heading text-sm tracking-wide text-gold-bright uppercase transition hover:bg-ink disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Connexion…" : "Se connecter"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-center text-sm">
|
||||
<p className="mt-6 text-center text-sm text-text-mut">
|
||||
Pas encore de compte ?{" "}
|
||||
<Link href="/signup" className="font-medium text-navy underline hover:text-gold">
|
||||
<Link href="/signup" className="font-medium text-oxblood underline hover:text-gold">
|
||||
Créer un compte
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -18,14 +18,27 @@ export default async function ProfilePage() {
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
const isJudge = profile?.role === "judge";
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-sm px-4 py-8">
|
||||
<h1 className="mb-6 text-2xl font-semibold text-navy">Mon profil</h1>
|
||||
<div className="rounded-xl border border-navy/10 bg-white p-6 shadow-sm sm:p-8">
|
||||
<p className="mb-4 text-sm text-ink/60">{user.email}</p>
|
||||
<div className="mb-6 text-center">
|
||||
<h1 className="font-heading text-2xl tracking-[0.1em] text-gold-bright uppercase">
|
||||
Mon profil
|
||||
</h1>
|
||||
<span
|
||||
className={`mt-1 inline-block font-heading text-xs tracking-wide uppercase ${
|
||||
isJudge ? "text-gold-bright" : "text-marble/50"
|
||||
}`}
|
||||
>
|
||||
{isJudge ? "⚜ Archonte" : "Citoyen"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="marble-surface rounded-2xl border border-gold/40 p-6 shadow-xl sm:p-8">
|
||||
<p className="mb-4 text-sm text-text-mut">{user.email}</p>
|
||||
<ProfileForm
|
||||
userId={user.id}
|
||||
isJudge={profile?.role === "judge"}
|
||||
isJudge={isJudge}
|
||||
initialPseudo={profile?.pseudo ?? ""}
|
||||
initialAvatarUrl={profile?.avatar_url ?? null}
|
||||
initialPseudoLocked={profile?.pseudo_locked ?? false}
|
||||
|
||||
@@ -11,7 +11,7 @@ function mapError(message: string): string {
|
||||
return "Ce pseudo est déjà pris.";
|
||||
}
|
||||
if (message.includes("pseudo is locked")) {
|
||||
return "Ton pseudo est déjà figé.";
|
||||
return "Ton pseudo est déjà figé, contacte un Archonte.";
|
||||
}
|
||||
return "Une erreur est survenue, réessaie.";
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export function ProfileForm({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="pseudo" className="text-sm font-medium">
|
||||
<label htmlFor="pseudo" className="text-sm font-medium text-text-marble">
|
||||
Pseudo
|
||||
</label>
|
||||
<input
|
||||
@@ -119,26 +119,26 @@ export function ProfileForm({
|
||||
disabled={!canEditPseudo}
|
||||
value={pseudo}
|
||||
onChange={(event) => setPseudo(event.target.value)}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-2 text-ink outline-none focus:border-gold disabled:bg-navy/5 disabled:text-ink/50"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-3 py-2 text-text-marble outline-none focus:border-gold disabled:bg-gold/5 disabled:text-text-mut"
|
||||
/>
|
||||
{!canEditPseudo && (
|
||||
<p className="text-xs text-ink/60">
|
||||
Pseudo figé — contacte un juge pour le changer.
|
||||
<p className="text-xs text-text-mut">
|
||||
Pseudo figé — contacte un Archonte pour le changer.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-sm text-red-700">
|
||||
<p role="alert" className="text-sm text-oxblood">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{success && <p className="text-sm text-green-700">Profil mis à jour.</p>}
|
||||
{success && <p className="text-sm text-olive">Profil mis à jour.</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="mt-2 rounded-md bg-navy px-4 py-2 font-medium text-ivory transition hover:bg-navy/90 disabled:opacity-50"
|
||||
className="mt-2 rounded-md bg-ink-2 px-4 py-2 font-heading text-sm tracking-wide text-gold-bright uppercase transition hover:bg-ink disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Enregistrement…" : "Enregistrer"}
|
||||
</button>
|
||||
|
||||
+31
-19
@@ -5,6 +5,7 @@ import { useState, type FormEvent } from "react";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { Avatar } from "@/components/avatar";
|
||||
import { AvatarPicker } from "@/components/avatar-picker";
|
||||
import { LaurelWreath } from "@/components/laurel-wreath";
|
||||
|
||||
function mapSignUpError(message: string): string {
|
||||
const lower = message.toLowerCase();
|
||||
@@ -102,16 +103,19 @@ export default function SignupPage() {
|
||||
if (confirmationSent) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-sm rounded-xl border border-navy/10 bg-white p-6 text-center shadow-sm sm:p-8">
|
||||
<h1 className="mb-4 text-2xl font-semibold text-navy">Compte créé !</h1>
|
||||
<p className="text-sm text-ink/80">
|
||||
<div className="marble-surface w-full max-w-sm rounded-2xl border border-gold/40 p-6 text-center shadow-xl sm:p-8">
|
||||
<LaurelWreath className="mx-auto mb-3 h-10 w-10" color="#5E6B3B" />
|
||||
<h1 className="mb-4 font-heading text-xl tracking-[0.1em] text-text-marble uppercase">
|
||||
Compte créé !
|
||||
</h1>
|
||||
<p className="text-sm text-text-marble/80">
|
||||
Vérifie ta boîte mail (et les spams) pour confirmer ton adresse, puis
|
||||
connecte-toi. Tu pourras ajouter ta photo de profil ensuite depuis ton
|
||||
profil.
|
||||
</p>
|
||||
<Link
|
||||
href="/login"
|
||||
className="mt-6 inline-block font-medium text-navy underline hover:text-gold"
|
||||
className="mt-6 inline-block font-medium text-oxblood underline hover:text-gold"
|
||||
>
|
||||
Aller à la connexion
|
||||
</Link>
|
||||
@@ -122,14 +126,20 @@ export default function SignupPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-sm rounded-xl border border-navy/10 bg-white p-6 shadow-sm sm:p-8">
|
||||
<h1 className="mb-6 text-center text-2xl font-semibold text-navy">
|
||||
Créer un compte
|
||||
</h1>
|
||||
<div className="marble-surface w-full max-w-sm rounded-2xl border border-gold/40 p-6 shadow-xl sm:p-8">
|
||||
<div className="mb-4 flex flex-col items-center gap-2">
|
||||
<LaurelWreath className="h-10 w-10" color="#A5342A" />
|
||||
<h1 className="font-heading text-xl tracking-[0.1em] text-text-marble uppercase">
|
||||
Créer un compte
|
||||
</h1>
|
||||
<p className="font-serif text-sm text-text-mut italic">
|
||||
« Que ton nom soit gravé parmi les Citoyens. »
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
<label htmlFor="email" className="text-sm font-medium text-text-marble">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
@@ -139,12 +149,12 @@ export default function SignupPage() {
|
||||
required
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-2 text-ink outline-none focus:border-gold"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-3 py-2 text-text-marble outline-none focus:border-gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="pseudo" className="text-sm font-medium">
|
||||
<label htmlFor="pseudo" className="text-sm font-medium text-text-marble">
|
||||
Pseudo
|
||||
</label>
|
||||
<input
|
||||
@@ -154,12 +164,12 @@ export default function SignupPage() {
|
||||
required
|
||||
value={pseudo}
|
||||
onChange={(event) => setPseudo(event.target.value)}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-2 text-ink outline-none focus:border-gold"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-3 py-2 text-text-marble outline-none focus:border-gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="password" className="text-sm font-medium">
|
||||
<label htmlFor="password" className="text-sm font-medium text-text-marble">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
@@ -170,18 +180,20 @@ export default function SignupPage() {
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
className="rounded-md border border-navy/20 bg-white px-3 py-2 text-ink outline-none focus:border-gold"
|
||||
className="rounded-md border border-gold/30 bg-white/50 px-3 py-2 text-text-marble outline-none focus:border-gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<span className="self-start text-sm font-medium">Photo de profil (optionnel)</span>
|
||||
<span className="self-start text-sm font-medium text-text-marble">
|
||||
Photo de profil (optionnel)
|
||||
</span>
|
||||
<Avatar pseudo={pseudo || "?"} avatarUrl={avatarPreview} size="lg" />
|
||||
<AvatarPicker label="Photo de profil" onCropped={handleCropped} />
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-sm text-red-700">
|
||||
<p role="alert" className="text-sm text-oxblood">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
@@ -189,15 +201,15 @@ export default function SignupPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="mt-2 rounded-md bg-navy px-4 py-2 font-medium text-ivory transition hover:bg-navy/90 disabled:opacity-50"
|
||||
className="mt-2 rounded-md bg-ink-2 px-4 py-2 font-heading text-sm tracking-wide text-gold-bright uppercase transition hover:bg-ink disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Création…" : "Créer mon compte"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-center text-sm">
|
||||
<p className="mt-6 text-center text-sm text-text-mut">
|
||||
Déjà un compte ?{" "}
|
||||
<Link href="/login" className="font-medium text-navy underline hover:text-gold">
|
||||
<Link href="/login" className="font-medium text-oxblood underline hover:text-gold">
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user