// frontend/src/App.jsx import React, { useEffect, useRef, useState } from "react"; import { api } from "./api/client"; import { cycleTag } from "./utils/cycleTag"; import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage"; import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles"; import { styles } from "./styles/styles"; import { applyTheme, DEFAULT_THEME_KEY } from "./styles/themes"; import { stylesTokens } from "./styles/theme"; import LoginPage from "./components/LoginPage"; import SheetSection from "./components/SheetSection"; import ChipModal from "./components/ChipModal"; import DicePanel from "./components/Dice/DicePanel"; import "./AppLayout.css"; 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 (minimal) const [games, setGames] = useState([]); const [gameId, setGameId] = useState(null); const [sheet, setSheet] = useState(null); const [pulseId, setPulseId] = useState(null); // Chip modal const [chipOpen, setChipOpen] = useState(false); const [chipEntry, setChipEntry] = useState(null); const aliveRef = useRef(true); const load = async () => { const m = await api("/auth/me"); setMe(m); const tk = m?.theme_key || DEFAULT_THEME_KEY; applyTheme(tk); const gs = await api("/games"); setGames(gs); // Auto-pick first game (kein UI dafür) if (gs[0] && !gameId) setGameId(gs[0].id); }; const reloadSheet = async () => { if (!gameId) return; const sh = await api(`/games/${gameId}/sheet`); setSheet(sh); }; // initial load useEffect(() => { aliveRef.current = true; (async () => { try { await load(); } catch { // ignore } })(); return () => { aliveRef.current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // on game change useEffect(() => { (async () => { if (!gameId) return; try { await reloadSheet(); } catch { // ignore } })(); }, [gameId]); // Live refresh (nur Sheet) useEffect(() => { if (!me || !gameId) return; let alive = true; const tick = async () => { try { await reloadSheet(); } catch {} }; tick(); const id = setInterval(() => { if (!alive) return; tick(); }, 2500); return () => { alive = false; clearInterval(id); }; }, [me?.id, gameId]); // ===== Auth actions ===== const doLogin = async () => { await api("/auth/login", { method: "POST", body: JSON.stringify({ email: loginEmail, password: loginPassword }), }); await load(); }; // ===== Sheet actions (wie bisher) ===== 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, chip: null }), }); 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", chip }), }); } 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, chip: null }), }); } finally { await reloadSheet(); } }; const displayTag = (entry) => { const t = entry.note_tag; if (!t) return "—"; if (t === "s") { const chip = entry.chip || getChipLS(gameId, entry.entry_id); return chip ? `s.${chip}` : "s"; } return t; // i oder m }; // ===== 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 || [] }, ] : []; /** * ✅ Unified Placeholder system * Variants: * - "compact": small top / dice (low height) * - "tile": normal cards (HUD, decks, etc.) * - "panel": large container (Board) */ const PlaceholderCard = ({ title, subtitle = "(placeholder)", variant = "tile", icon = null, children = null, overflow = "hidden", // ✅ FIX: allow some tiles to not clip neighbors }) => { const v = variant; const pad = v === "compact" ? 10 : v === "panel" ? 14 : 12; const titleSize = v === "compact" ? 12.5 : v === "panel" ? 14.5 : 13; const subSize = v === "compact" ? 11.5 : 12; const dashHeight = v === "compact" ? 46 : v === "panel" ? null : 64; const base = { borderRadius: 18, border: `1px solid ${stylesTokens.panelBorder}`, background: stylesTokens.panelBg, boxShadow: v === "panel" ? "0 20px 70px rgba(0,0,0,0.45)" : "0 12px 30px rgba(0,0,0,0.35)", backdropFilter: "blur(10px)", padding: pad, overflow, // ✅ FIX: default hidden, but can be visible where needed minWidth: 0, position: "relative", }; const headerRow = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, }; const titleStyle = { fontWeight: 900, color: stylesTokens.textMain, fontSize: titleSize, letterSpacing: 0.2, lineHeight: 1.15, }; const subStyle = { marginTop: 6, color: stylesTokens.textDim, fontSize: subSize, opacity: 0.95, lineHeight: 1.25, }; const dashStyle = { marginTop: v === "compact" ? 8 : 10, height: dashHeight, borderRadius: 14, border: `1px dashed ${stylesTokens.panelBorder}`, opacity: 0.75, }; const glowLine = v === "panel" ? { content: '""', position: "absolute", inset: 0, background: `linear-gradient(90deg, transparent, ${stylesTokens.goldLine}, transparent)`, opacity: 0.18, pointerEvents: "none", } : null; return (
{v === "panel" ?
: null}
{icon ?
{icon}
: null}
{title}
{subtitle ?
{subtitle}
: null} {/* Content area */} {children ? (
{children}
) : v === "panel" ? null : (
)}
); }; // Player rail placeholder (rechts vom Board, vor Notizen) const players = [ { id: "harry", name: "Harry Potter", img: "/players/harry.jpg", color: "#7c4dff", // Lila active: true, }, { id: "ginny", name: "Ginny Weasley", img: "/players/ginny.jpg", color: "#3b82f6", // Blau }, { id: "hermione", name: "Hermine Granger", img: "/players/hermione.jpg", color: "#ef4444", // Rot }, { id: "luna", name: "Luna Lovegood", img: "/players/luna.jpg", color: "#e5e7eb", // Weiß }, { id: "neville", name: "Neville Longbottom", img: "/players/neville.jpg", color: "#22c55e", // Grün }, { id: "ron", name: "Ron Weasley", img: "/players/ron.jpg", color: "#facc15", // Gelb }, ]; const PlayerIcon = ({ player }) => { const size = player.active ? 56 : 40; return (
{player.name}
); }; const HogwartsPointsCard = ({ value = 0 }) => { const GOLD = "#f2d27a"; return (
{value}
HP
SCORE
); }; const PlayerIdentityCard = ({ name = "Harry Potter", houseLabel = "Gryffindor", borderColor = "#7c4dff", img = "/player_cards/harry.png", }) => { return (
{ const rect = e.currentTarget.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; const rx = (0.5 - y) * 6; const ry = (x - 0.5) * 8; e.currentTarget.style.transform = `translateY(-16px) rotate(-0.6deg) perspective(700px) rotateX(${rx}deg) rotateY(${ry}deg)`; }} onMouseLeave={(e) => { e.currentTarget.style.transform = "translateY(-10px) rotate(-0.6deg)"; e.currentTarget.style.boxShadow = `0 18px 55px rgba(0,0,0,0.55), 0 0 26px rgba(124,77,255,0.22)`; }} onMouseEnter={(e) => { e.currentTarget.style.transform = "translateY(-18px) rotate(-0.6deg)"; e.currentTarget.style.boxShadow = `0 26px 70px rgba(0,0,0,0.62), 0 0 34px ${borderColor}`; }} > {name}
{name}
{houseLabel}
Identity
); }; return (
); }