diff --git a/frontend/public/bg/gryffindor.png b/frontend/public/bg/gryffindor.png new file mode 100644 index 0000000..9b34434 Binary files /dev/null and b/frontend/public/bg/gryffindor.png differ diff --git a/frontend/public/bg/hufflepuff.png b/frontend/public/bg/hufflepuff.png new file mode 100644 index 0000000..e9a84ee Binary files /dev/null and b/frontend/public/bg/hufflepuff.png differ diff --git a/frontend/public/bg/ravenclaw.png b/frontend/public/bg/ravenclaw.png new file mode 100644 index 0000000..b6f2d70 Binary files /dev/null and b/frontend/public/bg/ravenclaw.png differ diff --git a/frontend/public/bg/slytherin.png b/frontend/public/bg/slytherin.png new file mode 100644 index 0000000..949e3de Binary files /dev/null and b/frontend/public/bg/slytherin.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4f56f65..e637ae2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,9 +4,14 @@ import React, { useEffect, useState } from "react"; import { api } from "./api/client"; import { cycleTag } from "./utils/cycleTag"; import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage"; + +import { getWinnerLS, setWinnerLS, clearWinnerLS } 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 +20,9 @@ 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"; export default function App() { useHpGlobalStyles(); @@ -31,6 +39,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 +56,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); @@ -86,15 +106,19 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // load sheet when game changes + // load sheet + winner when game changes useEffect(() => { (async () => { if (!gameId) return; + try { await reloadSheet(); } catch { // ignore } + + // Sieger pro Game aus localStorage laden + setWinnerName(getWinnerLS(gameId)); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameId]); @@ -114,6 +138,7 @@ export default function App() { setGames([]); setGameId(null); setSheet(null); + setWinnerName(""); }; // ===== Password change ===== @@ -153,15 +178,47 @@ 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", { method: "POST", body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }), }); + const gs = await api("/games"); setGames(gs); setGameId(g.id); + + // Neues Spiel -> Sieger leer + clearWinnerLS(g.id); + setWinnerName(""); + }; + + // ===== Winner actions ===== + const saveWinner = () => { + if (!gameId) return; + const v = (winnerName || "").trim(); + + if (!v) { + clearWinnerLS(gameId); + setWinnerName(""); + return; + } + + setWinnerLS(gameId, v); + setWinnerName(v); }; // ===== Sheet actions ===== @@ -185,14 +242,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 +265,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 +306,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 +344,7 @@ export default function App() { userMenuOpen={userMenuOpen} setUserMenuOpen={setUserMenuOpen} openPwModal={openPwModal} + openDesignModal={openDesignModal} doLogout={doLogout} newGame={newGame} /> @@ -306,6 +360,9 @@ export default function App() { setHelpOpen(false)} /> + {/* Sieger Badge: nur wenn gesetzt */} + +
{sections.map((sec) => ( + {/* Sieger ganz unten */} + +
@@ -335,6 +395,16 @@ export default function App() { savePassword={savePassword} /> + setDesignOpen(false)} + themeKey={themeKey} + onSelect={(k) => { + selectTheme(k); + setDesignOpen(false); + }} + /> + +
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/SheetSection.jsx b/frontend/src/components/SheetSection.jsx index 9ca0cb5..e16ef2e 100644 --- a/frontend/src/components/SheetSection.jsx +++ b/frontend/src/components/SheetSection.jsx @@ -3,15 +3,6 @@ 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, @@ -20,18 +11,18 @@ export default function SheetSection({ 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)"; + if (status === 1) return stylesTokens.rowNoBg; + if (status === 2) return stylesTokens.rowOkBg; + if (status === 3) return stylesTokens.rowMaybeBg; + if (status === 0) return stylesTokens.rowEmptyBg; + return stylesTokens.rowDefaultBg; }; const getNameColor = (status) => { - if (status === 1) return "#ffb3b3"; - if (status === 2) return "#baf3c9"; - if (status === 3) return "rgba(233,216,166,0.78)"; + if (status === 1) return stylesTokens.rowNoText; + if (status === 2) return stylesTokens.rowOkText; + if (status === 3) return stylesTokens.rowMaybeText; return stylesTokens.textMain; }; @@ -43,11 +34,17 @@ export default function SheetSection({ }; 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)" }; + if (status === 2) return { color: stylesTokens.badgeOkText, background: stylesTokens.badgeOkBg }; + if (status === 1) return { color: stylesTokens.badgeNoText, background: stylesTokens.badgeNoBg }; + if (status === 3) return { color: stylesTokens.badgeMaybeText, background: stylesTokens.badgeMaybeBg }; + return { color: stylesTokens.badgeEmptyText, background: stylesTokens.badgeEmptyBg }; + }; + + const getBorderLeft = (status) => { + if (status === 2) return `4px solid ${stylesTokens.rowOkBorder}`; + if (status === 1) return `4px solid ${stylesTokens.rowNoBorder}`; + if (status === 3) return `4px solid ${stylesTokens.rowMaybeBorder}`; + return `4px solid ${stylesTokens.rowEmptyBorder}`; }; return ( @@ -56,7 +53,6 @@ export default function SheetSection({
{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; @@ -70,14 +66,7 @@ export default function SheetSection({ ...styles.row, background: getRowBg(effectiveStatus), animation: pulseId === e.entry_id ? "rowPulse 220ms ease-out" : "none", - borderLeft: - effectiveStatus === 2 - ? "4px solid rgba(0,190,80,0.55)" - : effectiveStatus === 1 - ? "4px solid rgba(255,35,35,0.55)" - : effectiveStatus === 3 - ? "4px solid rgba(233,216,166,0.22)" - : "4px solid rgba(0,0,0,0)", + borderLeft: getBorderLeft(effectiveStatus), }} >
- {/* 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/WinnerBadge.jsx b/frontend/src/components/WinnerBadge.jsx new file mode 100644 index 0000000..64ac143 --- /dev/null +++ b/frontend/src/components/WinnerBadge.jsx @@ -0,0 +1,43 @@ +// src/components/WinnerBadge.jsx +import React from "react"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; + +export default function WinnerBadge({ winner }) { + const w = (winner || "").trim(); + if (!w) return null; + + return ( +
+
+
+
🏆 Sieger
+
{w}
+
+ +
+ Gewonnen +
+
+
+ ); +} 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/styles.js b/frontend/src/styles/styles.js index 6687ca2..a7b4deb 100644 --- a/frontend/src/styles/styles.js +++ b/frontend/src/styles/styles.js @@ -49,8 +49,11 @@ export const styles = { 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}`, + + // WICHTIG: Header-Farben aus Theme-Tokens, nicht hart codiert + background: `linear-gradient(180deg, ${stylesTokens.headerBgTop}, ${stylesTokens.headerBgBottom})`, + borderBottom: `1px solid ${stylesTokens.headerBorder}`, + textTransform: "uppercase", textShadow: "0 1px 0 rgba(0,0,0,0.6)", }, @@ -418,7 +421,7 @@ export const styles = { bgMap: { position: "absolute", inset: 0, - backgroundImage: 'url("/bg/marauders-map-blur.jpg")', + backgroundImage: stylesTokens.bgImage, backgroundSize: "cover", backgroundPosition: "center", backgroundRepeat: "no-repeat", diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js index 60c18ad..25e9a70 100644 --- a/frontend/src/styles/theme.js +++ b/frontend/src/styles/theme.js @@ -1,11 +1,36 @@ 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)", + + // Header + headerBgTop: "var(--hp-headerBgTop)", + headerBgBottom: "var(--hp-headerBgBottom)", + headerBorder: "var(--hp-headerBorder)", + + // Rows + rowNoBg: "var(--hp-rowNoBg)", + rowNoText: "var(--hp-rowNoText)", + rowNoBorder: "var(--hp-rowNoBorder)", + + rowOkBg: "var(--hp-rowOkBg)", + rowOkText: "var(--hp-rowOkText)", + rowOkBorder: "var(--hp-rowOkBorder)", + + rowMaybeBg: "var(--hp-rowMaybeBg)", + rowMaybeText: "var(--hp-rowMaybeText)", + rowMaybeBorder: "var(--hp-rowMaybeBorder)", + + rowEmptyBg: "var(--hp-rowEmptyBg)", + rowEmptyText: "var(--hp-rowEmptyText)", + rowEmptyBorder: "var(--hp-rowEmptyBorder)", + + // Background + bgImage: "var(--hp-bgImage)", +}; diff --git a/frontend/src/styles/themes.js b/frontend/src/styles/themes.js new file mode 100644 index 0000000..b71b02d --- /dev/null +++ b/frontend/src/styles/themes.js @@ -0,0 +1,223 @@ +// 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)", + + // Section header (wie TopBar, aber leicht “tiefer”) + headerBgTop: "rgba(32,32,36,0.92)", + headerBgBottom: "rgba(14,14,16,0.92)", + headerBorder: "rgba(233, 216, 166, 0.18)", + + // Row colors (fix falsch bleibt rot in ALLEN themes) + rowNoBg: "rgba(255, 35, 35, 0.16)", + rowNoText: "#ffb3b3", + rowNoBorder: "rgba(255,35,35,0.55)", + + rowOkBg: "rgba(0, 190, 80, 0.16)", + rowOkText: "#baf3c9", + rowOkBorder: "rgba(0,190,80,0.55)", + + rowMaybeBg: "rgba(140, 140, 140, 0.12)", + rowMaybeText: "rgba(233,216,166,0.85)", + rowMaybeBorder: "rgba(233,216,166,0.22)", + + rowEmptyBg: "rgba(255,255,255,0.06)", + rowEmptyText: "rgba(233,216,166,0.75)", + rowEmptyBorder: "rgba(0,0,0,0)", + + // Background + bgImage: "url('/bg/marauders-map-blur.jpg')", + }, + }, + + 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)", + + headerBgTop: "rgba(42,18,18,0.92)", + headerBgBottom: "rgba(18,10,10,0.92)", + headerBorder: "rgba(255, 184, 107, 0.22)", + + rowNoBg: "rgba(255, 35, 35, 0.16)", + rowNoText: "#ffb3b3", + rowNoBorder: "rgba(255,35,35,0.55)", + + rowOkBg: "rgba(255, 184, 107, 0.16)", + rowOkText: "#ffd2a8", + rowOkBorder: "rgba(255,184,107,0.55)", + + rowMaybeBg: "rgba(140, 140, 140, 0.12)", + rowMaybeText: "rgba(255,210,170,0.85)", + rowMaybeBorder: "rgba(255,184,107,0.22)", + + rowEmptyBg: "rgba(255,255,255,0.06)", + rowEmptyText: "rgba(255,210,170,0.75)", + rowEmptyBorder: "rgba(0,0,0,0)", + + // Background + bgImage: "url('/bg/gryffindor.png')", + }, + }, + + 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)", + + headerBgTop: "rgba(14,28,22,0.92)", + headerBgBottom: "rgba(10,14,12,0.92)", + headerBorder: "rgba(124, 255, 182, 0.22)", + + rowNoBg: "rgba(255, 35, 35, 0.16)", + rowNoText: "#ffb3b3", + rowNoBorder: "rgba(255,35,35,0.55)", + + rowOkBg: "rgba(124, 255, 182, 0.16)", + rowOkText: "rgba(190,255,220,0.92)", + rowOkBorder: "rgba(124,255,182,0.55)", + + rowMaybeBg: "rgba(120, 255, 190, 0.10)", + rowMaybeText: "rgba(175,240,210,0.85)", + rowMaybeBorder: "rgba(120,255,190,0.22)", + + rowEmptyBg: "rgba(255,255,255,0.06)", + rowEmptyText: "rgba(175,240,210,0.75)", + rowEmptyBorder: "rgba(0,0,0,0)", + + // Background + bgImage: "url('/bg/slytherin.png')", + }, + }, + + 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)", + + headerBgTop: "rgba(18,22,40,0.92)", + headerBgBottom: "rgba(10,12,20,0.92)", + headerBorder: "rgba(143, 182, 255, 0.22)", + + rowNoBg: "rgba(255, 35, 35, 0.16)", + rowNoText: "#ffb3b3", + rowNoBorder: "rgba(255,35,35,0.55)", + + rowOkBg: "rgba(143, 182, 255, 0.16)", + rowOkText: "rgba(210,230,255,0.92)", + rowOkBorder: "rgba(143,182,255,0.55)", + + rowMaybeBg: "rgba(140, 180, 255, 0.10)", + rowMaybeText: "rgba(180,205,255,0.85)", + rowMaybeBorder: "rgba(143,182,255,0.22)", + + rowEmptyBg: "rgba(255,255,255,0.06)", + rowEmptyText: "rgba(180,205,255,0.78)", + rowEmptyBorder: "rgba(0,0,0,0)", + + // Background + bgImage: "url('/bg/ravenclaw.png')", + }, + }, + + 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)", + + headerBgTop: "rgba(34,30,14,0.92)", + headerBgBottom: "rgba(16,14,8,0.92)", + headerBorder: "rgba(255, 226, 122, 0.22)", + + rowNoBg: "rgba(255, 35, 35, 0.16)", + rowNoText: "#ffb3b3", + rowNoBorder: "rgba(255,35,35,0.55)", + + rowOkBg: "rgba(255, 226, 122, 0.16)", + rowOkText: "rgba(255,240,190,0.92)", + rowOkBorder: "rgba(255,226,122,0.55)", + + rowMaybeBg: "rgba(255, 226, 122, 0.10)", + rowMaybeText: "rgba(255,240,180,0.85)", + rowMaybeBorder: "rgba(255,226,122,0.22)", + + rowEmptyBg: "rgba(255,255,255,0.06)", + rowEmptyText: "rgba(255,240,180,0.78)", + rowEmptyBorder: "rgba(0,0,0,0)", + + // Background + bgImage: "url('/bg/hufflepuff.png')", + }, + }, +}; + +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..323bf1f --- /dev/null +++ b/frontend/src/utils/winnerStorage.js @@ -0,0 +1,28 @@ +// frontend/src/utils/winnerStorage.js + +function winnerKey(gameId) { + return `winner:${gameId}`; +} + +export function getWinnerLS(gameId) { + if (!gameId) return ""; + try { + return localStorage.getItem(winnerKey(gameId)) || ""; + } catch { + return ""; + } +} + +export function setWinnerLS(gameId, name) { + if (!gameId) return; + try { + localStorage.setItem(winnerKey(gameId), (name || "").trim()); + } catch {} +} + +export function clearWinnerLS(gameId) { + if (!gameId) return; + try { + localStorage.removeItem(winnerKey(gameId)); + } catch {} +}