"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; type User = { id: string; name: string; email: string; banned: boolean; createdAt: string; _count: { games: number }; }; type Game = { id: string; mode: string; startArticle: string; targetArticle: string; clicks: number; timeSeconds: number; won: boolean; playedAt: string; user: { name: string }; }; type ModeStat = { mode: string; _count: { id: number }; _avg: { clicks: number | null; timeSeconds: number | null }; }; type ActivityDay = { date: string; count: number }; type AdminStats = { totalUsers: number; totalGames: number; todayGames: number; recentUsers: User[]; recentGames: Game[]; modeStats: ModeStat[]; activity: ActivityDay[]; }; type DailyPuzzle = { id: string; date: string; startArticle: string; targetArticle: string; _count: { results: number }; }; function fmt(s: number) { const m = Math.floor(s / 60); const sec = Math.round(s % 60); return m > 0 ? `${m}m${sec.toString().padStart(2, "0")}s` : `${sec}s`; } function MiniBarChart({ data }: { data: ActivityDay[] }) { const max = Math.max(...data.map((d) => d.count), 1); return (
{data.map((d) => { const pct = (d.count / max) * 100; const day = new Date(d.date + "T12:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "short" }); return (
{day} — {d.count}
); })}
); } export default function AdminPage() { const [data, setData] = useState(null); const [dailyPuzzles, setDailyPuzzles] = useState(null); const [loading, setLoading] = useState(true); const [tab, setTab] = useState<"overview" | "users" | "games" | "daily">("overview"); // Daily puzzle form const [puzzleForm, setPuzzleForm] = useState({ date: "", startArticle: "", targetArticle: "" }); const [editingPuzzle, setEditingPuzzle] = useState(null); const [puzzleSaving, setPuzzleSaving] = useState(false); // Confirm delete const [confirmDelete, setConfirmDelete] = useState<{ type: "user" | "puzzle"; id: string; label: string } | null>(null); function refreshStats() { return fetch("/api/admin/stats") .then((r) => { if (!r.ok) throw new Error(); return r.json(); }) .then(setData); } function refreshDaily() { return fetch("/api/admin/daily") .then((r) => r.json()) .then(setDailyPuzzles); } useEffect(() => { Promise.all([refreshStats(), refreshDaily()]) .catch(() => setData(null)) .finally(() => setLoading(false)); }, []); async function handleBan(user: User) { const res = await fetch(`/api/admin/users/${user.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ banned: !user.banned }), }); if (res.ok) await refreshStats(); } async function handleDeleteUser(id: string) { await fetch(`/api/admin/users/${id}`, { method: "DELETE" }); await refreshStats(); setConfirmDelete(null); } async function handleDeletePuzzle(id: string) { await fetch(`/api/admin/daily/${id}`, { method: "DELETE" }); await refreshDaily(); setConfirmDelete(null); } async function handleSavePuzzle() { setPuzzleSaving(true); try { if (editingPuzzle) { await fetch(`/api/admin/daily/${editingPuzzle.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ startArticle: puzzleForm.startArticle, targetArticle: puzzleForm.targetArticle }), }); } else { await fetch("/api/admin/daily", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(puzzleForm), }); } await refreshDaily(); setPuzzleForm({ date: "", startArticle: "", targetArticle: "" }); setEditingPuzzle(null); } finally { setPuzzleSaving(false); } } function startEditPuzzle(p: DailyPuzzle) { setEditingPuzzle(p); setPuzzleForm({ date: p.date, startArticle: p.startArticle, targetArticle: p.targetArticle }); } const card = "bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4"; const tabBtn = (active: boolean) => `px-3 py-2 rounded-lg text-xs sm:text-sm font-semibold transition-colors cursor-pointer ${ active ? "bg-[#7c3aed] text-white" : "bg-[#1a1a1a] border border-[#2e2e2e] text-[#888] hover:text-[#f0f0f0]" }`; const inputCls = "w-full min-h-10 px-3 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] transition-colors"; return (
{/* Header */}

Admin

WikiRush — panneau d'administration

← Accueil
{loading &&
Chargement...
} {!loading && !data &&
Accès refusé ou erreur serveur.
} {/* Confirm dialog */} {confirmDelete && (

Confirmer la suppression

Supprimer {confirmDelete.label} ? Cette action est irréversible.

)} {data && ( <> {/* Tabs */}
{/* Overview */} {tab === "overview" && (
{[ { label: "Utilisateurs", value: data.totalUsers.toLocaleString("fr-FR") }, { label: "Parties totales", value: data.totalGames.toLocaleString("fr-FR") }, { label: "Parties aujourd'hui", value: data.todayGames.toLocaleString("fr-FR") }, ].map(({ label, value }) => (

{label}

{value}

))}
{/* Graphique activité */}

Activité — 14 derniers jours

{new Date(data.activity[0]?.date + "T12:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "short" })} aujourd'hui
{/* Modes */}

Parties par mode

{data.modeStats.sort((a, b) => b._count.id - a._count.id).map((m) => (
{m.mode}
{m._count.id.toLocaleString("fr-FR")} parties {m._avg.clicks !== null && ~{Math.round(m._avg.clicks)} clics} {m._avg.timeSeconds !== null && ~{fmt(m._avg.timeSeconds)}}
))}
)} {/* Users */} {tab === "users" && (

Utilisateurs

{data.recentUsers.map((u) => (

{u.name} {u.banned && Banni}

{u.email}

{u._count.games} parties

{new Date(u.createdAt).toLocaleDateString("fr-FR")}

))}
)} {/* Games */} {tab === "games" && (

50 dernières parties

{data.recentGames.map((g) => (

{g.user.name} {g.startArticle} → {g.targetArticle}

{new Date(g.playedAt).toLocaleString("fr-FR")}

{g.won ? "Gagné" : "Perdu"} {g.mode} {g.clicks} clics {fmt(g.timeSeconds)}
))}
)} {/* Daily puzzles */} {tab === "daily" && (
{/* Formulaire */}

{editingPuzzle ? `Modifier le puzzle du ${editingPuzzle.date}` : "Nouveau puzzle"}

{!editingPuzzle && ( setPuzzleForm((f) => ({ ...f, date: e.target.value }))} placeholder="Date (YYYY-MM-DD)" /> )} setPuzzleForm((f) => ({ ...f, startArticle: e.target.value }))} placeholder="Article de départ (ex: Tour Eiffel)" /> setPuzzleForm((f) => ({ ...f, targetArticle: e.target.value }))} placeholder="Article cible (ex: Napoléon Bonaparte)" />
{editingPuzzle && ( )}
{/* Liste */} {dailyPuzzles && (

Puzzles existants

{dailyPuzzles.map((p) => (

{p.date}

{p.startArticle} → {p.targetArticle}

{p._count.results} résultat{p._count.results > 1 ? "s" : ""}

))}
)}
)} )}
); }