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
This commit is contained in:
+41
-1
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import WinnerCelebration from "./components/WinnerCelebration";
|
||||
import GameStartCelebration from "./components/GameStartCelebration";
|
||||
|
||||
import { api } from "./api/client";
|
||||
import { cycleTag } from "./utils/cycleTag";
|
||||
@@ -91,10 +92,13 @@ export default function App() {
|
||||
// ===== Winner Celebration =====
|
||||
const [celebrateOpen, setCelebrateOpen] = useState(false);
|
||||
const [celebrateName, setCelebrateName] = useState("");
|
||||
const [startCelebrateOpen, setStartCelebrateOpen] = useState(false);
|
||||
|
||||
// baseline per game: beim ersten Meta-Load NICHT feiern
|
||||
const winnerBaselineRef = useRef(false);
|
||||
const lastWinnerIdRef = useRef(null);
|
||||
const gameStartBaselineRef = useRef(false);
|
||||
const lastGameStartedRef = useRef(false);
|
||||
|
||||
const showSnack = (msg) => {
|
||||
setSnack(msg);
|
||||
@@ -212,8 +216,11 @@ export default function App() {
|
||||
// reset winner celebration baseline when switching games
|
||||
winnerBaselineRef.current = false;
|
||||
lastWinnerIdRef.current = null;
|
||||
gameStartBaselineRef.current = false;
|
||||
lastGameStartedRef.current = false;
|
||||
setCelebrateOpen(false);
|
||||
setCelebrateName("");
|
||||
setStartCelebrateOpen(false);
|
||||
|
||||
(async () => {
|
||||
if (!gameId) return;
|
||||
@@ -288,6 +295,23 @@ export default function App() {
|
||||
}
|
||||
}, [gameMeta?.winner_user_id, gameMeta?.winner_display_name, gameMeta?.winner_email]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gameMeta) return;
|
||||
|
||||
const started = !!gameMeta.started;
|
||||
if (!gameStartBaselineRef.current) {
|
||||
gameStartBaselineRef.current = true;
|
||||
lastGameStartedRef.current = started;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastGameStartedRef.current && started) {
|
||||
setStartCelebrateOpen(true);
|
||||
vibrate([25, 55, 25]);
|
||||
}
|
||||
lastGameStartedRef.current = started;
|
||||
}, [gameMeta?.started]);
|
||||
|
||||
|
||||
// ===== Auth actions =====
|
||||
const doLogin = async () => {
|
||||
@@ -342,8 +366,11 @@ export default function App() {
|
||||
// reset winner celebration on logout
|
||||
winnerBaselineRef.current = false;
|
||||
lastWinnerIdRef.current = null;
|
||||
gameStartBaselineRef.current = false;
|
||||
lastGameStartedRef.current = false;
|
||||
setCelebrateOpen(false);
|
||||
setCelebrateName("");
|
||||
setStartCelebrateOpen(false);
|
||||
};
|
||||
|
||||
// ===== Password =====
|
||||
@@ -452,8 +479,11 @@ export default function App() {
|
||||
// reset winner celebration baseline for the new game
|
||||
winnerBaselineRef.current = false;
|
||||
lastWinnerIdRef.current = null;
|
||||
gameStartBaselineRef.current = false;
|
||||
lastGameStartedRef.current = false;
|
||||
setCelebrateOpen(false);
|
||||
setCelebrateName("");
|
||||
setStartCelebrateOpen(false);
|
||||
|
||||
const g = await api("/games", {
|
||||
method: "POST",
|
||||
@@ -498,6 +528,7 @@ export default function App() {
|
||||
|
||||
// ===== Sheet actions =====
|
||||
const cycleStatus = async (entry) => {
|
||||
if (gameMeta?.winner_user_id) return;
|
||||
let next = 0;
|
||||
if (entry.status === 0) next = 2;
|
||||
else if (entry.status === 2) next = 1;
|
||||
@@ -510,11 +541,13 @@ export default function App() {
|
||||
});
|
||||
|
||||
await reloadSheet();
|
||||
vibrate(10);
|
||||
setPulseId(entry.entry_id);
|
||||
setTimeout(() => setPulseId(null), 220);
|
||||
};
|
||||
|
||||
const toggleTag = async (entry) => {
|
||||
if (gameMeta?.winner_user_id) return;
|
||||
const next = cycleTag(entry.note_tag);
|
||||
|
||||
if (next === "s") {
|
||||
@@ -535,10 +568,11 @@ export default function App() {
|
||||
});
|
||||
|
||||
await reloadSheet();
|
||||
vibrate(10);
|
||||
};
|
||||
|
||||
const chooseChip = async (chip) => {
|
||||
if (!chipEntry) return;
|
||||
if (!chipEntry || gameMeta?.winner_user_id) return;
|
||||
|
||||
const entry = chipEntry;
|
||||
setChipOpen(false);
|
||||
@@ -553,6 +587,7 @@ export default function App() {
|
||||
});
|
||||
} finally {
|
||||
await reloadSheet();
|
||||
vibrate(12);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -637,6 +672,10 @@ export default function App() {
|
||||
winnerName={celebrateName}
|
||||
onClose={() => setCelebrateOpen(false)}
|
||||
/>
|
||||
<GameStartCelebration
|
||||
open={startCelebrateOpen}
|
||||
onClose={() => setStartCelebrateOpen(false)}
|
||||
/>
|
||||
|
||||
<div style={styles.bgFixed} aria-hidden="true">
|
||||
<div style={styles.bgMap} />
|
||||
@@ -694,6 +733,7 @@ export default function App() {
|
||||
onCycleStatus={cycleStatus}
|
||||
onToggleTag={toggleTag}
|
||||
displayTag={displayTag}
|
||||
readOnly={!!gameMeta?.winner_user_id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user