diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 19d5644..aca6d5c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -19,27 +19,110 @@ function cycleTag(tag) { return null; } +function AdminPanel() { + const [users, setUsers] = useState([]); + 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 createUser = async () => { + setMsg(""); + try { + await api("/admin/users", { + method: "POST", + body: JSON.stringify({ email, password, role }), + }); + setEmail(""); + setPassword(""); + setRole("user"); + setMsg("✅ User erstellt."); + await loadUsers(); + } catch (e) { + setMsg("❌ Fehler: " + (e?.message || "unknown")); + } + }; + + return ( +
+
Admin Dashboard
+ +
+ setEmail(e.target.value)} + placeholder="Email" + style={styles.input} + /> + setPassword(e.target.value)} + placeholder="Initial Passwort" + type="password" + style={styles.input} + /> + + +
+ + {msg &&
{msg}
} + +
Vorhandene User
+
+ {users.map((u) => ( +
+
{u.email}
+
{u.role}
+
+ {u.disabled ? "disabled" : "active"} +
+
+ ))} +
+
+ ); +} + export default function App() { const [me, setMe] = useState(null); - const [email, setEmail] = useState("admin@local"); - const [password, setPassword] = useState(""); + const [loginEmail, setLoginEmail] = useState("admin@local"); + const [loginPassword, setLoginPassword] = useState(""); const [games, setGames] = useState([]); const [gameId, setGameId] = useState(null); const [sheet, setSheet] = useState(null); - const [tab, setTab] = useState("suspect"); const load = async () => { const m = await api("/auth/me"); setMe(m); + const gs = await api("/games"); setGames(gs); + + // wenn noch kein game ausgewählt ist -> erstes nehmen if (gs[0] && !gameId) setGameId(gs[0].id); }; useEffect(() => { (async () => { - try { await load(); } catch {} + try { + await load(); + } catch { + // not logged in + } })(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -48,31 +131,51 @@ export default function App() { try { const sh = await api(`/games/${gameId}/sheet`); setSheet(sh); - } catch {} + } catch { + // ignore + } })(); }, [gameId]); const doLogin = async () => { - await api("/auth/login", { method: "POST", body: JSON.stringify({ email, password }) }); + 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 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 toggleCross = async (entry) => { - // Tap auf Name: unknown <-> crossed const next = entry.status === 1 ? 0 : 1; await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ status: next }), }); - const sh = await api(`/games/${gameId}/sheet`); - setSheet(sh); + await reloadSheet(); }; const toggleTag = async (entry) => { @@ -81,72 +184,265 @@ export default function App() { method: "PATCH", body: JSON.stringify({ note_tag: next }), }); - const sh = await api(`/games/${gameId}/sheet`); - setSheet(sh); + await reloadSheet(); }; + // Login Screen if (!me) { return ( -
-

Login

- setEmail(e.target.value)} placeholder="Email" style={{ width: "100%", padding: 10, marginBottom: 8 }} /> - setPassword(e.target.value)} placeholder="Passwort" type="password" style={{ width: "100%", padding: 10, marginBottom: 8 }} /> - -

Default Admin: admin@local (Passwort aus .env)

