import React, { useEffect, useState } from "react"; const API = "/api"; async function api(path, opts = {}) { const res = await fetch(API + path, { credentials: "include", headers: { "Content-Type": "application/json" }, ...opts, }); if (!res.ok) throw new Error(await res.text()); return res.json(); } function cycleTag(tag) { if (!tag) return "i"; if (tag === "i") return "m"; if (tag === "m") return "s"; return null; } function AdminPanel() { const [users, setUsers] = useState([]); const [open, setOpen] = useState(false); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [role, setRole] = useState("user"); const [msg, setMsg] = useState(""); const loadUsers = async () => { const u = await api("/admin/users"); setUsers(u); }; useEffect(() => { loadUsers().catch(() => {}); }, []); const resetForm = () => { setEmail(""); setPassword(""); setRole("user"); }; const createUser = async () => { setMsg(""); try { await api("/admin/users", { method: "POST", body: JSON.stringify({ email, password, role }), }); setMsg("✅ User erstellt."); await loadUsers(); resetForm(); setOpen(false); } catch (e) { setMsg("❌ Fehler: " + (e?.message || "unknown")); } }; const closeModal = () => { setOpen(false); setMsg(""); }; return (
Admin Dashboard
Vorhandene User
{users.map((u) => (
{u.email}
{u.role}
{u.disabled ? "disabled" : "active"}
))}
{open && (
e.stopPropagation()} >
Neuen User anlegen
setEmail(e.target.value)} placeholder="Email" style={styles.input} autoFocus /> setPassword(e.target.value)} placeholder="Initial Passwort" type="password" style={styles.input} /> {msg &&
{msg}
}
Tipp: Klick auf Item: Grün → Rot → Grau → Leer
)}
); } export default function App() { const [me, setMe] = useState(null); const [loginEmail, setLoginEmail] = useState(""); const [loginPassword, setLoginPassword] = useState(""); const [showPw, setShowPw] = useState(false); const [games, setGames] = useState([]); const [gameId, setGameId] = useState(null); const [sheet, setSheet] = useState(null); const [pulseId, setPulseId] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const load = async () => { const m = await api("/auth/me"); setMe(m); const gs = await api("/games"); setGames(gs); if (gs[0] && !gameId) setGameId(gs[0].id); }; // ✅ Google Fonts laden useEffect(() => { if (document.getElementById("hp-fonts")) return; const pre1 = document.createElement("link"); pre1.id = "hp-fonts-pre1"; pre1.rel = "preconnect"; pre1.href = "https://fonts.googleapis.com"; const pre2 = document.createElement("link"); pre2.id = "hp-fonts-pre2"; pre2.rel = "preconnect"; pre2.href = "https://fonts.gstatic.com"; pre2.crossOrigin = "anonymous"; const link = document.createElement("link"); link.id = "hp-fonts"; link.rel = "stylesheet"; link.href = "https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700;900&family=IM+Fell+English:ital,wght@0,400;0,0.700;1,400&display=swap"; document.head.appendChild(pre1); document.head.appendChild(pre2); document.head.appendChild(link); }, []); // ✅ Keyframes useEffect(() => { if (document.getElementById("hp-anim-style")) return; const style = document.createElement("style"); style.id = "hp-anim-style"; style.innerHTML = ` @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes popIn { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes rowPulse { 0%{ transform: scale(1); } 50%{ transform: scale(1.01); } 100%{ transform: scale(1); } } @keyframes candleGlow { 0% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); } 35% { opacity: .85; transform: translateY(-2px) scale(1.02); filter: blur(18px); } 70% { opacity: .62; transform: translateY(1px) scale(1.01); filter: blur(17px); } 100% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); } } `; document.head.appendChild(style); }, []); // ✅ html/body reset useEffect(() => { document.documentElement.style.height = "100%"; document.body.style.height = "100%"; document.documentElement.style.margin = "0"; document.body.style.margin = "0"; document.documentElement.style.padding = "0"; document.body.style.padding = "0"; }, []); // ✅ Global CSS (Hover komplett weg + sauberes Scroll-Verhalten) useEffect(() => { if (document.getElementById("hp-global-style")) return; const style = document.createElement("style"); style.id = "hp-global-style"; style.innerHTML = ` html, body { overscroll-behavior-y: none; -webkit-text-size-adjust: 100%; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background: #1c140d; } body { overflow-x: hidden; // touch-action: pan-y; -webkit-overflow-scrolling: touch; } #root { background: transparent; } /* .hp-row:hover, .hp-row:active, .hp-row:focus, .hp-row:focus-within { background: inherit !important; filter: none !important; box-shadow: none !important; outline: none !important; } .hp-row *:hover, .hp-row *:active, .hp-row *:focus { filter: none !important; box-shadow: none !important; outline: none !important; } .hp-row, .hp-row * { -webkit-tap-highlight-color: transparent !important; -webkit-touch-callout: none; } button:hover, button:active, button:focus { filter: none !important; box-shadow: none !important; outline: none !important; }*/ `; document.head.appendChild(style); }, []); useEffect(() => { (async () => { try { await load(); } catch { // not logged in } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { (async () => { if (!gameId) return; try { const sh = await api(`/games/${gameId}/sheet`); setSheet(sh); } catch { // ignore } })(); }, [gameId]); const doLogin = async () => { await api("/auth/login", { method: "POST", body: JSON.stringify({ email: loginEmail, password: loginPassword }), }); await load(); }; const doLogout = async () => { await api("/auth/logout", { method: "POST" }); setMe(null); setGames([]); setGameId(null); setSheet(null); }; const newGame = async () => { const g = await api("/games", { method: "POST", body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }), }); const gs = await api("/games"); setGames(gs); setGameId(g.id); }; const reloadSheet = async () => { if (!gameId) return; const sh = await api(`/games/${gameId}/sheet`); setSheet(sh); }; const cycleStatus = async (entry) => { let next = 0; if (entry.status === 0) next = 2; else if (entry.status === 2) next = 1; else if (entry.status === 1) next = 3; else next = 0; await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ status: next }), }); await reloadSheet(); setPulseId(entry.entry_id); setTimeout(() => setPulseId(null), 220); }; const toggleTag = async (entry) => { const next = cycleTag(entry.note_tag); await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: next }), }); await reloadSheet(); }; if (!me) { return (