diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4f56f65..9581f76 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,9 +4,13 @@ import React, { useEffect, useState } from "react"; import { api } from "./api/client"; import { cycleTag } from "./utils/cycleTag"; import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage"; +import { getWinner, setWinner as saveWinnerLS } from "./utils/winnerStorage"; + import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles"; import { styles } from "./styles/styles"; +import { applyTheme, loadThemeKey, saveThemeKey, DEFAULT_THEME_KEY } from "./styles/themes"; + import AdminPanel from "./components/AdminPanel"; import LoginPage from "./components/LoginPage"; import TopBar from "./components/TopBar"; @@ -15,6 +19,8 @@ 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"; export default function App() { useHpGlobalStyles(); @@ -31,6 +37,9 @@ export default function App() { const [sheet, setSheet] = useState(null); const [pulseId, setPulseId] = useState(null); + // Winner (per game) + const [winnerName, setWinnerName] = useState(""); + // Modals const [helpOpen, setHelpOpen] = useState(false); @@ -45,11 +54,20 @@ export default function App() { const [pwMsg, setPwMsg] = useState(""); const [pwSaving, setPwSaving] = useState(false); + // Theme + const [designOpen, setDesignOpen] = useState(false); + const [themeKey, setThemeKey] = useState(DEFAULT_THEME_KEY); + // ===== Data loaders ===== const load = async () => { const m = await api("/auth/me"); setMe(m); + // Theme pro User laden & anwenden + const tk = loadThemeKey(m?.email); + setThemeKey(tk); + applyTheme(tk); + const gs = await api("/games"); setGames(gs); @@ -62,8 +80,6 @@ export default function App() { setSheet(sh); }; - // ===== Effects ===== - // Dropdown outside click useEffect(() => { const onDown = (e) => { @@ -92,9 +108,9 @@ export default function App() { if (!gameId) return; try { await reloadSheet(); - } catch { - // ignore - } + } catch {} + // Winner pro Game aus LS laden + setWinnerName(getWinner(gameId)); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameId]); @@ -114,6 +130,7 @@ export default function App() { setGames([]); setGameId(null); setSheet(null); + setWinnerName(""); }; // ===== Password change ===== @@ -153,6 +170,18 @@ export default function App() { } }; + // ===== Theme actions ===== + const openDesignModal = () => { + setDesignOpen(true); + setUserMenuOpen(false); + }; + + const selectTheme = (key) => { + setThemeKey(key); + applyTheme(key); + saveThemeKey(me?.email, key); + }; + // ===== Game actions ===== const newGame = async () => { const g = await api("/games", { @@ -164,6 +193,12 @@ export default function App() { setGameId(g.id); }; + // ===== Winner actions ===== + const saveWinner = () => { + if (!gameId) return; + saveWinnerLS(gameId, winnerName.trim()); + }; + // ===== Sheet actions ===== const cycleStatus = async (entry) => { let next = 0; @@ -185,14 +220,12 @@ 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}`, { @@ -210,11 +243,9 @@ 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" }), @@ -253,7 +284,7 @@ export default function App() { const chip = getChipLS(gameId, entry.entry_id); return chip ? `s.${chip}` : "s"; } - return t; // i oder m + return t; }; // ===== Login page ===== @@ -291,6 +322,7 @@ export default function App() { userMenuOpen={userMenuOpen} setUserMenuOpen={setUserMenuOpen} openPwModal={openPwModal} + openDesignModal={openDesignModal} doLogout={doLogout} newGame={newGame} /> @@ -320,6 +352,9 @@ export default function App() { ))} + {/* Sieger ganz unten */} + +
@@ -335,11 +370,17 @@ export default function App() { savePassword={savePassword} /> - setDesignOpen(false)} + themeKey={themeKey} + onSelect={(k) => { + selectTheme(k); + setDesignOpen(false); + }} /> + + ); } diff --git a/frontend/src/components/DesignModal.jsx b/frontend/src/components/DesignModal.jsx new file mode 100644 index 0000000..84a2ca9 --- /dev/null +++ b/frontend/src/components/DesignModal.jsx @@ -0,0 +1,57 @@ +import React from "react"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; +import { THEMES } from "../styles/themes"; + +export default function DesignModal({ open, onClose, themeKey, onSelect }) { + if (!open) return null; + + const themeEntries = Object.entries(THEMES); + + return ( +
+
e.stopPropagation()}> +
+
Design ändern
+ +
+ +
+ Wähle dein Theme: +
+ +
+ {themeEntries.map(([key, t]) => { + const active = key === themeKey; + + return ( + + ); + })} +
+ +
+ Hinweis: Das Design wird pro User gespeichert (Email). +
+
+
+ ); +} diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index dfdbbd3..7a74545 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -7,44 +7,20 @@ export default function TopBar({ userMenuOpen, setUserMenuOpen, openPwModal, + openDesignModal, doLogout, newGame, }) { return (
- {/* LINKS: nur Rolle */}
-
- Notizbogen -
-
- {me.email} -
+
Notizbogen
+
{me.email}
- {/* RECHTS: Account + Neues Spiel */} -
- {/* Account Dropdown */} +
- + +
@@ -88,7 +63,6 @@ export default function TopBar({ )}
- {/* Neues Spiel Button */} diff --git a/frontend/src/components/WinnerCard.jsx b/frontend/src/components/WinnerCard.jsx new file mode 100644 index 0000000..e365d0b --- /dev/null +++ b/frontend/src/components/WinnerCard.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; + +export default function WinnerCard({ value, setValue, onSave }) { + return ( +
+
+
Sieger
+ +
+ setValue(e.target.value)} + placeholder="Name des Siegers" + style={{ ...styles.input, flex: 1 }} + onKeyDown={(e) => { + if (e.key === "Enter") onSave(); + }} + /> + + +
+ +
+ Wird pro Spiel lokal gespeichert. +
+
+
+ ); +} diff --git a/frontend/src/styles/hooks/useHpGlobalStyles.js b/frontend/src/styles/hooks/useHpGlobalStyles.js index 85edad3..90fa927 100644 --- a/frontend/src/styles/hooks/useHpGlobalStyles.js +++ b/frontend/src/styles/hooks/useHpGlobalStyles.js @@ -1,5 +1,8 @@ import { useEffect } from "react"; import { stylesTokens } from "../theme"; +import { applyTheme } from "../themes"; + + export function useHpGlobalStyles() { // Google Fonts @@ -81,4 +84,9 @@ export function useHpGlobalStyles() { `; document.head.appendChild(style); }, []); + + // Ensure a theme is applied once (fallback) + useEffect(() => { + applyTheme("default"); + }, []); } \ No newline at end of file diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js index 60c18ad..50dd625 100644 --- a/frontend/src/styles/theme.js +++ b/frontend/src/styles/theme.js @@ -1,11 +1,11 @@ export const stylesTokens = { - pageBg: "#0b0b0c", - panelBg: "rgba(20, 20, 22, 0.55)", - panelBorder: "rgba(233, 216, 166, 0.14)", + pageBg: "var(--hp-pageBg)", + panelBg: "var(--hp-panelBg)", + panelBorder: "var(--hp-panelBorder)", - textMain: "rgba(245, 239, 220, 0.92)", - textDim: "rgba(233, 216, 166, 0.70)", - textGold: "#e9d8a6", + textMain: "var(--hp-textMain)", + textDim: "var(--hp-textDim)", + textGold: "var(--hp-textGold)", - goldLine: "rgba(233, 216, 166, 0.18)", - }; \ No newline at end of file + goldLine: "var(--hp-goldLine)", + }; \ No newline at end of file diff --git a/frontend/src/styles/themes.js b/frontend/src/styles/themes.js new file mode 100644 index 0000000..2350018 --- /dev/null +++ b/frontend/src/styles/themes.js @@ -0,0 +1,106 @@ +// frontend/src/styles/themes.js +export const THEMES = { + default: { + label: "Standard", + tokens: { + 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)", + }, + }, + + gryffindor: { + label: "Gryffindor", + tokens: { + pageBg: "#0b0b0c", + panelBg: "rgba(20, 14, 14, 0.58)", + panelBorder: "rgba(255, 190, 120, 0.16)", + + textMain: "rgba(245, 239, 220, 0.92)", + textDim: "rgba(255, 210, 170, 0.70)", + textGold: "#ffb86b", + + goldLine: "rgba(255, 184, 107, 0.18)", + }, + }, + + slytherin: { + label: "Slytherin", + tokens: { + pageBg: "#070a09", + panelBg: "rgba(12, 20, 16, 0.58)", + panelBorder: "rgba(120, 255, 190, 0.12)", + + textMain: "rgba(235, 245, 240, 0.92)", + textDim: "rgba(175, 240, 210, 0.70)", + textGold: "#7CFFB6", + + goldLine: "rgba(124, 255, 182, 0.18)", + }, + }, + + ravenclaw: { + label: "Ravenclaw", + tokens: { + pageBg: "#07080c", + panelBg: "rgba(14, 16, 24, 0.60)", + panelBorder: "rgba(140, 180, 255, 0.14)", + + textMain: "rgba(235, 240, 250, 0.92)", + textDim: "rgba(180, 205, 255, 0.72)", + textGold: "#8FB6FF", + + goldLine: "rgba(143, 182, 255, 0.18)", + }, + }, + + hufflepuff: { + label: "Hufflepuff", + tokens: { + pageBg: "#0b0b0c", + panelBg: "rgba(18, 18, 14, 0.60)", + panelBorder: "rgba(255, 230, 120, 0.16)", + + textMain: "rgba(245, 239, 220, 0.92)", + textDim: "rgba(255, 240, 180, 0.70)", + textGold: "#FFE27A", + + goldLine: "rgba(255, 226, 122, 0.18)", + }, + }, +}; + +export const DEFAULT_THEME_KEY = "default"; + +export function applyTheme(themeKey) { + const t = THEMES[themeKey] || THEMES[DEFAULT_THEME_KEY]; + const root = document.documentElement; + + for (const [k, v] of Object.entries(t.tokens)) { + root.style.setProperty(`--hp-${k}`, v); + } +} + +export function themeStorageKey(email) { + return `hpTheme:${(email || "guest").toLowerCase()}`; +} + +export function loadThemeKey(email) { + try { + return localStorage.getItem(themeStorageKey(email)) || DEFAULT_THEME_KEY; + } catch { + return DEFAULT_THEME_KEY; + } +} + +export function saveThemeKey(email, key) { + try { + localStorage.setItem(themeStorageKey(email), key); + } catch {} +} diff --git a/frontend/src/utils/winnerStorage.js b/frontend/src/utils/winnerStorage.js new file mode 100644 index 0000000..7b65297 --- /dev/null +++ b/frontend/src/utils/winnerStorage.js @@ -0,0 +1,20 @@ +function winnerKey(gameId) { + return `winner:${gameId}`; + } + + export function getWinner(gameId) { + if (!gameId) return ""; + try { + return localStorage.getItem(winnerKey(gameId)) || ""; + } catch { + return ""; + } + } + + export function setWinner(gameId, name) { + if (!gameId) return; + try { + localStorage.setItem(winnerKey(gameId), name || ""); + } catch {} + } + \ No newline at end of file