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.
This commit is contained in:
@@ -4,9 +4,13 @@ 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 { getWinner, setWinner as saveWinnerLS } 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 +19,8 @@ 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";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
useHpGlobalStyles();
|
useHpGlobalStyles();
|
||||||
@@ -31,6 +37,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 +54,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);
|
||||||
|
|
||||||
@@ -62,8 +80,6 @@ export default function App() {
|
|||||||
setSheet(sh);
|
setSheet(sh);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Effects =====
|
|
||||||
|
|
||||||
// Dropdown outside click
|
// Dropdown outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onDown = (e) => {
|
const onDown = (e) => {
|
||||||
@@ -92,9 +108,9 @@ export default function App() {
|
|||||||
if (!gameId) return;
|
if (!gameId) return;
|
||||||
try {
|
try {
|
||||||
await reloadSheet();
|
await reloadSheet();
|
||||||
} catch {
|
} catch {}
|
||||||
// ignore
|
// Winner pro Game aus LS laden
|
||||||
}
|
setWinnerName(getWinner(gameId));
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [gameId]);
|
}, [gameId]);
|
||||||
@@ -114,6 +130,7 @@ export default function App() {
|
|||||||
setGames([]);
|
setGames([]);
|
||||||
setGameId(null);
|
setGameId(null);
|
||||||
setSheet(null);
|
setSheet(null);
|
||||||
|
setWinnerName("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Password change =====
|
// ===== 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 =====
|
// ===== Game actions =====
|
||||||
const newGame = async () => {
|
const newGame = async () => {
|
||||||
const g = await api("/games", {
|
const g = await api("/games", {
|
||||||
@@ -164,6 +193,12 @@ export default function App() {
|
|||||||
setGameId(g.id);
|
setGameId(g.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== Winner actions =====
|
||||||
|
const saveWinner = () => {
|
||||||
|
if (!gameId) return;
|
||||||
|
saveWinnerLS(gameId, winnerName.trim());
|
||||||
|
};
|
||||||
|
|
||||||
// ===== Sheet actions =====
|
// ===== Sheet actions =====
|
||||||
const cycleStatus = async (entry) => {
|
const cycleStatus = async (entry) => {
|
||||||
let next = 0;
|
let next = 0;
|
||||||
@@ -185,14 +220,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 +243,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 +284,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 +322,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}
|
||||||
/>
|
/>
|
||||||
@@ -320,6 +352,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,11 +370,17 @@ export default function App() {
|
|||||||
savePassword={savePassword}
|
savePassword={savePassword}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChipModal
|
<DesignModal
|
||||||
chipOpen={chipOpen}
|
open={designOpen}
|
||||||
closeChipModalToDash={closeChipModalToDash}
|
onClose={() => setDesignOpen(false)}
|
||||||
chooseChip={chooseChip}
|
themeKey={themeKey}
|
||||||
|
onSelect={(k) => {
|
||||||
|
selectTheme(k);
|
||||||
|
setDesignOpen(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ChipModal chipOpen={chipOpen} closeChipModalToDash={closeChipModalToDash} chooseChip={chooseChip} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
57
frontend/src/components/DesignModal.jsx
Normal file
57
frontend/src/components/DesignModal.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
33
frontend/src/components/WinnerCard.jsx
Normal file
33
frontend/src/components/WinnerCard.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
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)",
|
||||||
};
|
};
|
||||||
106
frontend/src/styles/themes.js
Normal file
106
frontend/src/styles/themes.js
Normal file
@@ -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 {}
|
||||||
|
}
|
||||||
20
frontend/src/utils/winnerStorage.js
Normal file
20
frontend/src/utils/winnerStorage.js
Normal file
@@ -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 {}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user