"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_H = 64; // px, correspond à h-16 const max = Math.max(...data.map((d) => d.count), 1); return (
{data.map((d) => { const h = Math.max((d.count / max) * MAX_H, 3); 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" : ""}

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