WP-17. Minor: push notifications become usable and nothing a player could reach was removed. The admin console's jobs need backend 3.0.0. Push (M35): the whole browser half - the service worker's handlers, the subscribe/unsubscribe calls, the server's VAPID key - had shipped with nothing calling it, so no browser ever held a subscription. The toggle lives in Profile because the permission prompt has to come from a click. The old isPushSubscribed awaited serviceWorker.ready, which never settles with no worker registered; it uses getRegistration() now. Admin: each job pages and the button becomes Continue with the server's cursor; the recalculate card also stopped printing "undefined leaderboard entries" for a field the server had not sent for several releases. The three copy-pasted job cards are one component. Consolidation: the four game screens share use-game-screen and one search panel; the review page's static chain is ChainDisplay in a static mode (21 rendered states verified byte-identical); formatTime and error extraction exist once; Endless and quick play share a franchise-aware picker instead of two divergent sort(() => Math.random() - 0.5) copies. The dead api/index barrel and the fourteen functions only it referenced are gone. noUncheckedIndexedAccess is on. Whiplash's TMDB id is corrected and every preset title is checked against TMDB.
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useNavigate } from 'react-router';
|
|
import { useGameStore } from '@/stores/game-store';
|
|
import { useGameNightStore } from '@/stores/game-night-store';
|
|
import { useGameScreen } from '@/hooks/use-game-screen';
|
|
import PageLayout from '@/components/layout/PageLayout';
|
|
import GameHeader from '@/components/game/GameHeader';
|
|
import ChainDisplay from '@/components/game/ChainDisplay';
|
|
import GameSearchPanel from '@/components/game/GameSearchPanel';
|
|
import PlayersProgress from '@/components/game-night/PlayersProgress';
|
|
import GameNightCompletionModal from '@/components/game-night/GameNightCompletionModal';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Flag } from 'lucide-react';
|
|
|
|
export default function GameNightPlay() {
|
|
const screen = useGameScreen();
|
|
const { status, chain } = screen;
|
|
const score = useGameStore((s) => s.score);
|
|
const navigate = useNavigate();
|
|
|
|
const sendChainUpdate = useGameNightStore((s) => s.sendChainUpdate);
|
|
const sendPlayerComplete = useGameNightStore((s) => s.sendPlayerComplete);
|
|
const sessionId = useGameStore((s) => s.sessionId);
|
|
const sendPlayerForfeit = useGameNightStore((s) => s.sendPlayerForfeit);
|
|
const isSpectator = useGameNightStore((s) => s.isSpectator);
|
|
const lobbyState = useGameNightStore((s) => s.lobbyState);
|
|
const roomId = useGameNightStore((s) => s.roomId);
|
|
|
|
// Broadcast chain updates
|
|
useEffect(() => {
|
|
if (chain.length > 0 && !isSpectator) {
|
|
sendChainUpdate(chain.length);
|
|
}
|
|
}, [chain.length, sendChainUpdate, isSpectator]);
|
|
|
|
// Send completion when game completes
|
|
useEffect(() => {
|
|
if (status === 'completed' && score && sessionId && !isSpectator) {
|
|
sendPlayerComplete(sessionId);
|
|
}
|
|
}, [status, score, sessionId, sendPlayerComplete, isSpectator]);
|
|
|
|
useEffect(() => {
|
|
if (status === 'idle') {
|
|
navigate('/game-night');
|
|
}
|
|
}, [status, navigate]);
|
|
|
|
// Navigate to results when room finishes
|
|
useEffect(() => {
|
|
if (lobbyState === 'finished' && roomId) {
|
|
navigate(`/game-night/${roomId}/results`);
|
|
}
|
|
}, [lobbyState, roomId, navigate]);
|
|
|
|
if (!screen.ready) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<PageLayout>
|
|
<div className="flex gap-6">
|
|
{/* Main game area */}
|
|
<div className="flex-1 space-y-4">
|
|
<GameHeader />
|
|
|
|
<Separator />
|
|
|
|
<ChainDisplay />
|
|
|
|
{status === 'playing' && !isSpectator && (
|
|
<GameSearchPanel screen={screen}>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={sendPlayerForfeit}
|
|
>
|
|
<Flag className="mr-1 h-3.5 w-3.5" />
|
|
I Give Up
|
|
</Button>
|
|
</GameSearchPanel>
|
|
)}
|
|
</div>
|
|
|
|
{/* Players sidebar */}
|
|
<div className="hidden w-64 shrink-0 lg:block">
|
|
<PlayersProgress />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile players progress */}
|
|
<div className="mt-4 lg:hidden">
|
|
<PlayersProgress />
|
|
</div>
|
|
|
|
<GameNightCompletionModal />
|
|
</PageLayout>
|
|
);
|
|
}
|