Compare commits

..

8 Commits

Author SHA1 Message Date
be0f5e9a9f Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2026-02-06 09:43:03 +00:00
6732208792 Refactor WinnerBadge component to simplify implementation
Moved logic for displaying the winner badge directly into the `WinnerBadge` component, removing unused local storage helper functions. Updated styling and streamlined the component for better clarity and maintainability.
2026-02-06 10:05:21 +01:00
74de7bf4dd Enhance winner management with localStorage updates
Refactored winner storage logic by introducing `clearWinnerLS` and replacing outdated functions with `getWinnerLS` and `setWinnerLS`. Added a `WinnerBadge` component to display the winner's status and updated game lifecycle handling to ensure proper winner reset and management.
2026-02-06 10:02:11 +01:00
7024a681da Add themed background images for Hogwarts houses
This commit introduces specific background images for Gryffindor, Slytherin, Ravenclaw, and Hufflepuff houses. Updated the theme configuration to dynamically set these images per house and adjusted the styles to utilize the new theme token for background images.
2026-02-06 09:52:54 +01:00
4295b139b2 Refactor header and row styles to use theme tokens.
Replaced hardcoded header colors with theme-based tokens for better maintainability and consistency across themes. Simplified the structure by consolidating row and header-specific design tokens and removing unused badge tokens.
2026-02-06 09:39:21 +01:00
6e460b69b4 Refactor and expand style tokens for rows and badges.
This commit separates badge and row styles into distinct tokens, improving modularity and theme customization. It also introduces new tokens for section headers and replaces hardcoded values with more consistent styling logic. These changes enhance maintainability and visual consistency across the application.
2026-02-06 09:33:09 +01:00
a9021fb4f1 Refactor status-based styles to use theme tokens.
This change replaces hardcoded status-based styles with theme tokens, improving consistency and maintainability. The update also enables easier customization of styles across different themes by centralizing row and badge colors in theme configurations.
2026-02-06 09:25:43 +01:00
a08b74ff7a Add theme customization and winner management features
Introduced a theme selection feature, allowing users to customize the application's appearance, with themes stored per user. Added functionality to manage and store the game's winner locally. These changes improve user experience and personalization.
2026-02-06 09:15:51 +01:00
15 changed files with 537 additions and 84 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 KiB

View File

