Compare commits
8 Commits
1db91c6c88
...
be0f5e9a9f
| Author | SHA1 | Date | |
|---|---|---|---|
| be0f5e9a9f | |||
| 6732208792 | |||
| 74de7bf4dd | |||
| 7024a681da | |||
| 4295b139b2 | |||
| 6e460b69b4 | |||
| a9021fb4f1 | |||
| a08b74ff7a |
BIN
frontend/public/bg/gryffindor.png
Normal file
BIN
frontend/public/bg/gryffindor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1022 KiB |
BIN
frontend/public/bg/hufflepuff.png
Normal file
BIN
frontend/public/bg/hufflepuff.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 749 KiB |
BIN
frontend/public/bg/ravenclaw.png
Normal file
BIN
frontend/public/bg/ravenclaw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
BIN
frontend/public/bg/slytherin.png
Normal file
BIN
frontend/public/bg/slytherin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 980 KiB |
@@ -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}
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
43
frontend/src/components/WinnerBadge.jsx
Normal file
43
frontend/src/components/WinnerBadge.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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");
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)",
|
||||||
};
|
};
|
||||||
223
frontend/src/styles/themes.js
Normal file
223
frontend/src/styles/themes.js
Normal 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 {}
|
||||||
|
}
|
||||||
28
frontend/src/utils/winnerStorage.js
Normal file
28
frontend/src/utils/winnerStorage.js
Normal 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 {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user