import { useState, useMemo, useCallback } from "react"; const VOTERS_DB = [ { id: 1, username: "alice", password: "alice123", name: "Alice Martin" }, { id: 2, username: "bob", password: "bob456", name: "Bob Dupont" }, { id: 3, username: "claire", password: "claire789", name: "Claire Rousseau" }, { id: 4, username: "david", password: "david123", name: "David Leroy" }, { id: 5, username: "emma", password: "emma456", name: "Emma Beaumont" }, { id: 6, username: "felix", password: "felix789", name: "Félix Moreau" }, { id: 7, username: "grace", password: "grace123", name: "Grace Petit" }, { id: 8, username: "hugo", password: "hugo456", name: "Hugo Fontaine" }, { id: 9, username: "iris", password: "iris789", name: "Iris Lambert" }, { id: 10, username: "jules", password: "jules123", name: "Jules Simon" }, { id: 11, username: "lea", password: "lea456", name: "Léa Girard" }, { id: 12, username: "marc", password: "marc789", name: "Marc Nguyen" }, ]; const CANDIDATES = [ { id: 1, name: "Sophie Beaumont", role: "Coordinatrice", initials: "SB" }, { id: 2, name: "Lucas Marchand", role: "Animateur", initials: "LM" }, { id: 3, name: "Chloé Girard", role: "Décoratrice", initials: "CG" }, { id: 4, name: "Antoine Roux", role: "DJ de la soirée", initials: "AR" }, { id: 5, name: "Manon Lefebvre", role: "Chef cuisinier", initials: "ML" }, { id: 6, name: "Raphaël Nguyen", role: "Photographe", initials: "RN" }, ]; const ADMIN_CREDS = { username: "admin", password: "admin2024" }; const GOLD = "#c9a84c"; const GOLD_DIM = "rgba(201,168,76,0.25)"; const GOLD_GLOW = "rgba(201,168,76,0.08)"; const TEXT = "#f0e6d0"; const MUTED = "rgba(240,230,208,0.45)"; function StarField() { const stars = useMemo(() => Array.from({ length: 60 }, (_, i) => ({ id: i, x: ((i * 137.5) % 100), y: ((i * 97.3) % 100), size: i % 5 === 0 ? 2 : 1, opacity: 0.08 + (i % 7) * 0.04, })), []); return (
{stars.map(s => (
))}
); } function Ornament({ style = {} }) { return (
); } const screen = { minHeight: "100vh", background: "radial-gradient(ellipse at 50% -20%, rgba(201,168,76,0.12) 0%, transparent 55%), #07071a", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "24px", position: "relative", overflow: "hidden", fontFamily: "'Segoe UI', system-ui, sans-serif", color: TEXT, }; const card = (wide = false) => ({ background: "rgba(255,255,255,0.035)", border: `1px solid ${GOLD_DIM}`, borderRadius: 20, padding: "40px 36px", width: "100%", maxWidth: wide ? 580 : 420, position: "relative", zIndex: 1, boxSizing: "border-box", }); const heading = { fontFamily: "Georgia, 'Times New Roman', serif", fontSize: 30, fontWeight: 300, color: GOLD, letterSpacing: "0.08em", textAlign: "center", margin: "0 0 4px", }; const sub = { fontSize: 10, letterSpacing: "0.35em", color: MUTED, textAlign: "center", textTransform: "uppercase", margin: "0 0 28px", }; const inp = { width: "100%", background: "rgba(255,255,255,0.04)", border: `1px solid ${GOLD_DIM}`, borderRadius: 10, padding: "13px 16px", color: TEXT, fontSize: 14, outline: "none", boxSizing: "border-box", marginBottom: 10, fontFamily: "inherit", }; const btn = (variant = "gold") => ({ width: "100%", border: "none", borderRadius: 10, padding: "13px", fontSize: 11, fontWeight: 600, letterSpacing: "0.22em", textTransform: "uppercase", cursor: "pointer", fontFamily: "inherit", marginTop: 6, ...(variant === "gold" ? { background: `linear-gradient(135deg, ${GOLD}, #a8822e)`, color: "#1a1208", } : { background: "transparent", border: `1px solid ${GOLD_DIM}`, color: MUTED, }), }); const backBtn = { background: "none", border: "none", color: MUTED, cursor: "pointer", fontSize: 11, letterSpacing: "0.15em", padding: 0, marginBottom: 24, fontFamily: "inherit", textTransform: "uppercase", }; export default function App() { const [view, setView] = useState("home"); const [user, setUser] = useState(null); const [votes, setVotes] = useState({}); const [voted, setVoted] = useState(new Set()); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showPwd, setShowPwd] = useState(false); const [err, setErr] = useState(""); const [pick, setPick] = useState(null); const login = useCallback(() => { setErr(""); if (username === ADMIN_CREDS.username && password === ADMIN_CREDS.password) { setUser({ isAdmin: true, name: "Administrateur" }); setView("results"); setUsername(""); setPassword(""); return; } const found = VOTERS_DB.find(v => v.username === username.trim().toLowerCase() && v.password === password); if (!found) { setErr("Identifiants incorrects. Réessayez."); return; } setUser(found); setUsername(""); setPassword(""); setView(voted.has(found.id) ? "done" : "vote"); }, [username, password, voted]); const submitVote = useCallback(() => { if (!pick || !user) return; setVotes(p => ({ ...p, [user.id]: pick })); setVoted(p => new Set([...p, user.id])); setPick(null); setView("done"); }, [pick, user]); const results = useMemo(() => { const c = {}; CANDIDATES.forEach(x => { c[x.id] = 0; }); Object.values(votes).forEach(id => { c[id]++; }); return [...CANDIDATES].map(x => ({ ...x, count: c[x.id] })).sort((a, b) => b.count - a.count); }, [votes]); const totalVotes = Object.keys(votes).length; // ── HOME ── if (view === "home") return (

