From a8335e20341bebcacde8874b8cef3ca1136d1577 Mon Sep 17 00:00:00 2001 From: nessi Date: Sun, 2 Aug 2026 09:28:16 +0200 Subject: [PATCH] Prevent sheet editing after game completion and add game start celebration with enhanced splash screen animations Added read-only enforcement for finished games by checking `winner_user_id` in backend PATCH endpoint and frontend sheet interactions. Implemented GameStartCelebration component with confetti burst, animated overlay card, and pulsing rune icon. Enhanced splash screen with orbiting border animation, sparkle effects, and title rise transition. Added haptic feedback (vibration) to sheet --- backend/app/routes/games.py | 3 ++ frontend/index.html | 47 +++++++++++++++++++ frontend/src/App.jsx | 42 ++++++++++++++++- .../src/components/GameStartCelebration.jsx | 45 ++++++++++++++++++ frontend/src/components/SheetSection.jsx | 11 +++-- .../src/styles/hooks/useHpGlobalStyles.js | 14 ++++++ 6 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/GameStartCelebration.jsx diff --git a/backend/app/routes/games.py b/backend/app/routes/games.py index b41ccf0..c2b07b2 100644 --- a/backend/app/routes/games.py +++ b/backend/app/routes/games.py @@ -308,6 +308,9 @@ def patch_sheet(req: Request, game_id: str, entry_id: str, data: dict, db: Sessi uid = require_user(req, db) g = require_game_member(db, game_id, uid) + if g.winner_user_id: + raise HTTPException(403, "game finished; sheet is read-only") + status = data.get("status") note_tag = data.get("note_tag") chip = data.get("chip") diff --git a/frontend/index.html b/frontend/index.html index b924493..bf1b8bf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -94,6 +94,36 @@ overflow: hidden; } + .splash-card::after { + content: ""; + position: absolute; + inset: 18% 28%; + border: 1px solid rgba(233, 216, 166, 0.24); + border-radius: 50%; + transform: rotate(-18deg); + animation: orbit 2.8s ease-in-out infinite; + pointer-events: none; + } + + .magic-orbit { + position: relative; + width: 74px; + height: 42px; + margin: 0 auto 4px; + } + + .magic-orbit::before, + .magic-orbit::after { + content: "✦"; + position: absolute; + color: rgba(233, 216, 166, 0.92); + font-size: 16px; + animation: sparkle 1.4s ease-in-out infinite; + } + + .magic-orbit::before { left: 8px; top: 14px; } + .magic-orbit::after { right: 8px; top: 3px; animation-delay: .55s; } + .splash-card::before { content: ""; position: absolute; @@ -111,6 +141,7 @@ color: rgba(245, 239, 220, 0.92); font-size: 18px; line-height: 1.25; + animation: titleRise 700ms ease-out both; } .splash-sub { @@ -156,6 +187,21 @@ 0%, 100% { opacity: 0.35; transform: scaleX(0.92); } 50% { opacity: 0.85; transform: scaleX(1.02); } } + + @keyframes orbit { + 0%, 100% { transform: rotate(-18deg) scale(.94); opacity: .45; } + 50% { transform: rotate(18deg) scale(1.06); opacity: .9; } + } + + @keyframes sparkle { + 0%, 100% { transform: translateY(3px) scale(.65); opacity: .25; } + 50% { transform: translateY(-4px) scale(1.2); opacity: 1; } + } + + @keyframes titleRise { + from { opacity: 0; transform: translateY(8px); letter-spacing: .18em; } + to { opacity: 1; transform: translateY(0); letter-spacing: .06em; } + } @@ -170,6 +216,7 @@