Add game cancellation feature with cascade deletion and host-only access control

Implemented DELETE `/games/{game_id}` endpoint allowing hosts to cancel games with cascade deletion of sheet states, chips, and memberships. Added `cancelGame` handler in App.jsx to delete game, refresh game list, reset state, and show confirmation snack. Added cancel button to GamePickerCard lobby section (pre-start) and started section (non-finished games) with bilingual confirmation dialog, loading state, and error handling.
This commit is contained in:
2026-08-02 10:46:38 +02:00
parent 565d538c35
commit b20056913b
3 changed files with 61 additions and 5 deletions
+17
View File
@@ -523,6 +523,22 @@ export default function App() {
await loadGameMeta();
};
const cancelGame = async () => {
if (!gameId || !isHost) return;
await api(`/games/${gameId}`, { method: "DELETE" });
const nextGames = await api("/games");
setGames(nextGames);
setGameId(nextGames[0]?.id || null);
setSheet(null);
setGameMeta(null);
setMembers([]);
setGameChips([]);
setWinnerUserId("");
setChipOpen(false);
setChipEntry(null);
showSnack(language === "en" ? "Game cancelled." : "Spiel abgebrochen.");
};
// ===== Winner =====
const saveWinner = async () => {
if (!gameId) return;
@@ -733,6 +749,7 @@ export default function App() {
winnerName={gameMeta?.winner_display_name || gameMeta?.winner_email || ""}
chipCount={gameChips.length}
onStartGame={startGame}
onCancelGame={cancelGame}
/>
{gameStarted && (
+28 -5
View File
@@ -17,11 +17,13 @@ export default function GamePickerCard({
winnerName = "",
chipCount = 0,
onStartGame,
onCancelGame,
}) {
const { language, t } = useLanguage();
const cur = games.find((x) => x.id === gameId);
const [starting, setStarting] = useState(false);
const [startError, setStartError] = useState("");
const [cancelling, setCancelling] = useState(false);
const renderMemberName = (m) => {
const base = ((m.display_name || "").trim() || (m.email || "").trim() || "—");
@@ -67,6 +69,21 @@ export default function GamePickerCard({
}
};
const handleCancelGame = async () => {
if (!onCancelGame || cancelling) return;
const confirmed = window.confirm(language === "en" ? "Cancel and delete this game? All game data will be removed." : "Dieses Spiel wirklich abbrechen und löschen? Alle Spieldaten werden entfernt.");
if (!confirmed) return;
setCancelling(true);
setStartError("");
try {
await onCancelGame();
} catch (e) {
setStartError(e?.message || (language === "en" ? "The game could not be cancelled." : "Das Spiel konnte nicht abgebrochen werden."));
} finally {
setCancelling(false);
}
};
return (
<div style={{ marginTop: 14 }}>
<div style={styles.card}>
@@ -169,6 +186,9 @@ export default function GamePickerCard({
<div style={{ marginTop: 7, textAlign: "center", color: stylesTokens.textDim, fontSize: 11 }}>
{members.length < 2 ? t("atLeastTwoPlayers") : t("chipsOnStart")}
</div>
<button onClick={handleCancelGame} style={{ ...styles.secondaryBtn, width: "100%", marginTop: 9, color: "#ffb3b3" }} disabled={cancelling}>
{cancelling ? (language === "en" ? "Cancelling …" : "Wird abgebrochen …") : (language === "en" ? "✕ Cancel game" : "✕ Spiel abbrechen")}
</button>
</>
) : (
<div style={{ marginTop: 11, padding: "9px 10px", borderRadius: 11, background: "rgba(233,216,166,0.07)", color: stylesTokens.textDim, fontSize: 12, textAlign: "center" }}>
@@ -179,11 +199,14 @@ export default function GamePickerCard({
{startError && <div style={{ marginTop: 8, color: "#ffb3b3", fontSize: 12 }}>{startError}</div>}
</div>
) : (
<div style={{ margin: "0 12px 12px", padding: "10px 12px", borderRadius: 13, border: `1px solid ${stylesTokens.panelBorder}`, background: finished ? "rgba(233,216,166,0.09)" : "rgba(124,255,182,0.07)", color: stylesTokens.textDim, fontSize: 12 }}>
{finished
? `🏆 ${t("finished")}${winnerName ? ` · ${t("winner")}: ${winnerName}` : ""}`
: ` ${t("started")} · ${chipCount} ${t("chipsCreated")}`}
</div>
<>
<div style={{ margin: "0 12px 12px", padding: "10px 12px", borderRadius: 13, border: `1px solid ${stylesTokens.panelBorder}`, background: finished ? "rgba(233,216,166,0.09)" : "rgba(124,255,182,0.07)", color: stylesTokens.textDim, fontSize: 12 }}>
{finished
? `🏆 ${t("finished")}${winnerName ? ` · ${t("winner")}: ${winnerName}` : ""}`
: `${t("started")} · ${chipCount} ${t("chipsCreated")}`}
</div>
{isHost && !finished && <button onClick={handleCancelGame} style={{ ...styles.secondaryBtn, margin: "0 12px 12px", width: "calc(100% - 24px)", color: "#ffb3b3" }} disabled={cancelling}>{cancelling ? (language === "en" ? "Cancelling …" : "Wird abgebrochen …") : (language === "en" ? "✕ Cancel game" : "✕ Spiel abbrechen")}</button>}
</>
)}
{/* Spieler */}