diff --git a/backend/app/routes/games.py b/backend/app/routes/games.py index c2b07b2..e0e90cf 100644 --- a/backend/app/routes/games.py +++ b/backend/app/routes/games.py @@ -205,6 +205,22 @@ def start_game(req: Request, game_id: str, db: Session = Depends(get_db)): return {"ok": True, "started": True} +@router.delete("/{game_id}") +def cancel_game(req: Request, game_id: str, db: Session = Depends(get_db)): + uid = require_user(req, db) + g = require_game_member(db, game_id, uid) + if g.host_user_id != uid: + raise HTTPException(403, "only host can cancel the game") + + # Remove dependent game state before deleting the game itself. + db.query(SheetState).filter(SheetState.game_id == game_id).delete(synchronize_session=False) + db.query(GameChip).filter(GameChip.game_id == game_id).delete(synchronize_session=False) + db.query(GameMember).filter(GameMember.game_id == game_id).delete(synchronize_session=False) + db.delete(g) + db.commit() + return {"ok": True, "cancelled": True} + + @router.get("/{game_id}/chips") def list_game_chips(req: Request, game_id: str, db: Session = Depends(get_db)): uid = require_user(req, db) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 52fa145..f21c6e7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 && ( diff --git a/frontend/src/components/GamePickerCard.jsx b/frontend/src/components/GamePickerCard.jsx index a78662b..a8f323f 100644 --- a/frontend/src/components/GamePickerCard.jsx +++ b/frontend/src/components/GamePickerCard.jsx @@ -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 (
@@ -169,6 +186,9 @@ export default function GamePickerCard({
{members.length < 2 ? t("atLeastTwoPlayers") : t("chipsOnStart")}
+ ) : (
@@ -179,11 +199,14 @@ export default function GamePickerCard({ {startError &&
{startError}
}
) : ( -
- {finished - ? `🏆 ${t("finished")}${winnerName ? ` · ${t("winner")}: ${winnerName}` : ""}` - : `✓ ${t("started")} · ${chipCount} ${t("chipsCreated")}`} -
+ <> +
+ {finished + ? `🏆 ${t("finished")}${winnerName ? ` · ${t("winner")}: ${winnerName}` : ""}` + : `✓ ${t("started")} · ${chipCount} ${t("chipsCreated")}`} +
+ {isHost && !finished && } + )} {/* Spieler */}