+
+
+
Zauber-Detektiv Notizbogen
+ +
+
Login
+ +
+ setLoginEmail(e.target.value)} + placeholder="Email" + style={styles.input} + /> + setLoginPassword(e.target.value)} + placeholder="Passwort" + type="password" + style={styles.input} + /> + + +
+ Default Admin: admin@local (Passwort aus .env) +
+
+
+
); } - const entries = sheet ? sheet[tab] : []; + const sections = sheet + ? [ + { key: "suspect", title: "VERDÄCHTIGE PERSON", entries: sheet.suspect || [] }, + { key: "item", title: "GEGENSTAND", entries: sheet.item || [] }, + { key: "location", title: "ORT", entries: sheet.location || [] }, + ] + : []; return ( -
-
-
-
{me.email}
-
{me.role}
-
- -
- -
- -
- -
- {["suspect","item","location"].map(t => ( - - ))} -
- -
- {entries.map((e) => ( -
- {/* Name: Tap toggelt crossed */} -
toggleCross(e)} style={{ cursor: "pointer", textDecoration: e.status === 1 ? "line-through" : "none", userSelect: "none" }}> - {e.label} -
- - {/* Spalte 1: X */} -
{e.status === 1 ? "X" : ""}
- - {/* Spalte 2: gelbes ✓ */} -
{e.status === 1 ? "✓" : ""}
- - {/* Spalte 3: grünes ✓ (für später, wenn du confirmed nutzt) */} -
{e.status === 2 ? "✓" : ""}
- - {/* Spalte 4: i/m/s */} - +
+
+ {/* Top Bar */} +
+
+
{me.email}
+
{me.role}
- ))} + +
+ + +
+
+ + {/* Admin Panel */} + {me.role === "admin" && } + + {/* Game Selector */} +
+
+
Spiel
+
+ +
+
+
+ + {/* Sheet */} +
+ {sections.map((sec) => ( +
+
{sec.title}
+ +
+ {sec.entries.map((e) => ( +
+ {/* Name: Tap toggelt crossed */} +
toggleCross(e)} + style={{ + ...styles.name, + textDecoration: e.status === 1 ? "line-through" : "none", + opacity: e.status === 1 ? 0.85 : 1, + }} + title="Tippen = durchstreichen" + > + {e.label} +
+ + {/* Spalte 1: X */} +
{e.status === 1 ? "X" : ""}
+ + {/* Spalte 2: gelbes ✓ */} +
{e.status === 1 ? "✓" : ""}
+ + {/* Spalte 3: confirmed (grünes ✓) – aktuell nicht per UI gesetzt */} +
{e.status === 2 ? "✓" : ""}
+ + {/* Spalte 4: i/m/s */} + +
+ ))} +
+
+ ))} +
+ +
); } + +/* ===== Styles (Parchment / Boardgame Look) ===== */ +const styles = { + page: { + minHeight: "100vh", + padding: 16, + background: + "radial-gradient(circle at 20% 10%, rgba(255,255,255,0.40), rgba(0,0,0,0) 45%), linear-gradient(180deg, #f3e7cf, #e5d2ac)", + }, + shell: { + fontFamily: "system-ui", + maxWidth: 620, + margin: "0 auto", + }, + title: { + fontWeight: 1000, + letterSpacing: 0.5, + fontSize: 22, + color: "#20140c", + marginBottom: 12, + textShadow: "0 1px 0 rgba(255,255,255,0.35)", + }, + topBar: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: 10, + padding: 12, + borderRadius: 16, + background: "rgba(255,255,255,0.35)", + border: "1px solid rgba(0,0,0,0.15)", + boxShadow: "0 6px 18px rgba(0,0,0,0.10)", + backdropFilter: "blur(4px)", + }, + card: { + borderRadius: 16, + overflow: "hidden", + border: "1px solid rgba(0,0,0,0.18)", + background: "rgba(255,255,255,0.35)", + boxShadow: "0 10px 24px rgba(0,0,0,0.12)", + }, + sectionHeader: { + padding: "10px 12px", + fontWeight: 1000, + letterSpacing: 0.7, + color: "#2b1a0e", + background: "linear-gradient(180deg, #caa45a, #a67a2a)", + borderBottom: "1px solid rgba(0,0,0,0.25)", + textTransform: "uppercase", + }, + row: { + display: "grid", + gridTemplateColumns: "1fr 40px 40px 40px 56px", + gap: 8, + padding: "10px 12px", + alignItems: "center", + borderBottom: "1px solid rgba(0,0,0,0.10)", + background: "rgba(255,255,255,0.22)", + }, + name: { + cursor: "pointer", + userSelect: "none", + color: "#20140c", + fontWeight: 700, + }, + cell: { + textAlign: "center", + fontWeight: 1000, + color: "#20140c", + }, + tagBtn: { + padding: "8px 0", + fontWeight: 1000, + borderRadius: 10, + border: "1px solid rgba(0,0,0,0.25)", + background: "linear-gradient(180deg, rgba(255,255,255,0.55), rgba(0,0,0,0.06))", + }, + input: { + width: "100%", + padding: 10, + borderRadius: 12, + border: "1px solid rgba(0,0,0,0.25)", + background: "rgba(255,255,255,0.55)", + outline: "none", + }, + primaryBtn: { + padding: "10px 12px", + borderRadius: 12, + border: "1px solid rgba(0,0,0,0.25)", + background: "linear-gradient(180deg, #f3d79b, #caa45a)", + fontWeight: 1000, + cursor: "pointer", + }, + secondaryBtn: { + padding: "10px 12px", + borderRadius: 12, + border: "1px solid rgba(0,0,0,0.25)", + background: "linear-gradient(180deg, rgba(255,255,255,0.75), rgba(0,0,0,0.05))", + fontWeight: 900, + cursor: "pointer", + }, + adminWrap: { + marginTop: 14, + padding: 12, + borderRadius: 16, + border: "1px solid rgba(0,0,0,0.18)", + background: "rgba(255,255,255,0.30)", + boxShadow: "0 8px 18px rgba(0,0,0,0.10)", + }, + adminTitle: { + fontWeight: 1000, + color: "#20140c", + marginBottom: 8, + }, + adminGrid: { + display: "grid", + gridTemplateColumns: "1fr 1fr 120px 140px", + gap: 8, + }, + userRow: { + display: "grid", + gridTemplateColumns: "1fr 80px 90px", + gap: 8, + padding: 10, + borderRadius: 12, + background: "rgba(255,255,255,0.55)", + border: "1px solid rgba(0,0,0,0.10)", + }, +};