From cf81c25e6ec99931794f6983c8a33b429fb1cb7f Mon Sep 17 00:00:00 2001 From: nessi Date: Sat, 7 Feb 2026 11:15:33 +0100 Subject: [PATCH] new logic, testing --- frontend/src/App.jsx | 639 +++++++++++-------------------------------- 1 file changed, 162 insertions(+), 477 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8d4e48e..3a8927f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,4 @@ import React, { useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import WinnerCelebration from "./components/WinnerCelebration"; import { api } from "./api/client"; import { cycleTag } from "./utils/cycleTag"; @@ -11,19 +9,9 @@ import { styles } from "./styles/styles"; import { applyTheme, DEFAULT_THEME_KEY } from "./styles/themes"; import { stylesTokens } from "./styles/theme"; -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"; -import NewGameModal from "./components/NewGameModal"; -import StatsModal from "./components/StatsModal"; +import ChipModal from "./components/ChipModal"; export default function App() { useHpGlobalStyles(); @@ -34,87 +22,30 @@ export default function App() { const [loginPassword, setLoginPassword] = useState(""); const [showPw, setShowPw] = useState(false); - // Game/Sheet state + // Game/Sheet state (minimal) const [games, setGames] = useState([]); const [gameId, setGameId] = useState(null); const [sheet, setSheet] = useState(null); const [pulseId, setPulseId] = useState(null); - // Game meta - const [gameMeta, setGameMeta] = useState(null); // {code, host_user_id, winner_email, winner_user_id} - const [members, setMembers] = useState([]); - - // Winner selection (host only) - const [winnerUserId, setWinnerUserId] = useState(""); - - // Modals - const [helpOpen, setHelpOpen] = useState(false); + // Chip modal 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); - - // New Game Modal - const [newGameOpen, setNewGameOpen] = useState(false); - - // ===== Stats Modal ===== - const [statsOpen, setStatsOpen] = useState(false); - const [stats, setStats] = useState(null); - const [statsLoading, setStatsLoading] = useState(false); - const [statsError, setStatsError] = useState(""); - - // ===== Join Snack (bottom toast) ===== - const [snack, setSnack] = useState(""); - const snackTimerRef = useRef(null); - - // track members to detect joins - const lastMemberIdsRef = useRef(new Set()); - const membersBaselineRef = useRef(false); - - // ===== Winner Celebration ===== - const [celebrateOpen, setCelebrateOpen] = useState(false); - const [celebrateName, setCelebrateName] = useState(""); - - // baseline per game: beim ersten Meta-Load NICHT feiern - const winnerBaselineRef = useRef(false); - const lastWinnerIdRef = useRef(null); - - const showSnack = (msg) => { - setSnack(msg); - if (snackTimerRef.current) clearTimeout(snackTimerRef.current); - snackTimerRef.current = setTimeout(() => setSnack(""), 1800); - }; - - const vibrate = (pattern) => { - try { - if (typeof navigator !== "undefined" && "vibrate" in navigator) { - navigator.vibrate(pattern); - } - } catch { - // ignore - } - }; + // Live refresh (optional) + const aliveRef = useRef(true); const load = async () => { const m = await api("/auth/me"); setMe(m); const tk = m?.theme_key || DEFAULT_THEME_KEY; - setThemeKey(tk); 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); }; @@ -124,110 +55,46 @@ export default function App() { setSheet(sh); }; - const loadGameMeta = async () => { - if (!gameId) return; - - const meta = await api(`/games/${gameId}`); - setGameMeta(meta); - setWinnerUserId(meta?.winner_user_id || ""); - - const mem = await api(`/games/${gameId}/members`); - setMembers(mem); - - // ✅ detect new members (join notifications for everyone) - try { - const prev = lastMemberIdsRef.current; - const nowIds = new Set((mem || []).map((m) => String(m.id))); - - if (!membersBaselineRef.current) { - // first load for this game -> set baseline, no notification - membersBaselineRef.current = true; - lastMemberIdsRef.current = nowIds; - return; - } - - const added = (mem || []).filter((m) => !prev.has(String(m.id))); - - if (added.length > 0) { - const names = added - .map((m) => ((m.display_name || "").trim() || (m.email || "").trim() || "Jemand")) - .slice(0, 3); - - const msg = - added.length === 1 - ? `✨ ${names[0]} ist beigetreten` - : `✨ ${names.join(", ")} ${added.length > 3 ? `(+${added.length - 3}) ` : ""}sind beigetreten`; - - showSnack(msg); - vibrate(25); // dezent & kurz - } - - lastMemberIdsRef.current = nowIds; - } catch { - // ignore snack errors - } - }; - - // 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 useEffect(() => { + aliveRef.current = true; (async () => { try { await load(); - } catch {} + } catch { + // ignore + } })(); + return () => { + aliveRef.current = false; + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // on game change useEffect(() => { - // reset join detection baseline when switching games - membersBaselineRef.current = false; - lastMemberIdsRef.current = new Set(); - - // reset winner celebration baseline when switching games - winnerBaselineRef.current = false; - lastWinnerIdRef.current = null; - setCelebrateOpen(false); - setCelebrateName(""); - (async () => { if (!gameId) return; try { await reloadSheet(); - await loadGameMeta(); - } catch {} + } catch { + // ignore + } })(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameId]); - // ✅ Live refresh (Members/Meta) – damit neue Joiner ohne Reload sichtbar sind - // Für 5–6 Spieler reicht 2.5s völlig, ist "live genug" und schont Backend. + // Live refresh (nur Sheet) useEffect(() => { if (!me || !gameId) return; let alive = true; - const tick = async () => { try { - await loadGameMeta(); // refresh members + winner meta - } catch { - // ignore - } + await reloadSheet(); + } catch {} }; - // sofort einmal ziehen tick(); - const id = setInterval(() => { if (!alive) return; tick(); @@ -237,38 +104,8 @@ export default function App() { alive = false; clearInterval(id); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [me?.id, gameId]); - useEffect(() => { - // wid kann auch "" sein (kein Sieger) - const wid = gameMeta?.winner_user_id ? String(gameMeta.winner_user_id) : ""; - - // Baseline beim ersten Meta-Load setzen – egal ob Winner existiert oder nicht - if (!winnerBaselineRef.current) { - winnerBaselineRef.current = true; - lastWinnerIdRef.current = wid; // kann "" sein - return; - } - - // Nur reagieren, wenn sich wid ändert - if (lastWinnerIdRef.current !== wid) { - lastWinnerIdRef.current = wid; - - // wenn wid leer wird (reset), nicht feiern - if (!wid) return; - - const name = - (gameMeta?.winner_display_name || "").trim() || - (gameMeta?.winner_email || "").trim() || - "Jemand"; - - setCelebrateName(name); - setCelebrateOpen(true); - } - }, [gameMeta?.winner_user_id, gameMeta?.winner_display_name, gameMeta?.winner_email]); - - // ===== Auth actions ===== const doLogin = async () => { await api("/auth/login", { @@ -278,167 +115,7 @@ export default function App() { await load(); }; - const doLogout = async () => { - await api("/auth/logout", { method: "POST" }); - setMe(null); - setGames([]); - setGameId(null); - setSheet(null); - setGameMeta(null); - setMembers([]); - setWinnerUserId(""); - - // reset winner celebration on logout - winnerBaselineRef.current = false; - lastWinnerIdRef.current = null; - setCelebrateOpen(false); - setCelebrateName(""); - }; - - // ===== Password ===== - 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 ===== - const openDesignModal = () => { - setDesignOpen(true); - setUserMenuOpen(false); - }; - - const selectTheme = async (key) => { - setThemeKey(key); - applyTheme(key); - - // ✅ sofort für nächsten Start merken (verhindert Flash) - try { - localStorage.setItem(`hpTheme:${(me?.email || "guest").toLowerCase()}`, key); - localStorage.setItem("hpTheme:guest", key); // fallback, falls noch nicht eingeloggt - } catch { - // ignore - } - - try { - await api("/auth/theme", { - method: "PATCH", - body: JSON.stringify({ theme_key: key }), - }); - } catch { - // theme locally already applied; ignore backend error - } - }; - - - // ===== Stats (always fresh on open) ===== - const openStatsModal = async () => { - setUserMenuOpen(false); - setStatsOpen(true); - setStatsError(""); - setStatsLoading(true); - - try { - const s = await api("/auth/me/stats"); - setStats(s); - } catch (e) { - setStats(null); - setStatsError("❌ Fehler: " + (e?.message || "unknown")); - } finally { - setStatsLoading(false); - } - }; - - const closeStatsModal = () => { - setStatsOpen(false); - setStatsError(""); - }; - - // ===== New game flow ===== - const createGame = async () => { - // ✅ alten Game-State komplett loswerden, damit nix am alten Spiel "hängen bleibt" - setSheet(null); - setGameMeta(null); - setMembers([]); - setWinnerUserId(""); - setPulseId(null); - - // auch Chip-Modal-State resetten - setChipOpen(false); - setChipEntry(null); - - // reset winner celebration baseline for the new game - winnerBaselineRef.current = false; - lastWinnerIdRef.current = null; - setCelebrateOpen(false); - setCelebrateName(""); - - const g = await api("/games", { - method: "POST", - body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }), - }); - - const gs = await api("/games"); - setGames(gs); - - // ✅ auf neues Game wechseln (triggert reloadSheet/loadGameMeta via effect) - setGameId(g.id); - - return g; // includes code - }; - - const joinGame = async (code) => { - const res = await api("/games/join", { - method: "POST", - body: JSON.stringify({ code }), - }); - - const gs = await api("/games"); - setGames(gs); - setGameId(res.id); - }; - - // ===== Winner ===== - const saveWinner = async () => { - if (!gameId) return; - await api(`/games/${gameId}/winner`, { - method: "PATCH", - body: JSON.stringify({ winner_user_id: winnerUserId || null }), - }); - await loadGameMeta(); - }; - - // ===== Sheet actions ===== + // ===== Sheet actions (wie bisher) ===== const cycleStatus = async (entry) => { let next = 0; if (entry.status === 0) next = 2; @@ -521,11 +198,9 @@ export default function App() { if (!t) return "—"; if (t === "s") { - // Prefer backend chip, fallback localStorage const chip = entry.chip || getChipLS(gameId, entry.entry_id); return chip ? `s.${chip}` : "s"; } - return t; // i oder m }; @@ -552,157 +227,167 @@ export default function App() { ] : []; - const isHost = !!(me?.id && gameMeta?.host_user_id && me.id === gameMeta.host_user_id); + const PlaceholderCard = ({ title, hint }) => ( +
+
+ {title} +
+
+ {hint} +
+
+
+ ); return (
- {/* Winner Celebration Overlay */} - setCelebrateOpen(false)} - /> -