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 (
);
}
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

{ 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 => (
))}
{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}
);
})}
Les 2 candidats en tête seront élus · Mis à jour en temps réel
);
}
return null;
}