From 3b628b6c575d811f80519f7064a3004a035d4a62 Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 4 Feb 2026 08:58:00 +0100 Subject: [PATCH] Refactor App structure and add modular components Split GamePickerCard, HelpModal, and SheetSection into separate components for better modularity and clarity. Refactored App.jsx to utilize these new components, restructured state variables, and organized functions for improved readability. Enhanced code comments for easier maintenance. --- frontend/src/App.jsx | 88 +++++++++++--- frontend/src/components/GamePickerCard.jsx | 36 ++++++ frontend/src/components/HelpModal.jsx | 134 +++++++++++++++++++++ frontend/src/components/SheetSection.jsx | 121 +++++++++++++++++++ 4 files changed, 360 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/GamePickerCard.jsx create mode 100644 frontend/src/components/HelpModal.jsx create mode 100644 frontend/src/components/SheetSection.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 07d7217..4f56f65 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,36 +1,42 @@ +// 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 { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles"; import { styles } from "./styles/styles"; -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"; -// HelpModal + SheetSection würdest du analog auslagern +import HelpModal from "./components/HelpModal"; +import GamePickerCard from "./components/GamePickerCard"; +import SheetSection from "./components/SheetSection"; 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); + // Modals + const [helpOpen, setHelpOpen] = useState(false); + const [chipOpen, setChipOpen] = useState(false); const [chipEntry, setChipEntry] = useState(null); - const [helpOpen, setHelpOpen] = useState(false); - const [userMenuOpen, setUserMenuOpen] = useState(false); const [pwOpen, setPwOpen] = useState(false); @@ -39,6 +45,7 @@ export default function App() { const [pwMsg, setPwMsg] = useState(""); const [pwSaving, setPwSaving] = useState(false); + // ===== Data loaders ===== const load = async () => { const m = await api("/auth/me"); setMe(m); @@ -49,6 +56,14 @@ export default function App() { 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) => { @@ -59,25 +74,32 @@ export default function App() { return () => document.removeEventListener("mousedown", onDown); }, [userMenuOpen]); + // initial load (try session) useEffect(() => { (async () => { try { await load(); - } catch {} + } catch { + // not logged in + } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // load sheet when game changes useEffect(() => { (async () => { if (!gameId) return; try { - const sh = await api(`/games/${gameId}/sheet`); - setSheet(sh); - } catch {} + await reloadSheet(); + } catch { + // ignore + } })(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameId]); + // ===== Auth actions ===== const doLogin = async () => { await api("/auth/login", { method: "POST", @@ -94,7 +116,7 @@ export default function App() { setSheet(null); }; - // Password change + // ===== Password change ===== const openPwModal = () => { setPwMsg(""); setPw1(""); @@ -112,6 +134,7 @@ export default function App() { 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."); @@ -130,6 +153,7 @@ export default function App() { } }; + // ===== Game actions ===== const newGame = async () => { const g = await api("/games", { method: "POST", @@ -140,12 +164,7 @@ export default function App() { setGameId(g.id); }; - const reloadSheet = async () => { - if (!gameId) return; - const sh = await api(`/games/${gameId}/sheet`); - setSheet(sh); - }; - + // ===== Sheet actions ===== const cycleStatus = async (entry) => { let next = 0; if (entry.status === 0) next = 2; @@ -166,18 +185,21 @@ export default function App() { const toggleTag = async (entry) => { const next = cycleTag(entry.note_tag); + // going to "s" -> open chip modal, don't write backend yet if (next === "s") { setChipEntry(entry); setChipOpen(true); return; } + // s -> — : clear local chip 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(); }; @@ -188,9 +210,11 @@ export default function App() { setChipOpen(false); setChipEntry(null); + // frontend-only save setChipLS(gameId, entry.entry_id, chip); try { + // backend only gets "s" await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: "s" }), @@ -229,7 +253,7 @@ export default function App() { const chip = getChipLS(gameId, entry.entry_id); return chip ? `s.${chip}` : "s"; } - return t; + return t; // i oder m }; // ===== Login page ===== @@ -273,8 +297,30 @@ export default function App() { {me.role === "admin" && } - {/* hier bleiben vorerst: Spiel selector + Help + Sections - -> kannst du als nächsten Schritt in Komponenten auslagern */} + setHelpOpen(true)} + /> + + setHelpOpen(false)} /> + +
+ {sections.map((sec) => ( + + ))} +
+ +
- + ); } diff --git a/frontend/src/components/GamePickerCard.jsx b/frontend/src/components/GamePickerCard.jsx new file mode 100644 index 0000000..1b89397 --- /dev/null +++ b/frontend/src/components/GamePickerCard.jsx @@ -0,0 +1,36 @@ +// src/components/GamePickerCard.jsx +import React from "react"; +import { styles } from "../styles/styles"; + +export default function GamePickerCard({ + games, + gameId, + setGameId, + onOpenHelp, +}) { + return ( +
+
+
Spiel
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/components/HelpModal.jsx b/frontend/src/components/HelpModal.jsx new file mode 100644 index 0000000..3196845 --- /dev/null +++ b/frontend/src/components/HelpModal.jsx @@ -0,0 +1,134 @@ +// src/components/HelpModal.jsx +import React from "react"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; + +export default function HelpModal({ open, onClose }) { + if (!open) return null; + + return ( +
+
e.stopPropagation()}> +
+
Hilfe
+ +
+ +
+
1) Namen anklicken (Status)
+
+ Tippe auf einen Namen, um den Status zu wechseln. Reihenfolge: +
+ +
+
+ + ✓ + +
+ Grün = bestätigt / fix richtig +
+
+ +
+ + ✕ + +
+ Rot = ausgeschlossen / fix falsch +
+
+ +
+ + ? + +
+ Grau = unsicher / „vielleicht“ +
+
+ +
+ + – + +
+ Leer = unknown / noch nicht bewertet +
+
+
+ +
+ +
2) i / m / s Button (Notiz)
+
+ Rechts pro Zeile gibt es einen Button, der durch diese Werte rotiert: +
+ +
+
+ i +
+ i = „Ich habe diese Geheimkarte“ +
+
+ +
+ m +
+ m = „Geheimkarte aus dem mittleren Deck“ +
+
+ +
+ s +
+ s = „Ein anderer Spieler hat diese Karte“ (Chip Auswahl) +
+
+ +
+ +
+ = keine Notiz +
+
+
+ +
+ +
+ Tipp: Jeder Spieler sieht nur seine eigenen Notizen – andere Spieler können nicht in + deinen Zettel schauen. +
+
+
+
+ ); +} diff --git a/frontend/src/components/SheetSection.jsx b/frontend/src/components/SheetSection.jsx new file mode 100644 index 0000000..9ca0cb5 --- /dev/null +++ b/frontend/src/components/SheetSection.jsx @@ -0,0 +1,121 @@ +// src/components/SheetSection.jsx +import React from "react"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; + +/** + * props: + * - title: string + * - entries: array + * - pulseId: number | null + * - onCycleStatus(entry): fn + * - onToggleTag(entry): fn + * - displayTag(entry): string + */ +export default function SheetSection({ + title, + entries, + pulseId, + onCycleStatus, + onToggleTag, + displayTag, +}) { + // --- helpers (lokal, weil sie rein UI sind) --- + const getRowBg = (status) => { + if (status === 1) return "rgba(255, 35, 35, 0.16)"; + if (status === 2) return "rgba(0, 190, 80, 0.16)"; + if (status === 3) return "rgba(140, 140, 140, 0.12)"; + return "rgba(255,255,255,0.06)"; + }; + + const getNameColor = (status) => { + if (status === 1) return "#ffb3b3"; + if (status === 2) return "#baf3c9"; + if (status === 3) return "rgba(233,216,166,0.78)"; + return stylesTokens.textMain; + }; + + const getStatusSymbol = (status) => { + if (status === 2) return "✓"; + if (status === 1) return "✕"; + if (status === 3) return "?"; + return "–"; + }; + + const getStatusBadge = (status) => { + if (status === 2) return { color: "#baf3c9", background: "rgba(0,190,80,0.18)" }; + if (status === 1) return { color: "#ffb3b3", background: "rgba(255,35,35,0.18)" }; + if (status === 3) + return { color: "rgba(233,216,166,0.85)", background: "rgba(140,140,140,0.14)" }; + return { color: "rgba(233,216,166,0.75)", background: "rgba(255,255,255,0.08)" }; + }; + + return ( +
+
{title}
+ +
+ {entries.map((e) => { + // UI "rot" wenn note_tag i/m/s (Backend s wird als s.XX angezeigt) + const isIorMorS = e.note_tag === "i" || e.note_tag === "m" || e.note_tag === "s"; + const effectiveStatus = e.status === 0 && isIorMorS ? 1 : e.status; + + const badge = getStatusBadge(effectiveStatus); + + return ( +
+
onCycleStatus(e)} + style={{ + ...styles.name, + textDecoration: effectiveStatus === 1 ? "line-through" : "none", + color: getNameColor(effectiveStatus), + opacity: effectiveStatus === 1 ? 0.8 : 1, + }} + title="Klick: Grün → Rot → Grau → Leer" + > + {e.label} +
+ +
+ + {getStatusSymbol(effectiveStatus)} + +
+ + +
+ ); + })} +
+
+ ); +}