@@ -4,9 +4,14 @@ import React, { useEffect, useState } from "react";
import { api } from "./api/client"; import { api } from "./api/client";
import { cycleTag } from "./utils/cycleTag"; import { cycleTag } from "./utils/cycleTag";
import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage"; import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage";
import { getWinnerLS, setWinnerLS, clearWinnerLS } from "./utils/winnerStorage";
import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles"; import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles";
import { styles } from "./styles/styles"; import { styles } from "./styles/styles";
import { applyTheme, loadThemeKey, saveThemeKey, DEFAULT_THEME_KEY } from "./styles/themes";
import AdminPanel from "./components/AdminPanel"; import AdminPanel from "./components/AdminPanel";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/LoginPage";
import TopBar from "./components/TopBar"; import TopBar from "./components/TopBar";
@@ -15,6 +20,9 @@ import ChipModal from "./components/ChipModal";
import HelpModal from "./components/HelpModal"; import HelpModal from "./components/HelpModal";
import GamePickerCard from "./components/GamePickerCard"; import GamePickerCard from "./components/GamePickerCard";
import SheetSection from "./components/SheetSection"; import SheetSection from "./components/SheetSection";
import DesignModal from "./components/DesignModal";
import WinnerCard from "./components/WinnerCard";
import WinnerBadge from "./components/WinnerBadge";
export default function App() { export default function App() {
useHpGlobalStyles(); useHpGlobalStyles();
@@ -31,6 +39,9 @@ export default function App() {
const [sheet, setSheet] = useState(null); const [sheet, setSheet] = useState(null);
const [pulseId, setPulseId] = useState(null); const [pulseId, setPulseId] = useState(null);
// Winner (per game)
const [winnerName, setWinnerName] = useState("");
// Modals // Modals
const [helpOpen, setHelpOpen] = useState(false); const [helpOpen, setHelpOpen] = useState(false);
@@ -45,11 +56,20 @@ export default function App() {
const [pwMsg, setPwMsg] = useState(""); const [pwMsg, setPwMsg] = useState("");
const [pwSaving, setPwSaving] = useState(false); const [pwSaving, setPwSaving] = useState(false);
// Theme
const [designOpen, setDesignOpen] = useState(false);
const [themeKey, setThemeKey] = useState(DEFAULT_THEME_KEY);
// ===== Data loaders ===== // ===== Data loaders =====
const load = async () => { const load = async () => {
const m = await api("/auth/me"); const m = await api("/auth/me");
setMe(m); setMe(m);
// Theme pro User laden & anwenden
const tk = loadThemeKey(m?.email);
setThemeKey(tk);
applyTheme(tk);
const gs = await api("/games"); const gs = await api("/games");
setGames(gs); setGames(gs);
@@ -86,15 +106,19 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// load sheet when game changes // load sheet + winner when game changes
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (!gameId) return; if (!gameId) return;
try { try {
await reloadSheet(); await reloadSheet();
} catch { } catch {
// ignore // ignore
} }
// Sieger pro Game aus localStorage laden
setWinnerName(getWinnerLS(gameId));
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [gameId]); }, [gameId]);
@@ -114,6 +138,7 @@ export default function App() {
setGames([]); setGames([]);
setGameId(null); setGameId(null);
setSheet(null); setSheet(null);
setWinnerName("");
}; };
// ===== Password change ===== // ===== 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 ===== // ===== Game actions =====
const newGame = async () => { const newGame = async () => {
const g = await api("/games", { const g = await api("/games", {
method: "POST", method: "POST",
body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }), body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }),
}); });
const gs = await api("/games"); const gs = await api("/games");
setGames(gs); setGames(gs);
setGameId(g.id); 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 ===== // ===== Sheet actions =====
@@ -185,14 +242,12 @@ export default function App() {
const toggleTag = async (entry) => { const toggleTag = async (entry) => {
const next = cycleTag(entry.note_tag); const next = cycleTag(entry.note_tag);
// going to "s" -> open chip modal, don't write backend yet
if (next === "s") { if (next === "s") {
setChipEntry(entry); setChipEntry(entry);
setChipOpen(true); setChipOpen(true);
return; return;
} }
// s -> — : clear local chip
if (next === null) clearChipLS(gameId, entry.entry_id); if (next === null) clearChipLS(gameId, entry.entry_id);
await api(`/games/${gameId}/sheet/${entry.entry_id}`, { await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
@@ -210,11 +265,9 @@ export default function App() {
setChipOpen(false); setChipOpen(false);
setChipEntry(null); setChipEntry(null);
// frontend-only save
setChipLS(gameId, entry.entry_id, chip); setChipLS(gameId, entry.entry_id, chip);
try { try {
// backend only gets "s"
await api(`/games/${gameId}/sheet/${entry.entry_id}`, { await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ note_tag: "s" }), body: JSON.stringify({ note_tag: "s" }),
@@ -253,7 +306,7 @@ export default function App() {
const chip = getChipLS(gameId, entry.entry_id); const chip = getChipLS(gameId, entry.entry_id);
return chip ? `s.${chip}` : "s"; return chip ? `s.${chip}` : "s";
} }
return t; // i oder m return t;
}; };
// ===== Login page ===== // ===== Login page =====
@@ -291,6 +344,7 @@ export default function App() {
userMenuOpen={userMenuOpen} userMenuOpen={userMenuOpen}
setUserMenuOpen={setUserMenuOpen} setUserMenuOpen={setUserMenuOpen}
openPwModal={openPwModal} openPwModal={openPwModal}
openDesignModal={openDesignModal}
doLogout={doLogout} doLogout={doLogout}
newGame={newGame} newGame={newGame}
/> />
@@ -306,6 +360,9 @@ export default function App() {
<HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} /> <HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
{/* Sieger Badge: nur wenn gesetzt */}
<WinnerBadge winner={(winnerName || "").trim()} />
<div style={{ marginTop: 14, display: "grid", gap: 14 }}> <div style={{ marginTop: 14, display: "grid", gap: 14 }}>
{sections.map((sec) => ( {sections.map((sec) => (
<SheetSection <SheetSection
@@ -320,6 +377,9 @@ export default function App() {
))} ))}
</div> </div>
{/* Sieger ganz unten */}
<WinnerCard value={winnerName} setValue={setWinnerName} onSave={saveWinner} />
<div style={{ height: 24 }} /> <div style={{ height: 24 }} />
</div> </div>
@@ -335,6 +395,16 @@ export default function App() {
savePassword={savePassword} savePassword={savePassword}
/> />
<DesignModal
open={designOpen}
onClose={() => setDesignOpen(false)}
themeKey={themeKey}
onSelect={(k) => {
selectTheme(k);
setDesignOpen(false);
}}
/>
<ChipModal <ChipModal
chipOpen={chipOpen} chipOpen={chipOpen}
closeChipModalToDash={closeChipModalToDash} closeChipModalToDash={closeChipModalToDash}

View File

@@ -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 (
<div style={styles.modalOverlay} onMouseDown={onClose}>
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
<div style={styles.modalHeader}>
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Design ändern</div>
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Schließen">
</button>
</div>
<div style={{ marginTop: 12, color: stylesTokens.textMain, opacity: 0.92 }}>
Wähle dein Theme:
</div>
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
{themeEntries.map(([key, t]) => {
const active = key === themeKey;
return (
<button
key={key}
onClick={() => onSelect(key)}
style={{
...styles.secondaryBtn,
textAlign: "left",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
border: active
? `1px solid rgba(233,216,166,0.40)`
: `1px solid rgba(233,216,166,0.18)`,
}}
>
<span style={{ fontWeight: 1000 }}>{t.label}</span>
<span style={{ opacity: 0.75 }}>{active ? "✓ aktiv" : ""}</span>
</button>
);
})}
</div>
<div style={{ marginTop: 12, fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
Hinweis: Das Design wird pro User gespeichert (Email).
</div>
</div>
</div>
);
}

View File

@@ -3,15 +3,6 @@ import React from "react";
import { styles } from "../styles/styles"; import { styles } from "../styles/styles";
import { stylesTokens } from "../styles/theme"; 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({ export default function SheetSection({
title, title,
entries, entries,
@@ -20,18 +11,18 @@ export default function SheetSection({
onToggleTag, onToggleTag,
displayTag, displayTag,
}) { }) {
// --- helpers (lokal, weil sie rein UI sind) ---
const getRowBg = (status) => { const getRowBg = (status) => {
if (status === 1) return "rgba(255, 35, 35, 0.16)"; if (status === 1) return stylesTokens.rowNoBg;
if (status === 2) return "rgba(0, 190, 80, 0.16)"; if (status === 2) return stylesTokens.rowOkBg;
if (status === 3) return "rgba(140, 140, 140, 0.12)"; if (status === 3) return stylesTokens.rowMaybeBg;
return "rgba(255,255,255,0.06)"; if (status === 0) return stylesTokens.rowEmptyBg;
return stylesTokens.rowDefaultBg;
}; };
const getNameColor = (status) => { const getNameColor = (status) => {
if (status === 1) return "#ffb3b3"; if (status === 1) return stylesTokens.rowNoText;
if (status === 2) return "#baf3c9"; if (status === 2) return stylesTokens.rowOkText;
if (status === 3) return "rgba(233,216,166,0.78)"; if (status === 3) return stylesTokens.rowMaybeText;
return stylesTokens.textMain; return stylesTokens.textMain;
}; };
@@ -43,11 +34,17 @@ export default function SheetSection({
}; };
const getStatusBadge = (status) => { const getStatusBadge = (status) => {
if (status === 2) return { color: "#baf3c9", background: "rgba(0,190,80,0.18)" }; if (status === 2) return { color: stylesTokens.badgeOkText, background: stylesTokens.badgeOkBg };
if (status === 1) return { color: "#ffb3b3", background: "rgba(255,35,35,0.18)" }; if (status === 1) return { color: stylesTokens.badgeNoText, background: stylesTokens.badgeNoBg };
if (status === 3) if (status === 3) return { color: stylesTokens.badgeMaybeText, background: stylesTokens.badgeMaybeBg };
return { color: "rgba(233,216,166,0.85)", background: "rgba(140,140,140,0.14)" }; return { color: stylesTokens.badgeEmptyText, background: stylesTokens.badgeEmptyBg };
return { color: "rgba(233,216,166,0.75)", background: "rgba(255,255,255,0.08)" }; };
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 ( return (
@@ -56,7 +53,6 @@ export default function SheetSection({
<div style={{ display: "grid" }}> <div style={{ display: "grid" }}>
{entries.map((e) => { {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 isIorMorS = e.note_tag === "i" || e.note_tag === "m" || e.note_tag === "s";
const effectiveStatus = e.status === 0 && isIorMorS ? 1 : e.status; const effectiveStatus = e.status === 0 && isIorMorS ? 1 : e.status;
@@ -70,14 +66,7 @@ export default function SheetSection({
...styles.row, ...styles.row,
background: getRowBg(effectiveStatus), background: getRowBg(effectiveStatus),
animation: pulseId === e.entry_id ? "rowPulse 220ms ease-out" : "none", animation: pulseId === e.entry_id ? "rowPulse 220ms ease-out" : "none",
borderLeft: borderLeft: getBorderLeft(effectiveStatus),
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)",
}} }}
> >
<div <div

View File

@@ -7,44 +7,20 @@ export default function TopBar({
userMenuOpen, userMenuOpen,
setUserMenuOpen, setUserMenuOpen,
openPwModal, openPwModal,
openDesignModal,
doLogout, doLogout,
newGame, newGame,
}) { }) {
return ( return (
<div style={styles.topBar}> <div style={styles.topBar}>
{/* LINKS: nur Rolle */}
<div> <div>
<div style={{ fontWeight: 900, color: stylesTokens.textGold }}> <div style={{ fontWeight: 900, color: stylesTokens.textGold }}>Notizbogen</div>
Notizbogen <div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>{me.email}</div>
</div>
<div
style={{
fontSize: 12,
opacity: 0.8,
color: stylesTokens.textDim,
}}
>
{me.email}
</div>
</div> </div>
{/* RECHTS: Account + Neues Spiel */} <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "nowrap" }} data-user-menu>
<div
style={{
display: "flex",
gap: 8,
alignItems: "center",
flexWrap: "nowrap",
}}
data-user-menu
>
{/* Account Dropdown */}
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<button <button onClick={() => setUserMenuOpen((v) => !v)} style={styles.userBtn} title="User Menü">
onClick={() => setUserMenuOpen((v) => !v)}
style={styles.userBtn}
title="User Menü"
>
<span style={{ fontSize: 16 }}>👤</span> <span style={{ fontSize: 16 }}>👤</span>
<span>User</span> <span>User</span>
<span style={{ opacity: 0.7 }}></span> <span style={{ opacity: 0.7 }}></span>
@@ -52,7 +28,6 @@ export default function TopBar({
{userMenuOpen && ( {userMenuOpen && (
<div style={styles.userDropdown}> <div style={styles.userDropdown}>
{/* Email Info */}
<div <div
style={{ style={{
padding: "10px 12px", padding: "10px 12px",
@@ -65,11 +40,14 @@ export default function TopBar({
{me.email} {me.email}
</div> </div>
{/* Actions */}
<button onClick={openPwModal} style={styles.userDropdownItem}> <button onClick={openPwModal} style={styles.userDropdownItem}>
Passwort setzen Passwort setzen
</button> </button>
<button onClick={openDesignModal} style={styles.userDropdownItem}>
Design ändern
</button>
<div style={styles.userDropdownDivider} /> <div style={styles.userDropdownDivider} />
<button <button
@@ -77,10 +55,7 @@ export default function TopBar({
setUserMenuOpen(false); setUserMenuOpen(false);
doLogout(); doLogout();
}} }}
style={{ style={{ ...styles.userDropdownItem, color: "#ffb3b3" }}
...styles.userDropdownItem,
color: "#ffb3b3",
}}
> >
Logout Logout
</button> </button>
@@ -88,7 +63,6 @@ export default function TopBar({
)} )}
</div> </div>
{/* Neues Spiel Button */}
<button onClick={newGame} style={styles.primaryBtn}> <button onClick={newGame} style={styles.primaryBtn}>
New Game New Game
</button> </button>

View File

@@ -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 (
<div style={{ marginTop: 14 }}>
<div
style={{
...styles.card,
padding: 12,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 10,
}}
>
<div>
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>🏆 Sieger</div>
<div style={{ marginTop: 2, color: stylesTokens.textMain, opacity: 0.95 }}>{w}</div>
</div>
<div
style={{
padding: "8px 12px",
borderRadius: 999,
border: `1px solid ${stylesTokens.panelBorder}`,
background: stylesTokens.panelBg,
color: stylesTokens.textGold,
fontWeight: 1000,
whiteSpace: "nowrap",
}}
>
Gewonnen
</div>
</div>
</div>
);
}

View File

@@ -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 (
<div style={{ marginTop: 14 }}>
<div style={styles.card}>
<div style={styles.sectionHeader}>Sieger</div>
<div style={styles.cardBody}>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Name des Siegers"
style={{ ...styles.input, flex: 1 }}
onKeyDown={(e) => {
if (e.key === "Enter") onSave();
}}
/>
<button onClick={onSave} style={styles.primaryBtn} title="Speichern">
Speichern
</button>
</div>
<div style={{ padding: "0 12px 12px", fontSize: 12, color: stylesTokens.textDim }}>
Wird pro Spiel lokal gespeichert.
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,8 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { stylesTokens } from "../theme"; import { stylesTokens } from "../theme";
import { applyTheme } from "../themes";
export function useHpGlobalStyles() { export function useHpGlobalStyles() {
// Google Fonts // Google Fonts
@@ -81,4 +84,9 @@ export function useHpGlobalStyles() {
`; `;
document.head.appendChild(style); document.head.appendChild(style);
}, []); }, []);
// Ensure a theme is applied once (fallback)
useEffect(() => {
applyTheme("default");
}, []);
} }

View File

@@ -49,8 +49,11 @@ export const styles = {
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui', fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
letterSpacing: 1.0, letterSpacing: 1.0,
color: stylesTokens.textGold, 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", textTransform: "uppercase",
textShadow: "0 1px 0 rgba(0,0,0,0.6)", textShadow: "0 1px 0 rgba(0,0,0,0.6)",
}, },
@@ -418,7 +421,7 @@ export const styles = {
bgMap: { bgMap: {
position: "absolute", position: "absolute",
inset: 0, inset: 0,
backgroundImage: 'url("/bg/marauders-map-blur.jpg")', backgroundImage: stylesTokens.bgImage,
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",

View File

@@ -1,11 +1,36 @@
export const stylesTokens = { export const stylesTokens = {
pageBg: "#0b0b0c", pageBg: "var(--hp-pageBg)",
panelBg: "rgba(20, 20, 22, 0.55)", panelBg: "var(--hp-panelBg)",
panelBorder: "rgba(233, 216, 166, 0.14)", panelBorder: "var(--hp-panelBorder)",
textMain: "rgba(245, 239, 220, 0.92)", textMain: "var(--hp-textMain)",
textDim: "rgba(233, 216, 166, 0.70)", textDim: "var(--hp-textDim)",
textGold: "#e9d8a6", textGold: "var(--hp-textGold)",
goldLine: "rgba(233, 216, 166, 0.18)", 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)",
};

View File

@@ -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 {}
}

View File

@@ -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 {}
}