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:
2026-02-06 11:21:43 +01:00
parent d0f65b856e
commit 4669d1f8c4
9 changed files with 488 additions and 268 deletions

View File

@@ -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>
);

View 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>
);
}

View File

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

View File

@@ -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>
);
}

View File

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