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:
@@ -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 && (
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user