Add game start functionality with automatic player chip generation
Implemented host-controlled game start mechanism that generates unique player chips from user names. Added `started_at` timestamp to games and new `game_chips` table to store player identifiers. Chips are created using first name initial plus first two surname letters (e.g., SNE for Sascha Nesterovic) with automatic conflict resolution. Updated frontend to display start button for hosts, show game status, and populate chip selection modal
This commit is contained in:
@@ -50,6 +50,7 @@ export default function App() {
|
||||
// Game meta
|
||||
const [gameMeta, setGameMeta] = useState(null); // {code, host_user_id, winner_email, winner_user_id}
|
||||
const [members, setMembers] = useState([]);
|
||||
const [gameChips, setGameChips] = useState([]);
|
||||
|
||||
// Winner selection (host only)
|
||||
const [winnerUserId, setWinnerUserId] = useState("");
|
||||
@@ -138,6 +139,9 @@ export default function App() {
|
||||
setGameMeta(meta);
|
||||
setWinnerUserId(meta?.winner_user_id || "");
|
||||
|
||||
const chips = meta?.started ? await api(`/games/${gameId}/chips`) : [];
|
||||
setGameChips(chips || []);
|
||||
|
||||
const mem = await api(`/games/${gameId}/members`);
|
||||
setMembers(mem);
|
||||
|
||||
@@ -327,6 +331,7 @@ export default function App() {
|
||||
setSheet(null);
|
||||
setGameMeta(null);
|
||||
setMembers([]);
|
||||
setGameChips([]);
|
||||
setWinnerUserId("");
|
||||
|
||||
// reset winner celebration on logout
|
||||
@@ -431,6 +436,7 @@ export default function App() {
|
||||
setSheet(null);
|
||||
setGameMeta(null);
|
||||
setMembers([]);
|
||||
setGameChips([]);
|
||||
setWinnerUserId("");
|
||||
setPulseId(null);
|
||||
|
||||
@@ -469,6 +475,12 @@ export default function App() {
|
||||
setGameId(res.id);
|
||||
};
|
||||
|
||||
const startGame = async () => {
|
||||
if (!gameId || !gameMeta?.host_user_id || me?.id !== gameMeta.host_user_id) return;
|
||||
await api(`/games/${gameId}/start`, { method: "POST" });
|
||||
await loadGameMeta();
|
||||
};
|
||||
|
||||
// ===== Winner =====
|
||||
const saveWinner = async () => {
|
||||
if (!gameId) return;
|
||||
@@ -501,6 +513,10 @@ export default function App() {
|
||||
const next = cycleTag(entry.note_tag);
|
||||
|
||||
if (next === "s") {
|
||||
if (!gameMeta?.started || gameChips.length === 0) {
|
||||
showSnack("Das Spiel muss zuerst gestartet werden.");
|
||||
return;
|
||||
}
|
||||
setChipEntry(entry);
|
||||
setChipOpen(true);
|
||||
return;
|
||||
@@ -642,6 +658,10 @@ export default function App() {
|
||||
members={members}
|
||||
me={me}
|
||||
hostUserId={gameMeta?.host_user_id || ""}
|
||||
isHost={isHost}
|
||||
started={!!gameMeta?.started}
|
||||
chipCount={gameChips.length}
|
||||
onStartGame={startGame}
|
||||
/>
|
||||
|
||||
{/* Sieger Badge: zwischen Spiel und Verdächtigte Person */}
|
||||
@@ -717,6 +737,7 @@ export default function App() {
|
||||
chipOpen={chipOpen}
|
||||
closeChipModalToDash={closeChipModalToDash}
|
||||
chooseChip={chooseChip}
|
||||
chips={gameChips}
|
||||
/>
|
||||
|
||||
<StatsModal
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
import { CHIP_LIST } from "../constants";
|
||||
|
||||
export default function ChipModal({ chipOpen, closeChipModalToDash, chooseChip }) {
|
||||
export default function ChipModal({ chipOpen, closeChipModalToDash, chooseChip, chips = [] }) {
|
||||
if (!chipOpen) return null;
|
||||
|
||||
return (
|
||||
@@ -19,13 +18,19 @@ export default function ChipModal({ chipOpen, closeChipModalToDash, chooseChip }
|
||||
<div style={{ marginTop: 12, color: stylesTokens.textMain }}>Chip auswählen:</div>
|
||||
|
||||
<div style={styles.chipGrid}>
|
||||
{CHIP_LIST.map((c) => (
|
||||
<button key={c} onClick={() => chooseChip(c)} style={styles.chipBtn}>
|
||||
{c}
|
||||
{chips.map((item) => (
|
||||
<button key={item.user_id || item.chip} onClick={() => chooseChip(item.chip)} style={styles.chipBtn} title={item.display_name || item.email}>
|
||||
{item.chip}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!chips.length && (
|
||||
<div style={{ marginTop: 12, color: stylesTokens.textDim }}>
|
||||
Das Spiel wurde noch nicht gestartet oder es sind keine Spieler-Chips vorhanden.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: stylesTokens.textDim }}>
|
||||
Tipp: Wenn du wieder auf den Notiz-Button klickst, geht’s von <b>s</b> zurück auf —.
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,10 @@ export default function GamePickerCard({
|
||||
members = [],
|
||||
me,
|
||||
hostUserId,
|
||||
isHost = false,
|
||||
started = false,
|
||||
chipCount = 0,
|
||||
onStartGame,
|
||||
}) {
|
||||
const cur = games.find((x) => x.id === gameId);
|
||||
|
||||
@@ -85,6 +89,24 @@ export default function GamePickerCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ padding: "0 12px 12px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
|
||||
{!started ? (
|
||||
isHost ? (
|
||||
<button onClick={onStartGame} style={styles.primaryBtn} disabled={!members.length}>
|
||||
▶ Spiel starten
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ fontSize: 12, color: stylesTokens.textDim }}>
|
||||
Warte auf den Host, der das Spiel startet.
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div style={{ fontSize: 12, color: stylesTokens.textDim }}>
|
||||
✓ Spiel läuft · {chipCount} Spieler-Chips erstellt
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Spieler */}
|
||||
{members?.length > 0 && (
|
||||
<div style={{ padding: "0 12px 12px" }}>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const API_BASE = "/api";
|
||||
export const CHIP_LIST = ["AL", "JG", "JN", "SN", "TL"];
|
||||
Reference in New Issue
Block a user