Refactor and enhance game management, user roles, and state handling
This commit introduces significant changes across the backend and frontend to improve game creation, joining, and member management. Key updates include adding a host role, structured handling of winners, and a New Game modal in the frontend. The refactor also simplifies join codes, improves persistence for user themes, and enhances overall user interaction with better UI feedback and logic.
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
// src/App.jsx
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { api } from "./api/client";
|
||||
import { cycleTag } from "./utils/cycleTag";
|
||||
import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage";
|
||||
|
||||
import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles";
|
||||
import { styles } from "./styles/styles";
|
||||
|
||||
import { applyTheme, DEFAULT_THEME_KEY } from "./styles/themes";
|
||||
|
||||
import AdminPanel from "./components/AdminPanel";
|
||||
@@ -20,7 +19,7 @@ import SheetSection from "./components/SheetSection";
|
||||
import DesignModal from "./components/DesignModal";
|
||||
import WinnerCard from "./components/WinnerCard";
|
||||
import WinnerBadge from "./components/WinnerBadge";
|
||||
import JoinGameModal from "./components/JoinGameModal";
|
||||
import NewGameModal from "./components/NewGameModal";
|
||||
|
||||
export default function App() {
|
||||
useHpGlobalStyles();
|
||||
@@ -37,17 +36,17 @@ export default function App() {
|
||||
const [sheet, setSheet] = useState(null);
|
||||
const [pulseId, setPulseId] = useState(null);
|
||||
|
||||
// Game meta / players / winner
|
||||
const [gameMeta, setGameMeta] = useState(null);
|
||||
const [players, setPlayers] = useState([]);
|
||||
const [winnerUserId, setWinnerUserId] = useState(null);
|
||||
// Game meta
|
||||
const [gameMeta, setGameMeta] = useState(null); // {code, host_user_id, winner_email, winner_user_id}
|
||||
const [members, setMembers] = useState([]);
|
||||
|
||||
// Winner selection (host only)
|
||||
const [winnerUserId, setWinnerUserId] = useState("");
|
||||
|
||||
// Modals
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
|
||||
const [chipOpen, setChipOpen] = useState(false);
|
||||
const [chipEntry, setChipEntry] = useState(null);
|
||||
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
|
||||
const [pwOpen, setPwOpen] = useState(false);
|
||||
@@ -60,15 +59,9 @@ export default function App() {
|
||||
const [designOpen, setDesignOpen] = useState(false);
|
||||
const [themeKey, setThemeKey] = useState(DEFAULT_THEME_KEY);
|
||||
|
||||
// Join game
|
||||
const [joinOpen, setJoinOpen] = useState(false);
|
||||
// New Game Modal
|
||||
const [newGameOpen, setNewGameOpen] = useState(false);
|
||||
|
||||
const currentGame = useMemo(
|
||||
() => games.find((g) => String(g.id) === String(gameId)) || null,
|
||||
[games, gameId]
|
||||
);
|
||||
|
||||
// ===== Data loaders =====
|
||||
const load = async () => {
|
||||
const m = await api("/auth/me");
|
||||
setMe(m);
|
||||
@@ -89,21 +82,16 @@ export default function App() {
|
||||
setSheet(sh);
|
||||
};
|
||||
|
||||
const reloadMeta = async () => {
|
||||
const loadGameMeta = async () => {
|
||||
if (!gameId) return;
|
||||
const meta = await api(`/games/${gameId}/meta`);
|
||||
const meta = await api(`/games/${gameId}`);
|
||||
setGameMeta(meta);
|
||||
setWinnerUserId(meta?.winner?.id || null);
|
||||
};
|
||||
setWinnerUserId(meta?.winner_user_id || "");
|
||||
|
||||
const reloadPlayers = async () => {
|
||||
if (!gameId) return;
|
||||
const ps = await api(`/games/${gameId}/players`);
|
||||
setPlayers(ps);
|
||||
const mem = await api(`/games/${gameId}/members`);
|
||||
setMembers(mem);
|
||||
};
|
||||
|
||||
// ===== Effects =====
|
||||
|
||||
// Dropdown outside click
|
||||
useEffect(() => {
|
||||
const onDown = (e) => {
|
||||
@@ -114,24 +102,23 @@ export default function App() {
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, [userMenuOpen]);
|
||||
|
||||
// initial load (try session)
|
||||
// initial load
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
await load();
|
||||
} catch {
|
||||
// not logged in
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// load sheet/meta when game changes
|
||||
// on game change
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!gameId) return;
|
||||
try {
|
||||
await Promise.all([reloadSheet(), reloadMeta(), reloadPlayers()]);
|
||||
await reloadSheet();
|
||||
await loadGameMeta();
|
||||
} catch {}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -153,11 +140,11 @@ export default function App() {
|
||||
setGameId(null);
|
||||
setSheet(null);
|
||||
setGameMeta(null);
|
||||
setPlayers([]);
|
||||
setWinnerUserId(null);
|
||||
setMembers([]);
|
||||
setWinnerUserId("");
|
||||
};
|
||||
|
||||
// ===== Password change =====
|
||||
// ===== Password =====
|
||||
const openPwModal = () => {
|
||||
setPwMsg("");
|
||||
setPw1("");
|
||||
@@ -194,7 +181,7 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Theme actions =====
|
||||
// ===== Theme =====
|
||||
const openDesignModal = () => {
|
||||
setDesignOpen(true);
|
||||
setUserMenuOpen(false);
|
||||
@@ -209,26 +196,24 @@ export default function App() {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ theme_key: key }),
|
||||
});
|
||||
setMe((prev) => (prev ? { ...prev, theme_key: key } : prev));
|
||||
} catch {
|
||||
// ignore; UI already switched
|
||||
// theme locally already applied; ignore backend error
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Game actions =====
|
||||
const newGame = async () => {
|
||||
// ===== New game flow =====
|
||||
const createGame = 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);
|
||||
};
|
||||
|
||||
const openJoinModal = () => {
|
||||
setJoinOpen(true);
|
||||
setUserMenuOpen(false);
|
||||
// meta/members will load via gameId effect
|
||||
return g; // includes code
|
||||
};
|
||||
|
||||
const joinGame = async (code) => {
|
||||
@@ -236,19 +221,20 @@ export default function App() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
|
||||
const gs = await api("/games");
|
||||
setGames(gs);
|
||||
setGameId(res?.game?.id || null);
|
||||
setGameId(res.id);
|
||||
};
|
||||
|
||||
// ===== Winner actions (shared per game) =====
|
||||
// ===== Winner =====
|
||||
const saveWinner = async () => {
|
||||
if (!gameId) return;
|
||||
await api(`/games/${gameId}/winner`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ winner_user_id: winnerUserId || null }),
|
||||
});
|
||||
await reloadMeta();
|
||||
await loadGameMeta();
|
||||
};
|
||||
|
||||
// ===== Sheet actions =====
|
||||
@@ -278,9 +264,11 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (next === null) clearChipLS(gameId, entry.entry_id);
|
||||
|
||||
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ note_tag: next }),
|
||||
body: JSON.stringify({ note_tag: next, chip: null }),
|
||||
});
|
||||
|
||||
await reloadSheet();
|
||||
@@ -293,10 +281,12 @@ export default function App() {
|
||||
setChipOpen(false);
|
||||
setChipEntry(null);
|
||||
|
||||
setChipLS(gameId, entry.entry_id, chip);
|
||||
|
||||
try {
|
||||
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ note_tag: "s", chip_code: chip }),
|
||||
body: JSON.stringify({ note_tag: "s", chip }),
|
||||
});
|
||||
} finally {
|
||||
await reloadSheet();
|
||||
@@ -313,10 +303,12 @@ export default function App() {
|
||||
setChipOpen(false);
|
||||
setChipEntry(null);
|
||||
|
||||
clearChipLS(gameId, entry.entry_id);
|
||||
|
||||
try {
|
||||
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ note_tag: null }),
|
||||
body: JSON.stringify({ note_tag: null, chip: null }),
|
||||
});
|
||||
} finally {
|
||||
await reloadSheet();
|
||||
@@ -326,10 +318,14 @@ export default function App() {
|
||||
const displayTag = (entry) => {
|
||||
const t = entry.note_tag;
|
||||
if (!t) return "—";
|
||||
|
||||
if (t === "s") {
|
||||
return entry.chip_code ? `s.${entry.chip_code}` : "s";
|
||||
// Prefer backend chip, fallback localStorage
|
||||
const chip = entry.chip || getChipLS(gameId, entry.entry_id);
|
||||
return chip ? `s.${chip}` : "s";
|
||||
}
|
||||
return t;
|
||||
|
||||
return t; // i oder m
|
||||
};
|
||||
|
||||
// ===== Login page =====
|
||||
@@ -355,7 +351,7 @@ export default function App() {
|
||||
]
|
||||
: [];
|
||||
|
||||
const winnerObj = gameMeta?.winner || null;
|
||||
const isHost = !!(me?.id && gameMeta?.host_user_id && me.id === gameMeta.host_user_id);
|
||||
|
||||
return (
|
||||
<div style={styles.page}>
|
||||
@@ -370,9 +366,8 @@ export default function App() {
|
||||
setUserMenuOpen={setUserMenuOpen}
|
||||
openPwModal={openPwModal}
|
||||
openDesignModal={openDesignModal}
|
||||
openJoinModal={openJoinModal}
|
||||
doLogout={doLogout}
|
||||
newGame={newGame}
|
||||
onOpenNewGame={() => setNewGameOpen(true)}
|
||||
/>
|
||||
|
||||
{me.role === "admin" && <AdminPanel />}
|
||||
@@ -381,11 +376,11 @@ export default function App() {
|
||||
games={games}
|
||||
gameId={gameId}
|
||||
setGameId={setGameId}
|
||||
joinCode={currentGame?.join_code || ""}
|
||||
onOpenHelp={() => setHelpOpen(true)}
|
||||
/>
|
||||
|
||||
{winnerObj && <WinnerBadge winnerEmail={winnerObj.email} />}
|
||||
{/* Sieger Badge: zwischen Spiel und Verdächtigte Person */}
|
||||
<WinnerBadge winnerEmail={gameMeta?.winner_email || ""} />
|
||||
|
||||
<HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||
|
||||
@@ -403,9 +398,10 @@ export default function App() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sieger (shared per Spiel) */}
|
||||
{/* Host-only Winner Auswahl */}
|
||||
<WinnerCard
|
||||
players={players}
|
||||
isHost={isHost}
|
||||
members={members}
|
||||
winnerUserId={winnerUserId}
|
||||
setWinnerUserId={setWinnerUserId}
|
||||
onSave={saveWinner}
|
||||
@@ -430,19 +426,17 @@ export default function App() {
|
||||
open={designOpen}
|
||||
onClose={() => setDesignOpen(false)}
|
||||
themeKey={themeKey}
|
||||
onSelect={async (k) => {
|
||||
await selectTheme(k);
|
||||
onSelect={(k) => {
|
||||
selectTheme(k);
|
||||
setDesignOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<JoinGameModal
|
||||
open={joinOpen}
|
||||
onClose={() => setJoinOpen(false)}
|
||||
onJoin={async (code) => {
|
||||
await joinGame(code);
|
||||
setJoinOpen(false);
|
||||
}}
|
||||
<NewGameModal
|
||||
open={newGameOpen}
|
||||
onClose={() => setNewGameOpen(false)}
|
||||
onCreate={createGame}
|
||||
onJoin={joinGame}
|
||||
/>
|
||||
|
||||
<ChipModal
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// src/components/GamePickerCard.jsx
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function GamePickerCard({ games, gameId, setGameId, joinCode, onOpenHelp }) {
|
||||
export default function GamePickerCard({ games, gameId, setGameId, onOpenHelp }) {
|
||||
return (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={styles.card}>
|
||||
@@ -17,7 +16,7 @@ export default function GamePickerCard({ games, gameId, setGameId, joinCode, onO
|
||||
>
|
||||
{games.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
{g.name} {g.code ? `• ${g.code}` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -27,18 +26,16 @@ export default function GamePickerCard({ games, gameId, setGameId, joinCode, onO
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!!joinCode && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0 12px 12px",
|
||||
fontSize: 12,
|
||||
opacity: 0.85,
|
||||
color: stylesTokens.textDim,
|
||||
}}
|
||||
>
|
||||
Spiel-Code: <b style={{ color: stylesTokens.textGold }}>{joinCode}</b>
|
||||
</div>
|
||||
)}
|
||||
{/* kleine Code Zeile unter dem Picker (optional nice) */}
|
||||
{(() => {
|
||||
const cur = games.find((x) => x.id === gameId);
|
||||
if (!cur?.code) return null;
|
||||
return (
|
||||
<div style={{ padding: "0 12px 12px", fontSize: 12, color: stylesTokens.textDim, opacity: 0.9 }}>
|
||||
Code: <b style={{ color: stylesTokens.textGold }}>{cur.code}</b>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
180
frontend/src/components/NewGameModal.jsx
Normal file
180
frontend/src/components/NewGameModal.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function NewGameModal({
|
||||
open,
|
||||
onClose,
|
||||
onCreate,
|
||||
onJoin,
|
||||
}) {
|
||||
const [mode, setMode] = useState("choice"); // choice | create | join
|
||||
const [joinCode, setJoinCode] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [created, setCreated] = useState(null); // { code }
|
||||
const [toast, setToast] = useState("");
|
||||
|
||||
const canJoin = useMemo(() => joinCode.trim().length >= 4, [joinCode]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const showToast = (msg) => {
|
||||
setToast(msg);
|
||||
setTimeout(() => setToast(""), 1100);
|
||||
};
|
||||
|
||||
const doCreate = async () => {
|
||||
setErr("");
|
||||
try {
|
||||
const res = await onCreate();
|
||||
setCreated({ code: res.code });
|
||||
setMode("create");
|
||||
} catch (e) {
|
||||
setErr("❌ Fehler: " + (e?.message || "unknown"));
|
||||
}
|
||||
};
|
||||
|
||||
const doJoin = async () => {
|
||||
setErr("");
|
||||
try {
|
||||
await onJoin(joinCode.trim().toUpperCase());
|
||||
onClose();
|
||||
} catch (e) {
|
||||
setErr("❌ Fehler: " + (e?.message || "unknown"));
|
||||
}
|
||||
};
|
||||
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(created?.code || "");
|
||||
showToast("✅ Code kopiert");
|
||||
} catch {
|
||||
showToast("❌ Copy nicht möglich");
|
||||
}
|
||||
};
|
||||
|
||||
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 }}>
|
||||
Spiel
|
||||
</div>
|
||||
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Schließen">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${stylesTokens.panelBorder}`,
|
||||
background: stylesTokens.panelBg,
|
||||
color: stylesTokens.textMain,
|
||||
fontWeight: 900,
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 120ms ease-out",
|
||||
}}
|
||||
>
|
||||
{toast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
|
||||
{mode === "choice" && (
|
||||
<>
|
||||
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
||||
Willst du ein Spiel <b>erstellen</b> oder einem Spiel <b>beitreten</b>?
|
||||
</div>
|
||||
|
||||
<button onClick={doCreate} style={styles.primaryBtn}>
|
||||
✦ Spiel erstellen
|
||||
</button>
|
||||
|
||||
<button onClick={() => setMode("join")} style={styles.secondaryBtn}>
|
||||
⎆ Spiel beitreten
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === "join" && (
|
||||
<>
|
||||
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
||||
Gib den <b>Code</b> ein:
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={joinCode}
|
||||
onChange={(e) => setJoinCode(e.target.value.toUpperCase())}
|
||||
placeholder="z.B. 8K3MZQ"
|
||||
style={styles.input}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||
<button onClick={() => setMode("choice")} style={styles.secondaryBtn}>
|
||||
Zurück
|
||||
</button>
|
||||
<button onClick={doJoin} style={styles.primaryBtn} disabled={!canJoin}>
|
||||
Beitreten
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === "create" && created && (
|
||||
<>
|
||||
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
||||
Dein Spiel wurde erstellt. Dieser Code bleibt auch bei „Alte Spiele“ sichtbar:
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
padding: 12,
|
||||
borderRadius: 16,
|
||||
border: `1px solid ${stylesTokens.panelBorder}`,
|
||||
background: stylesTokens.panelBg,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
|
||||
Spiel-Code
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 1100,
|
||||
letterSpacing: 2,
|
||||
color: stylesTokens.textGold,
|
||||
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
|
||||
}}
|
||||
>
|
||||
{created.code}
|
||||
</div>
|
||||
|
||||
<button onClick={copyCode} style={styles.primaryBtn}>
|
||||
⧉ Code kopieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||
<button onClick={onClose} style={styles.primaryBtn}>
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{err && <div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>{err}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,22 +8,27 @@ export default function TopBar({
|
||||
setUserMenuOpen,
|
||||
openPwModal,
|
||||
openDesignModal,
|
||||
openJoinModal,
|
||||
doLogout,
|
||||
newGame,
|
||||
onOpenNewGame, // NEW
|
||||
}) {
|
||||
return (
|
||||
<div style={styles.topBar}>
|
||||
{/* LINKS */}
|
||||
<div>
|
||||
<div style={{ fontWeight: 900, color: stylesTokens.textGold }}>Notizbogen</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>{me.email}</div>
|
||||
<div style={{ fontWeight: 900, color: stylesTokens.textGold }}>
|
||||
Notizbogen
|
||||
</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
|
||||
{me.email}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RECHTS */}
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "nowrap" }} data-user-menu>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setUserMenuOpen((v) => !v)} style={styles.userBtn} title="User Menü">
|
||||
<button
|
||||
onClick={() => setUserMenuOpen((v) => !v)}
|
||||
style={styles.userBtn}
|
||||
title="User Menü"
|
||||
>
|
||||
<span style={{ fontSize: 16 }}>👤</span>
|
||||
<span>User</span>
|
||||
<span style={{ opacity: 0.7 }}>▾</span>
|
||||
@@ -31,14 +36,13 @@ export default function TopBar({
|
||||
|
||||
{userMenuOpen && (
|
||||
<div style={styles.userDropdown}>
|
||||
{/* Email Info */}
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 12px",
|
||||
fontSize: 13,
|
||||
opacity: 0.85,
|
||||
color: stylesTokens.textDim,
|
||||
borderBottom: `1px solid ${stylesTokens.goldLine}`,
|
||||
borderBottom: "1px solid rgba(233,216,166,0.12)",
|
||||
}}
|
||||
>
|
||||
{me.email}
|
||||
@@ -48,26 +52,10 @@ export default function TopBar({
|
||||
Passwort setzen
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
openDesignModal();
|
||||
}}
|
||||
style={styles.userDropdownItem}
|
||||
>
|
||||
<button onClick={openDesignModal} style={styles.userDropdownItem}>
|
||||
Design ändern
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
openJoinModal();
|
||||
}}
|
||||
style={styles.userDropdownItem}
|
||||
>
|
||||
Spiel beitreten
|
||||
</button>
|
||||
|
||||
<div style={styles.userDropdownDivider} />
|
||||
|
||||
<button
|
||||
@@ -83,8 +71,8 @@ export default function TopBar({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={newGame} style={styles.primaryBtn}>
|
||||
✦ Neues Spiel
|
||||
<button onClick={onOpenNewGame} style={styles.primaryBtn}>
|
||||
✦ New Game
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,20 +8,29 @@ export default function WinnerBadge({ winnerEmail }) {
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
padding: "12px 14px",
|
||||
padding: "10px 12px",
|
||||
borderRadius: 16,
|
||||
border: `1px solid ${stylesTokens.panelBorder}`,
|
||||
background: stylesTokens.panelBg,
|
||||
boxShadow: "0 12px 30px rgba(0,0,0,0.45)",
|
||||
boxShadow: "0 12px 30px rgba(0,0,0,0.35)",
|
||||
backdropFilter: "blur(6px)",
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16 }}>🏆</span>
|
||||
<span style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Sieger:</span>
|
||||
<span style={{ color: stylesTokens.textMain }}>{winnerEmail}</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div style={{ fontSize: 18 }}>🏆</div>
|
||||
<div style={{ color: stylesTokens.textMain, fontWeight: 900 }}>
|
||||
Sieger:
|
||||
<span style={{ color: stylesTokens.textGold }}>{" "}{winnerEmail}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
|
||||
festgelegt
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,42 @@
|
||||
// src/components/WinnerCard.jsx
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
/**
|
||||
* props:
|
||||
* - players: [{id,email}]
|
||||
* - winnerUserId: string|null
|
||||
* - setWinnerUserId: fn
|
||||
* - onSave: fn (async ok)
|
||||
*/
|
||||
export default function WinnerCard({ players, winnerUserId, setWinnerUserId, onSave }) {
|
||||
const hasPlayers = Array.isArray(players) && players.length > 0;
|
||||
export default function WinnerCard({
|
||||
isHost,
|
||||
members,
|
||||
winnerUserId,
|
||||
setWinnerUserId,
|
||||
onSave,
|
||||
}) {
|
||||
if (!isHost) return null;
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={styles.card}>
|
||||
<div style={styles.sectionHeader}>Sieger</div>
|
||||
|
||||
<div style={{ padding: 12, display: "grid", gap: 10 }}>
|
||||
{!hasPlayers ? (
|
||||
<div style={{ color: stylesTokens.textDim, opacity: 0.9 }}>
|
||||
Keine Spieler gefunden (Admin wird nicht angezeigt).
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={winnerUserId || ""}
|
||||
onChange={(e) => setWinnerUserId(e.target.value || null)}
|
||||
style={styles.input}
|
||||
>
|
||||
<option value="">— kein Sieger gesetzt —</option>
|
||||
{players.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<div style={styles.cardBody}>
|
||||
<select
|
||||
value={winnerUserId || ""}
|
||||
onChange={(e) => setWinnerUserId(e.target.value || "")}
|
||||
style={{ ...styles.input, flex: 1 }}
|
||||
>
|
||||
<option value="">— kein Sieger —</option>
|
||||
{members.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||
<button onClick={() => setWinnerUserId(null)} style={styles.secondaryBtn}>
|
||||
Leeren
|
||||
</button>
|
||||
<button onClick={onSave} style={styles.primaryBtn} disabled={!hasPlayers}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={onSave} style={styles.primaryBtn}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
|
||||
Der Sieger wird im Spiel gespeichert und ist für alle Spieler sichtbar.
|
||||
</div>
|
||||
<div style={{ padding: "0 12px 12px", fontSize: 12, color: stylesTokens.textDim, opacity: 0.9 }}>
|
||||
Nur der Host (Spiel-Ersteller) kann den Sieger setzen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user