Soirée de Gala

Élection de la soirée

QR Code voter { e.target.style.display = "none"; }} />

Scannez le QR code pour accéder au vote

); // ── LOGIN ── if (view === "login") return (

Connexion

Entrez vos identifiants

setUsername(e.target.value)} onKeyDown={e => e.key === "Enter" && login()} />
setPassword(e.target.value)} onKeyDown={e => e.key === "Enter" && login()} />
{err && (
{err}
)}

Comptes de démonstration

{VOTERS_DB.slice(0, 3).map(v => (
{v.username} · {v.password}
))}
Admin : admin · admin2024
); // ── VOTE ── if (view === "vote") return (

Votre vote

Choisissez votre candidat

Bonjour {user?.name}. Sélectionnez la personne que vous souhaitez élire ce soir.

{CANDIDATES.map(c => { const sel = pick === c.id; return (
setPick(c.id)} style={{ background: sel ? "rgba(201,168,76,0.12)" : "rgba(255,255,255,0.025)", border: `1px solid ${sel ? GOLD : GOLD_DIM}`, borderRadius: 14, padding: "18px 12px", cursor: "pointer", textAlign: "center", transition: "all 0.15s", position: "relative", }}> {sel && (
)}
{c.initials}
{c.name}
{c.role}
); })}
); // ── DONE ── if (view === "done") { const votedFor = user && votes[user.id] ? CANDIDATES.find(c => c.id === votes[user.id]) : null; return (

Vote enregistré

Merci pour votre participation

{votedFor && (

Vous avez voté pour

{votedFor.name}

{votedFor.role}

)}

Les résultats seront annoncés ce soir.
Bonne soirée à vous ! ✦

); } // ── RESULTS ── if (view === "results") { const maxVotes = results[0]?.count || 0; return (

Résultats en direct

Tableau des votes — Accès administrateur

{[ { label: "Votes exprimés", val: totalVotes }, { label: "Votants inscrits", val: VOTERS_DB.length }, { label: "Participation", val: `${Math.round((totalVotes / VOTERS_DB.length) * 100)}%` }, ].map(s => (
{s.val}
{s.label}
))}
{results.map((c, i) => { const elected = i < 2; const pct = maxVotes > 0 ? Math.round((c.count / maxVotes) * 100) : 0; return (
{c.initials}
{c.name} {elected && ( {i === 0 ? "★ 1ER" : "★ 2ÈME"} )}
{c.role}
{c.count}
{pct}%
); })}

Les 2 candidats en tête seront élus · Mis à jour en temps réel

); } return null; }