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)} + +
+ + +
+ ); + })} +
+
+ ); +}