From 1afb060bbcd20816c7197ffe0bb32ddcba9122df Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 4 Feb 2026 08:49:34 +0100 Subject: [PATCH 1/7] Refactor app by modularizing components and extracting utilities. The changes split large features into smaller, reusable components like `AdminPanel`, `LoginPage`, `TopBar`, `PasswordModal`, and `ChipModal`. Utility functions such as `cycleTag` and `chipStorage` were extracted for better organization. This improves the code's readability, maintainability, and scalability. --- frontend/src/App.jsx | 1272 +---------------- frontend/src/api/client.js | 11 + frontend/src/components/AdminPanel.jsx | 136 ++ frontend/src/components/ChipModal.jsx | 35 + frontend/src/components/LoginPage.jsx | 71 + frontend/src/components/PasswordModal.jsx | 66 + frontend/src/components/TopBar.jsx | 59 + frontend/src/constants.js | 2 + .../src/styles/hooks/useHpGlobalStyles.js | 84 ++ frontend/src/styles/styles.js | 490 +++++++ frontend/src/styles/theme.js | 11 + frontend/src/utils/chipStorage.js | 23 + frontend/src/utils/cycleTag.js | 11 + 13 files changed, 1056 insertions(+), 1215 deletions(-) create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/components/AdminPanel.jsx create mode 100644 frontend/src/components/ChipModal.jsx create mode 100644 frontend/src/components/LoginPage.jsx create mode 100644 frontend/src/components/PasswordModal.jsx create mode 100644 frontend/src/components/TopBar.jsx create mode 100644 frontend/src/constants.js create mode 100644 frontend/src/styles/hooks/useHpGlobalStyles.js create mode 100644 frontend/src/styles/styles.js create mode 100644 frontend/src/styles/theme.js create mode 100644 frontend/src/utils/chipStorage.js create mode 100644 frontend/src/utils/cycleTag.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5b9d8d9..07d7217 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,190 +1,21 @@ 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"; -const API = "/api"; -const CHIP_LIST = ["AL", "JG", "JN", "SN", "TL"]; +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 -async function api(path, opts = {}) { - const res = await fetch(API + path, { - credentials: "include", - headers: { "Content-Type": "application/json" }, - ...opts, - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -/** - * Backend erlaubt: null | "i" | "m" | "s" - * Rotation: - * null -> i -> m -> s (Popup) -> null - */ -function cycleTag(tag) { - if (!tag) return "i"; - if (tag === "i") return "m"; - if (tag === "m") return "s"; - return null; // "s" -> null -} - -/* ========= Chip localStorage (Frontend-only) ========= */ -function chipStorageKey(gameId, entryId) { - return `chip:${gameId}:${entryId}`; -} - -function getChipLS(gameId, entryId) { - try { - return localStorage.getItem(chipStorageKey(gameId, entryId)); - } catch { - return null; - } -} - -function setChipLS(gameId, entryId, chip) { - try { - localStorage.setItem(chipStorageKey(gameId, entryId), chip); - } catch {} -} - -function clearChipLS(gameId, entryId) { - try { - localStorage.removeItem(chipStorageKey(gameId, entryId)); - } catch {} -} - -/* ========= Admin Panel ========= */ -function AdminPanel() { - const [users, setUsers] = useState([]); - - const [open, setOpen] = useState(false); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [role, setRole] = useState("user"); - const [msg, setMsg] = useState(""); - - const loadUsers = async () => { - const u = await api("/admin/users"); - setUsers(u); - }; - - useEffect(() => { - loadUsers().catch(() => {}); - }, []); - - const resetForm = () => { - setEmail(""); - setPassword(""); - setRole("user"); - }; - - const createUser = async () => { - setMsg(""); - try { - await api("/admin/users", { - method: "POST", - body: JSON.stringify({ email, password, role }), - }); - setMsg("✅ User erstellt."); - await loadUsers(); - resetForm(); - setOpen(false); - } catch (e) { - setMsg("❌ Fehler: " + (e?.message || "unknown")); - } - }; - - const closeModal = () => { - setOpen(false); - setMsg(""); - }; - - return ( -
-
-
Admin Dashboard
- -
- -
- Vorhandene User -
- -
- {users.map((u) => ( -
-
{u.email}
-
- {u.role} -
-
- {u.disabled ? "disabled" : "active"} -
-
- ))} -
- - {open && ( -
-
e.stopPropagation()}> -
-
- Neuen User anlegen -
- -
- -
- setEmail(e.target.value)} - placeholder="Email" - style={styles.input} - autoFocus - /> - setPassword(e.target.value)} - placeholder="Initial Passwort" - type="password" - style={styles.input} - /> - - - {msg &&
{msg}
} - -
- - -
- -
- Tipp: Klick auf Item: Grün → Rot → Grau → Leer -
-
-
-
- )} -
- ); -} - -/* ========= App ========= */ export default function App() { + useHpGlobalStyles(); + const [me, setMe] = useState(null); const [loginEmail, setLoginEmail] = useState(""); const [loginPassword, setLoginPassword] = useState(""); @@ -195,13 +26,11 @@ export default function App() { const [sheet, setSheet] = useState(null); const [pulseId, setPulseId] = useState(null); - // Chip popup const [chipOpen, setChipOpen] = useState(false); const [chipEntry, setChipEntry] = useState(null); const [helpOpen, setHelpOpen] = useState(false); - // User dropdown + Passwort Modal const [userMenuOpen, setUserMenuOpen] = useState(false); const [pwOpen, setPwOpen] = useState(false); @@ -220,105 +49,21 @@ export default function App() { if (gs[0] && !gameId) setGameId(gs[0].id); }; - // Usermanager + // Dropdown outside click useEffect(() => { const onDown = (e) => { - // wenn Dropdown offen & Klick ist NICHT in einem Element mit data-user-menu const root = e.target?.closest?.("[data-user-menu]"); if (!root) setUserMenuOpen(false); }; - if (userMenuOpen) document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [userMenuOpen]); - // Google Fonts - useEffect(() => { - if (document.getElementById("hp-fonts")) return; - - const pre1 = document.createElement("link"); - pre1.id = "hp-fonts-pre1"; - pre1.rel = "preconnect"; - pre1.href = "https://fonts.googleapis.com"; - - const pre2 = document.createElement("link"); - pre2.id = "hp-fonts-pre2"; - pre2.rel = "preconnect"; - pre2.href = "https://fonts.gstatic.com"; - pre2.crossOrigin = "anonymous"; - - const link = document.createElement("link"); - link.id = "hp-fonts"; - link.rel = "stylesheet"; - link.href = - "https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700;900&family=IM+Fell+English:ital,wght@0,400;0,700;1,400&display=swap"; - - document.head.appendChild(pre1); - document.head.appendChild(pre2); - document.head.appendChild(link); - }, []); - - // Keyframes - useEffect(() => { - if (document.getElementById("hp-anim-style")) return; - const style = document.createElement("style"); - style.id = "hp-anim-style"; - style.innerHTML = ` - @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } - @keyframes popIn { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } - @keyframes rowPulse { 0%{ transform: scale(1); } 50%{ transform: scale(1.01); } 100%{ transform: scale(1); } } - @keyframes candleGlow { - 0% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); } - 35% { opacity: .85; transform: translateY(-2px) scale(1.02); filter: blur(18px); } - 70% { opacity: .62; transform: translateY(1px) scale(1.01); filter: blur(17px); } - 100% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); } - } - `; - document.head.appendChild(style); - }, []); - - // html/body reset - useEffect(() => { - document.documentElement.style.height = "100%"; - document.body.style.height = "100%"; - document.documentElement.style.margin = "0"; - document.body.style.margin = "0"; - document.documentElement.style.padding = "0"; - document.body.style.padding = "0"; - }, []); - - // Global CSS - useEffect(() => { - if (document.getElementById("hp-global-style")) return; - const style = document.createElement("style"); - style.id = "hp-global-style"; - style.innerHTML = ` - html, body { - overscroll-behavior-y: none; - -webkit-text-size-adjust: 100%; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background: ${stylesTokens.pageBg}; - color: ${stylesTokens.textMain}; - } - body { - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - } - #root { background: transparent; } - * { -webkit-tap-highlight-color: transparent; } - `; - document.head.appendChild(style); - }, []); - useEffect(() => { (async () => { try { await load(); - } catch { - // not logged in - } + } catch {} })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -329,9 +74,7 @@ export default function App() { try { const sh = await api(`/games/${gameId}/sheet`); setSheet(sh); - } catch { - // ignore - } + } catch {} })(); }, [gameId]); @@ -351,7 +94,7 @@ export default function App() { setSheet(null); }; - // ===== Password change ===== + // Password change const openPwModal = () => { setPwMsg(""); setPw1(""); @@ -369,15 +112,8 @@ export default function App() { const savePassword = async () => { setPwMsg(""); - - if (!pw1 || pw1.length < 8) { - setPwMsg("❌ Passwort muss mindestens 8 Zeichen haben."); - return; - } - if (pw1 !== pw2) { - setPwMsg("❌ Passwörter stimmen nicht überein."); - return; - } + 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 { @@ -385,12 +121,9 @@ export default function App() { method: "PATCH", body: JSON.stringify({ password: pw1 }), }); - setPwMsg("✅ Passwort gespeichert."); - // Optional: nach kurzer Zeit automatisch schließen setTimeout(() => closePwModal(), 650); } catch (e) { - // dein api() wirft res.text() -> oft JSON Detail. Wir zeigen es einfach roh. setPwMsg("❌ Fehler: " + (e?.message || "unknown")); } finally { setPwSaving(false); @@ -430,21 +163,16 @@ export default function App() { setTimeout(() => setPulseId(null), 220); }; - // Notiz-Button: i -> m -> (Popup) s -> null const toggleTag = async (entry) => { const next = cycleTag(entry.note_tag); - // Wenn wir zu "s" gehen würden -> Chip Popup öffnen, aber NICHT ins Backend schreiben if (next === "s") { setChipEntry(entry); setChipOpen(true); return; } - // Wenn wir auf null gehen, Chip lokal löschen (weil s -> —) - if (next === null) { - clearChipLS(gameId, entry.entry_id); - } + if (next === null) clearChipLS(gameId, entry.entry_id); await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", @@ -453,22 +181,16 @@ export default function App() { await reloadSheet(); }; - // Chip wählen: - // Backend: note_tag = "s" - // Frontend: Chip in localStorage const chooseChip = async (chip) => { if (!chipEntry) return; - // UI sofort schließen -> fühlt sich besser an const entry = chipEntry; setChipOpen(false); setChipEntry(null); - // local speichern setChipLS(gameId, entry.entry_id, chip); try { - // Backend bekommt nur "s" await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: "s" }), @@ -478,24 +200,19 @@ export default function App() { } }; - // X im Modal: - // Backend zurück auf null und lokalen Chip löschen const closeChipModalToDash = async () => { if (!chipEntry) { setChipOpen(false); return; } - // UI sofort schließen const entry = chipEntry; setChipOpen(false); setChipEntry(null); - // Frontend-only Chip entfernen clearChipLS(gameId, entry.entry_id); try { - // Backend zurück auf — await api(`/games/${gameId}/sheet/${entry.entry_id}`, { method: "PATCH", body: JSON.stringify({ note_tag: null }), @@ -505,115 +222,31 @@ export default function App() { } }; - // Anzeige im Tag-Button: - // - "s" wird zu "s.AL" (aus localStorage), sonst "s" 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"; // <-- genau wie gewünscht + return chip ? `s.${chip}` : "s"; } - - return t; // i oder m + return t; }; - // --- helpers --- - 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)" }; - }; - - const closeHelp = () => setHelpOpen(false); - - // ===== Login ===== + // ===== Login page ===== if (!me) { return ( -
-
-
-
-
{me.email}
-
{me.role}
-
- -
-
- - - {userMenuOpen && ( -
- - -
- - -
- )} -
- - -
-
+ {me.role === "admin" && } -
-
-
Spiel
-
- - - -
-
-
- - {helpOpen && ( -
-
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. -
-
-
-
- )} - - {pwOpen && ( -
-
e.stopPropagation()}> -
-
Passwort setzen
- -
- -
- setPw1(e.target.value)} - placeholder="Neues Passwort" - type="password" - style={styles.input} - autoFocus - /> - setPw2(e.target.value)} - placeholder="Neues Passwort wiederholen" - type="password" - style={styles.input} - /> - - {pwMsg &&
{pwMsg}
} - -
- - -
- -
- Hinweis: Mindestens 8 Zeichen empfohlen. -
-
-
-
- )} - - {/* Chip Popup */} - {chipOpen && ( -
-
e.stopPropagation()}> -
-
Wer hat die Karte?
- -
- -
Chip auswählen:
- -
- {CHIP_LIST.map((c) => ( - - ))} -
- -
- Tipp: Wenn du wieder auf den Notiz-Button klickst, geht’s von s.XX zurück auf —. -
-
-
- )} - -
- {sections.map((sec) => ( -
-
{sec.title}
- -
- {sec.entries.map((e) => { - // UI "rot" wenn note_tag i oder 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 ( -
-
cycleStatus(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)} - -
- - -
- ); - })} -
-
- ))} -
- -
+ {/* hier bleiben vorerst: Spiel selector + Help + Sections + -> kannst du als nächsten Schritt in Komponenten auslagern */}
+ + + +
); } - -/* ===== Theme Tokens ===== */ -const stylesTokens = { - pageBg: "#0b0b0c", - panelBg: "rgba(20, 20, 22, 0.55)", - panelBorder: "rgba(233, 216, 166, 0.14)", - - textMain: "rgba(245, 239, 220, 0.92)", - textDim: "rgba(233, 216, 166, 0.70)", - textGold: "#e9d8a6", - - goldLine: "rgba(233, 216, 166, 0.18)", -}; - -/* ===== Styles ===== */ -const styles = { - page: { - minHeight: "100dvh", - background: "transparent", - position: "relative", - zIndex: 1, - }, - - shell: { - fontFamily: '"IM Fell English", system-ui', - padding: 16, - maxWidth: 680, - margin: "0 auto", - }, - - topBar: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - gap: 10, - padding: 12, - borderRadius: 16, - background: stylesTokens.panelBg, - border: `1px solid ${stylesTokens.panelBorder}`, - boxShadow: "0 12px 30px rgba(0,0,0,0.45)", - backdropFilter: "blur(6px)", - }, - - card: { - borderRadius: 18, - overflow: "hidden", - border: `1px solid ${stylesTokens.panelBorder}`, - background: "rgba(18, 18, 20, 0.50)", - boxShadow: "0 18px 40px rgba(0,0,0,0.50), inset 0 1px 0 rgba(255,255,255,0.06)", - }, - - cardBody: { - padding: 12, - display: "flex", - gap: 10, - alignItems: "center", - }, - - sectionHeader: { - padding: "11px 14px", - fontWeight: 1000, - fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui', - letterSpacing: 1.0, - color: stylesTokens.textGold, - background: "linear-gradient(180deg, rgba(32,32,36,0.92), rgba(14,14,16,0.92))", - borderBottom: `1px solid ${stylesTokens.goldLine}`, - textTransform: "uppercase", - textShadow: "0 1px 0 rgba(0,0,0,0.6)", - }, - - row: { - display: "grid", - gridTemplateColumns: "1fr 54px 68px", - gap: 10, - padding: "12px 14px", - alignItems: "center", - borderBottom: "1px solid rgba(233,216,166,0.08)", - borderLeft: "4px solid rgba(0,0,0,0)", - }, - - name: { - cursor: "pointer", - userSelect: "none", - fontWeight: 800, - letterSpacing: 0.2, - color: stylesTokens.textMain, - }, - - statusCell: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - - statusBadge: { - width: 34, - height: 34, - display: "inline-flex", - justifyContent: "center", - alignItems: "center", - borderRadius: 999, - border: `1px solid rgba(233,216,166,0.18)`, - fontWeight: 1100, - fontSize: 16, - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)", - }, - - tagBtn: { - padding: "8px 0", - fontWeight: 1000, - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(255,255,255,0.06)", - color: stylesTokens.textGold, - cursor: "pointer", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)", - }, - - helpBtn: { - padding: "10px 12px", - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(255,255,255,0.06)", - color: stylesTokens.textGold, - fontWeight: 1000, - cursor: "pointer", - whiteSpace: "nowrap", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)", - }, - - input: { - width: "100%", - padding: 10, - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(10,10,12,0.55)", - color: stylesTokens.textMain, - outline: "none", - fontSize: 16, - }, - - primaryBtn: { - padding: "10px 12px", - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.28)`, - background: "linear-gradient(180deg, rgba(233,216,166,0.24), rgba(233,216,166,0.10))", - color: stylesTokens.textGold, - fontWeight: 1000, - cursor: "pointer", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)", - }, - - secondaryBtn: { - padding: "10px 12px", - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(255,255,255,0.05)", - color: stylesTokens.textMain, - fontWeight: 900, - cursor: "pointer", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)", - }, - - // Admin - adminWrap: { - marginTop: 14, - padding: 12, - borderRadius: 16, - border: `1px solid rgba(233,216,166,0.14)`, - background: "rgba(18, 18, 20, 0.40)", - boxShadow: "0 12px 30px rgba(0,0,0,0.45)", - backdropFilter: "blur(6px)", - }, - adminTop: { - display: "flex", - alignItems: "center", - justifyContent: "space-between", - gap: 10, - }, - adminTitle: { - fontWeight: 1000, - color: stylesTokens.textGold, - }, - userRow: { - display: "grid", - gridTemplateColumns: "1fr 80px 90px", - gap: 8, - padding: 10, - borderRadius: 12, - background: "rgba(255,255,255,0.06)", - border: `1px solid rgba(233,216,166,0.10)`, - }, - - // Modal - modalOverlay: { - position: "fixed", - inset: 0, - background: "rgba(0,0,0,0.65)", - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: 16, - zIndex: 9999, - animation: "fadeIn 160ms ease-out", - }, - modalCard: { - width: "100%", - maxWidth: 560, - borderRadius: 18, - border: `1px solid rgba(233,216,166,0.18)`, - background: "linear-gradient(180deg, rgba(20,20,24,0.92), rgba(12,12,14,0.86))", - boxShadow: "0 18px 55px rgba(0,0,0,0.70)", - padding: 14, - backdropFilter: "blur(6px)", - animation: "popIn 160ms ease-out", - color: stylesTokens.textMain, - }, - modalHeader: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - gap: 10, - }, - modalCloseBtn: { - width: 38, - height: 38, - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(255,255,255,0.06)", - color: stylesTokens.textGold, - fontWeight: 1000, - cursor: "pointer", - lineHeight: "38px", - textAlign: "center", - }, - - // Help - helpBody: { - marginTop: 10, - paddingTop: 4, - maxHeight: "70vh", - overflow: "auto", - }, - helpSectionTitle: { - fontWeight: 1000, - color: stylesTokens.textGold, - marginTop: 10, - marginBottom: 6, - }, - helpText: { - color: stylesTokens.textMain, - opacity: 0.92, - lineHeight: 1.35, - }, - helpList: { - marginTop: 10, - display: "grid", - gap: 8, - }, - helpListRow: { - display: "grid", - gridTemplateColumns: "42px 1fr", - gap: 10, - alignItems: "center", - color: stylesTokens.textMain, - }, - helpBadge: { - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - width: 38, - height: 38, - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - fontWeight: 1100, - fontSize: 18, - }, - helpMiniTag: { - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - width: 38, - height: 38, - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(255,255,255,0.06)", - color: stylesTokens.textGold, - fontWeight: 1100, - }, - helpDivider: { - margin: "14px 0", - height: 1, - background: "rgba(233,216,166,0.12)", - }, - - // Login - loginPage: { - minHeight: "100dvh", - display: "flex", - alignItems: "center", - justifyContent: "center", - position: "relative", - overflow: "hidden", - padding: 20, - background: "transparent", - zIndex: 1, - }, - loginCard: { - width: "100%", - maxWidth: 420, - padding: 26, - borderRadius: 22, - position: "relative", - zIndex: 2, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(18, 18, 20, 0.55)", - boxShadow: "0 18px 60px rgba(0,0,0,0.70)", - backdropFilter: "blur(8px)", - animation: "popIn 240ms ease-out", - color: stylesTokens.textMain, - }, - loginTitle: { - fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui', - fontWeight: 1000, - fontSize: 26, - color: stylesTokens.textGold, - textAlign: "center", - letterSpacing: 0.6, - }, - loginSubtitle: { - marginTop: 6, - textAlign: "center", - color: stylesTokens.textMain, - opacity: 0.9, - fontSize: 15, - lineHeight: 1.4, - }, - loginFieldWrap: { - width: "100%", - display: "flex", - justifyContent: "center", - }, - loginInput: { - width: "100%", - padding: 10, - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(10,10,12,0.60)", - color: stylesTokens.textMain, - outline: "none", - fontSize: 16, - }, - loginBtn: { - padding: "12px 14px", - borderRadius: 14, - border: `1px solid rgba(233,216,166,0.28)`, - background: "linear-gradient(180deg, rgba(233,216,166,0.24), rgba(233,216,166,0.10))", - color: stylesTokens.textGold, - fontWeight: 1000, - fontSize: 16, - cursor: "pointer", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)", - }, - loginHint: { - marginTop: 18, - fontSize: 13, - opacity: 0.78, - textAlign: "center", - color: stylesTokens.textDim, - lineHeight: 1.35, - }, - - candleGlowLayer: { - position: "absolute", - inset: 0, - pointerEvents: "none", - background: ` - radial-gradient(circle at 20% 25%, rgba(255, 200, 120, 0.16), rgba(0,0,0,0) 40%), - radial-gradient(circle at 80% 30%, rgba(255, 210, 140, 0.12), rgba(0,0,0,0) 42%), - radial-gradient(circle at 55% 75%, rgba(255, 180, 100, 0.08), rgba(0,0,0,0) 45%) - `, - animation: "candleGlow 3.8s ease-in-out infinite", - mixBlendMode: "multiply", - }, - - inputRow: { - display: "flex", - alignItems: "stretch", - width: "100%", - }, - inputInRow: { - flex: 1, - padding: 10, - borderRadius: "12px 0 0 12px", - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(10,10,12,0.60)", - color: stylesTokens.textMain, - outline: "none", - minWidth: 0, - fontSize: 16, - }, - pwToggleBtn: { - width: 48, - display: "flex", - alignItems: "center", - justifyContent: "center", - borderRadius: "0 12px 12px 0", - border: `1px solid rgba(233,216,166,0.18)`, - borderLeft: "none", - background: "rgba(255,255,255,0.06)", - color: stylesTokens.textGold, - cursor: "pointer", - fontWeight: 900, - padding: 0, - }, - - // Background - bgFixed: { - position: "fixed", - top: 0, - left: 0, - width: "100vw", - height: "100dvh", - zIndex: -1, - pointerEvents: "none", - transform: "translateZ(0)", - backfaceVisibility: "hidden", - willChange: "transform", - }, - - bgMap: { - position: "absolute", - inset: 0, - backgroundImage: 'url("/bg/marauders-map-blur.jpg")', - backgroundSize: "cover", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - filter: "saturate(0.9) contrast(1.05) brightness(0.55)", - }, - - chipGrid: { - marginTop: 12, - display: "grid", - gridTemplateColumns: "repeat(5, minmax(0, 1fr))", - gap: 8, - }, - - chipBtn: { - padding: "10px 14px", - borderRadius: 12, - border: "1px solid rgba(233,216,166,0.18)", - background: "rgba(255,255,255,0.06)", - color: stylesTokens.textGold, - fontWeight: 1000, - cursor: "pointer", - minWidth: 64, - }, - - userBtn: { - display: "inline-flex", - alignItems: "center", - gap: 8, - padding: "10px 12px", - borderRadius: 12, - border: `1px solid rgba(233,216,166,0.18)`, - background: "rgba(255,255,255,0.05)", - color: stylesTokens.textMain, - fontWeight: 900, - cursor: "pointer", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)", - maxWidth: 180, - }, - - userDropdown: { - position: "absolute", - right: 0, - top: "calc(100% + 8px)", - minWidth: 220, - borderRadius: 14, - border: `1px solid rgba(233,216,166,0.18)`, - background: "linear-gradient(180deg, rgba(20,20,24,0.96), rgba(12,12,14,0.92))", - boxShadow: "0 18px 55px rgba(0,0,0,0.70)", - overflow: "hidden", - zIndex: 10000, - backdropFilter: "blur(8px)", - }, - - userDropdownItem: { - width: "100%", - textAlign: "left", - padding: "10px 12px", - border: "none", - background: "transparent", - color: stylesTokens.textMain, - fontWeight: 900, - cursor: "pointer", - }, - - userDropdownDivider: { - height: 1, - background: "rgba(233,216,166,0.12)", - }, - -}; diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..7926117 --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,11 @@ +import { API_BASE } from "../constants"; + +export async function api(path, opts = {}) { + const res = await fetch(API_BASE + path, { + credentials: "include", + headers: { "Content-Type": "application/json" }, + ...opts, + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} \ No newline at end of file diff --git a/frontend/src/components/AdminPanel.jsx b/frontend/src/components/AdminPanel.jsx new file mode 100644 index 0000000..a49ac46 --- /dev/null +++ b/frontend/src/components/AdminPanel.jsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from "react"; +import { api } from "../api/client"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; + +export default function AdminPanel() { + const [users, setUsers] = useState([]); + + const [open, setOpen] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [role, setRole] = useState("user"); + const [msg, setMsg] = useState(""); + + const loadUsers = async () => { + const u = await api("/admin/users"); + setUsers(u); + }; + + useEffect(() => { + loadUsers().catch(() => {}); + }, []); + + const resetForm = () => { + setEmail(""); + setPassword(""); + setRole("user"); + }; + + const createUser = async () => { + setMsg(""); + try { + await api("/admin/users", { + method: "POST", + body: JSON.stringify({ email, password, role }), + }); + setMsg("✅ User erstellt."); + await loadUsers(); + resetForm(); + setOpen(false); + } catch (e) { + setMsg("❌ Fehler: " + (e?.message || "unknown")); + } + }; + + const closeModal = () => { + setOpen(false); + setMsg(""); + }; + + return ( +
+
+
Admin Dashboard
+ +
+ +
+ Vorhandene User +
+ +
+ {users.map((u) => ( +
+
{u.email}
+
+ {u.role} +
+
+ {u.disabled ? "disabled" : "active"} +
+
+ ))} +
+ + {open && ( +
+
e.stopPropagation()}> +
+
+ Neuen User anlegen +
+ +
+ +
+ setEmail(e.target.value)} + placeholder="Email" + style={styles.input} + autoFocus + /> + setPassword(e.target.value)} + placeholder="Initial Passwort" + type="password" + style={styles.input} + /> + + + {msg &&
{msg}
} + +
+ + +
+ +
+ Tipp: Klick auf Item: Grün → Rot → Grau → Leer +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ChipModal.jsx b/frontend/src/components/ChipModal.jsx new file mode 100644 index 0000000..3f40502 --- /dev/null +++ b/frontend/src/components/ChipModal.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; +import { CHIP_LIST } from "../constants"; + +export default function ChipModal({ chipOpen, closeChipModalToDash, chooseChip }) { + if (!chipOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+
Wer hat die Karte?
+ +
+ +
Chip auswählen:
+ +
+ {CHIP_LIST.map((c) => ( + + ))} +
+ +
+ Tipp: Wenn du wieder auf den Notiz-Button klickst, geht’s von s zurück auf —. +
+
+
+ ); +} diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx new file mode 100644 index 0000000..1c75807 --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,71 @@ +import React from "react"; +import { styles } from "../styles/styles"; + +export default function LoginPage({ + loginEmail, + setLoginEmail, + loginPassword, + setLoginPassword, + showPw, + setShowPw, + doLogin, +}) { + return ( +
+ -- 2.49.1 From a3216950c8d46b084705896a2d9143d45d5a0d57 Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 4 Feb 2026 09:06:17 +0100 Subject: [PATCH 4/7] Update TopBar UI for improved structure and clarity Refactored the TopBar component to enhance readability and organization. Added labeled sections for user account, role display, and the "Neues Spiel" button. Adjusted styles and improved spacing for better UI consistency. --- frontend/src/components/TopBar.jsx | 51 ++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 8cb0c6e..2b43917 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -12,25 +12,60 @@ export default function TopBar({ }) { return (
+ {/* LINKS: nur Rolle */}
-
{me.email}
-
{me.role}
+
+ Zauber-Notizbogen +
+
+ Eingeloggt als {me.role} +
-
+ {/* RECHTS: Account + Neues Spiel */} +
+ {/* Account Dropdown */}
{userMenuOpen && (
+ {/* Email Info */} +
+ {me.email} +
+ + {/* Actions */} @@ -42,7 +77,10 @@ export default function TopBar({ setUserMenuOpen(false); doLogout(); }} - style={{ ...styles.userDropdownItem, color: "#ffb3b3" }} + style={{ + ...styles.userDropdownItem, + color: "#ffb3b3", + }} > Logout @@ -50,6 +88,7 @@ export default function TopBar({ )}
+ {/* Neues Spiel Button */} -- 2.49.1 From c6a56b14ffe4d137ec592b3d25a6cbd30800330e Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 5 Feb 2026 09:08:15 +0100 Subject: [PATCH 5/7] Update TopBar labels for clarity and relevance Replaced "Zauber-Notizbogen" with "Notizbogen" to simplify terminology. Displayed user email instead of role for a more meaningful and personalized experience. --- frontend/src/components/TopBar.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 2b43917..8cc7622 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -15,7 +15,7 @@ export default function TopBar({ {/* LINKS: nur Rolle */}
- Zauber-Notizbogen + Notizbogen
- Eingeloggt als {me.role} + {me.email}
-- 2.49.1 From 1f326c2b46d9deab0552dd104c27c6ef97d61ba1 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 5 Feb 2026 09:10:11 +0100 Subject: [PATCH 6/7] Update label from "Account" to "User" in TopBar The label on the user menu button was changed to improve readability and consistency with the UI language. This ensures better comprehension for users interacting with the application. --- frontend/src/components/TopBar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 8cc7622..bc07d3e 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -46,7 +46,7 @@ export default function TopBar({ title="User Menü" > 👤 - Account + User -- 2.49.1 From 198175c0fe4613272a71fe2cf2f627e71f2c611c Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 5 Feb 2026 09:15:19 +0100 Subject: [PATCH 7/7] Rename "Neues Spiel" button text to "New Game" Updated the button label in the TopBar component to display "New Game" instead of "Neues Spiel." This change aligns the interface language with English for consistency. --- frontend/src/components/TopBar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index bc07d3e..dfdbbd3 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -90,7 +90,7 @@ export default function TopBar({ {/* Neues Spiel Button */}
-- 2.49.1