Integrate join codes, player management, and themes
This update introduces "join codes" for games to simplify game joining. Enhancements include player role and winner management for better organization. Additionally, theme preferences are now user-configurable and persisted server-side.
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
// 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,
|
||||
onOpenHelp,
|
||||
}) {
|
||||
export default function GamePickerCard({ games, gameId, setGameId, joinCode, onOpenHelp }) {
|
||||
return (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={styles.card}>
|
||||
@@ -30,6 +26,19 @@ export default function GamePickerCard({
|
||||
Hilfe
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
74
frontend/src/components/JoinGameModal.jsx
Normal file
74
frontend/src/components/JoinGameModal.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// src/components/JoinGameModal.jsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function JoinGameModal({ open, onClose, onJoin }) {
|
||||
const [code, setCode] = useState("");
|
||||
const [msg, setMsg] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setCode("");
|
||||
setMsg("");
|
||||
setBusy(false);
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const doJoin = async () => {
|
||||
const c = (code || "").trim();
|
||||
if (!c) return setMsg("❌ Bitte Code eingeben.");
|
||||
setBusy(true);
|
||||
setMsg("");
|
||||
try {
|
||||
await onJoin(c);
|
||||
} catch (e) {
|
||||
setMsg("❌ Fehler: " + (e?.message || "unknown"));
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 beitreten</div>
|
||||
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Schließen">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
|
||||
<input
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="z.B. 123456"
|
||||
style={styles.input}
|
||||
inputMode="numeric"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") doJoin();
|
||||
}}
|
||||
/>
|
||||
|
||||
{msg && <div style={{ opacity: 0.92, color: stylesTokens.textMain }}>{msg}</div>}
|
||||
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
|
||||
<button onClick={onClose} style={styles.secondaryBtn} disabled={busy}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onClick={doJoin} style={styles.primaryBtn} disabled={busy}>
|
||||
{busy ? "Beitreten..." : "Beitreten"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
|
||||
Tipp: Der Spiel-Code steht beim Host unter dem Spiel-Dropdown.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,16 +8,19 @@ export default function TopBar({
|
||||
setUserMenuOpen,
|
||||
openPwModal,
|
||||
openDesignModal,
|
||||
openJoinModal,
|
||||
doLogout,
|
||||
newGame,
|
||||
}) {
|
||||
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>
|
||||
|
||||
{/* 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ü">
|
||||
@@ -28,13 +31,14 @@ 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 rgba(233,216,166,0.12)",
|
||||
borderBottom: `1px solid ${stylesTokens.goldLine}`,
|
||||
}}
|
||||
>
|
||||
{me.email}
|
||||
@@ -44,10 +48,26 @@ export default function TopBar({
|
||||
Passwort setzen
|
||||
</button>
|
||||
|
||||
<button onClick={openDesignModal} style={styles.userDropdownItem}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
openDesignModal();
|
||||
}}
|
||||
style={styles.userDropdownItem}
|
||||
>
|
||||
Design ändern
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
openJoinModal();
|
||||
}}
|
||||
style={styles.userDropdownItem}
|
||||
>
|
||||
Spiel beitreten
|
||||
</button>
|
||||
|
||||
<div style={styles.userDropdownDivider} />
|
||||
|
||||
<button
|
||||
@@ -64,7 +84,7 @@ export default function TopBar({
|
||||
</div>
|
||||
|
||||
<button onClick={newGame} style={styles.primaryBtn}>
|
||||
✦ New Game
|
||||
✦ Neues Spiel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,27 @@
|
||||
// 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;
|
||||
export default function WinnerBadge({ winnerEmail }) {
|
||||
if (!winnerEmail) 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
|
||||
style={{
|
||||
marginTop: 14,
|
||||
padding: "12px 14px",
|
||||
borderRadius: 16,
|
||||
border: `1px solid ${stylesTokens.panelBorder}`,
|
||||
background: stylesTokens.panelBg,
|
||||
boxShadow: "0 12px 30px rgba(0,0,0,0.45)",
|
||||
backdropFilter: "blur(6px)",
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16 }}>🏆</span>
|
||||
<span style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Sieger:</span>
|
||||
<span style={{ color: stylesTokens.textMain }}>{winnerEmail}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
// src/components/WinnerCard.jsx
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function WinnerCard({ value, setValue, onSave }) {
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<button onClick={onSave} style={styles.primaryBtn} title="Speichern">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div style={{ padding: "0 12px 12px", fontSize: 12, color: stylesTokens.textDim }}>
|
||||
Wird pro Spiel lokal gespeichert.
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user