// src/App.jsx import React, { useEffect, useState } from "react"; import { api } from "./api/client"; import { cycleTag } from "./utils/cycleTag"; import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage"; import { getWinnerLS, setWinnerLS, clearWinnerLS } from "./utils/winnerStorage"; import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles"; import { styles } from "./styles/styles"; import { applyTheme, loadThemeKey, saveThemeKey, DEFAULT_THEME_KEY } from "./styles/themes"; import AdminPanel from "./components/AdminPanel"; import LoginPage from "./components/LoginPage"; import TopBar from "./components/TopBar"; import PasswordModal from "./components/PasswordModal"; import ChipModal from "./components/ChipModal"; import HelpModal from "./components/HelpModal"; import GamePickerCard from "./components/GamePickerCard"; import SheetSection from "./components/SheetSection"; import DesignModal from "./components/DesignModal"; import WinnerCard from "./components/WinnerCard"; import WinnerBadge from "./components/WinnerBadge"; export default function App() { useHpGlobalStyles(); // Auth/Login UI state const [me, setMe] = useState(null); const [loginEmail, setLoginEmail] = useState(""); const [loginPassword, setLoginPassword] = useState(""); const [showPw, setShowPw] = useState(false); // Game/Sheet state const [games, setGames] = useState([]); const [gameId, setGameId] = useState(null); const [sheet, setSheet] = useState(null); const [pulseId, setPulseId] = useState(null); // Winner (per game) const [winnerName, setWinnerName] = useState(""); // Modals const [helpOpen, setHelpOpen] = useState(false); const [chipOpen, setChipOpen] = useState(false); const [chipEntry, setChipEntry] = useState(null); const [userMenuOpen, setUserMenuOpen] = useState(false); const [pwOpen, setPwOpen] = useState(false); const [pw1, setPw1] = useState(""); const [pw2, setPw2] = useState(""); const [pwMsg, setPwMsg] = useState(""); const [pwSaving, setPwSaving] = useState(false); // Theme const [designOpen, setDesignOpen] = useState(false); const [themeKey, setThemeKey] = useState(DEFAULT_THEME_KEY); // ===== Data loaders ===== const load = async () => { const m = await api("/auth/me"); setMe(m); // Theme pro User laden & anwenden const tk = loadThemeKey(m?.email); setThemeKey(tk); applyTheme(tk); const gs = await api("/games"); setGames(gs); if (gs[0] && !gameId) setGameId(gs[0].id); }; const reloadSheet = async () => { if (!gameId) return; const sh = await api(`/games/${gameId}/sheet`); setSheet(sh); }; // ===== Effects ===== // Dropdown outside click useEffect(() => { const onDown = (e) => { const root = e.target?.closest?.("[data-user-menu]"); if (!root) setUserMenuOpen(false); }; if (userMenuOpen) document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [userMenuOpen]); // initial load (try session) useEffect(() => { (async () => { try { await load(); } catch { // not logged in } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // load sheet + winner when game changes useEffect(() => { (async () => { if (!gameId) return; try { await reloadSheet(); } catch { // ignore } // Sieger pro Game aus localStorage laden setWinnerName(getWinnerLS(gameId)); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameId]); // ===== Auth actions ===== 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); setWinnerName(""); }; // ===== Password change ===== const openPwModal = () => { setPwMsg(""); setPw1(""); setPw2(""); setPwOpen(true); setUserMenuOpen(false); }; const closePwModal = () => { setPwOpen(false); setPwMsg(""); setPw1(""); setPw2(""); }; const savePassword = async () => { setPwMsg(""); if (!pw1 || pw1.length < 8) return setPwMsg("❌ Passwort muss mindestens 8 Zeichen haben."); if (pw1 !== pw2) return setPwMsg("❌ Passwörter stimmen nicht überein."); setPwSaving(true); try { await api("/auth/password", { method: "PATCH", body: JSON.stringify({ password: pw1 }), }); setPwMsg("✅ Passwort gespeichert."); setTimeout(() => closePwModal(), 650); } catch (e) { setPwMsg("❌ Fehler: " + (e?.message || "unknown")); } finally { setPwSaving(false); } }; // ===== Theme actions ===== const openDesignModal = () => { setDesignOpen(true); setUserMenuOpen(false); }; const selectTheme = (key) => { setThemeKey(key); applyTheme(key); saveThemeKey(me?.email, key); }; // ===== Game actions ===== 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); // Neues Spiel -> Sieger leer clearWinnerLS(g.id); setWinnerName(""); }; // ===== Winner actions ===== const saveWinner = () => { if (!gameId) return; const v = (winnerName || "").trim(); if (!v) { clearWinnerLS(gameId); setWinnerName(""); return; } setWinnerLS(gameId, v); setWinnerName(v); }; // ===== Sheet actions ===== 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); if (next === "s") { setChipEntry(entry); setChipOpen(true); return; } if (next === null) clearChipLS(gameId, entry.entry_id); await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: next }), }); await reloadSheet(); }; const chooseChip = async (chip) => { if (!chipEntry) return; const entry = chipEntry; setChipOpen(false); setChipEntry(null); setChipLS(gameId, entry.entry_id, chip); try { await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: "s" }), }); } finally { await reloadSheet(); } }; const closeChipModalToDash = async () => { if (!chipEntry) { setChipOpen(false); return; } const entry = chipEntry; setChipOpen(false); setChipEntry(null); clearChipLS(gameId, entry.entry_id); try { await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: null }), }); } finally { await reloadSheet(); } }; const displayTag = (entry) => { const t = entry.note_tag; if (!t) return "—"; if (t === "s") { const chip = getChipLS(gameId, entry.entry_id); return chip ? `s.${chip}` : "s"; } return t; }; // ===== Login page ===== if (!me) { return ( ); } 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 (