import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import api from '../api/axios'; import GamePoolModal from '../components/GamePoolModal'; import RoomCodeModal from '../components/RoomCodeModal'; import { formatLocalTime } from '../utils/dateUtils'; import PopularityBadge from '../components/PopularityBadge'; function Picker() { const { isAuthenticated, loading: authLoading, token } = useAuth(); const navigate = useNavigate(); const [activeSession, setActiveSession] = useState(null); const [allGames, setAllGames] = useState([]); const [selectedGame, setSelectedGame] = useState(null); const [playingGame, setPlayingGame] = useState(null); const [hasPlayedGames, setHasPlayedGames] = useState(false); const [leadingGame, setLeadingGame] = useState(null); const [pollActive, setPollActive] = useState(false); const [pollResult, setPollResult] = useState(null); const [pollElapsed, setPollElapsed] = useState(0); const pollTimerRef = useRef(null); const pollStartedAtRef = useRef(null); const [pollEndingAt, setPollEndingAt] = useState(null); const [pollCountdown, setPollCountdown] = useState(null); const pollCountdownRef = useRef(null); const [showEndPollOptions, setShowEndPollOptions] = useState(false); const [customDelay, setCustomDelay] = useState(''); const [loading, setLoading] = useState(true); const [picking, setPicking] = useState(false); const [error, setError] = useState(''); const [showPopularity, setShowPopularity] = useState(true); const [sessionEnded, setSessionEnded] = useState(false); // Filters const [playerCount, setPlayerCount] = useState(''); const [drawingFilter, setDrawingFilter] = useState('both'); const [lengthFilter, setLengthFilter] = useState(''); const [familyFriendlyFilter, setFamilyFriendlyFilter] = useState(''); // Manual game selection const [showManualSelect, setShowManualSelect] = useState(false); const [manualGameId, setManualGameId] = useState(''); const [manualSearchQuery, setManualSearchQuery] = useState(''); const [showManualDropdown, setShowManualDropdown] = useState(false); const [filteredManualGames, setFilteredManualGames] = useState([]); // Game pool viewer const [showGamePool, setShowGamePool] = useState(false); const [eligibleGames, setEligibleGames] = useState([]); // Trigger to refresh session games list const [gamesUpdateTrigger, setGamesUpdateTrigger] = useState(0); // Mobile filters toggle const [showFilters, setShowFilters] = useState(false); // Exclude previously played games const [excludePlayedGames, setExcludePlayedGames] = useState(false); // Room code modal const [showRoomCodeModal, setShowRoomCodeModal] = useState(false); const [pendingGameAction, setPendingGameAction] = useState(null); const checkActiveSession = useCallback(async () => { try { const sessionResponse = await api.get('/sessions/active'); const session = sessionResponse.data?.session !== undefined ? sessionResponse.data.session : sessionResponse.data; // Check if session status changed setActiveSession(prevSession => { // If we had a session but now don't, mark it as ended if (prevSession && (!session || !session.id)) { setSessionEnded(true); return null; } else if (session && session.id) { setSessionEnded(false); return session; } return prevSession; }); } catch (err) { console.error('Failed to check session status', err); } }, []); const loadData = useCallback(async () => { try { // Load active session const sessionResponse = await api.get('/sessions/active'); // Handle new format { session: null } or old format (direct session object) let session = sessionResponse.data?.session !== undefined ? sessionResponse.data.session : sessionResponse.data; // Don't auto-create session - let user create it explicitly setActiveSession(session); // Load all enabled games for manual selection const gamesResponse = await api.get('/games?enabled=true'); setAllGames(gamesResponse.data); // Load currently playing game and restore poll state if session exists if (session && session.id) { // Restore poll state from persisted session data if (session.poll_active) { pollStartedAtRef.current = session.poll_started_at || null; setPollActive(true); if (session.poll_leading_game_id) { setLeadingGame({ gameId: session.poll_leading_game_id, label: session.poll_leading_label, votes: session.poll_leading_votes, }); } if (session.poll_ending_at && new Date(session.poll_ending_at) > new Date()) { setPollEndingAt(session.poll_ending_at); } } // Restore pending game selection if another admin picked one if (session.pending_game_id) { const pendingGame = gamesResponse.data.find(g => g.id === session.pending_game_id); if (pendingGame) { setSelectedGame(pendingGame); setGameSource(session.pending_game_source || 'dice'); } } try { const sessionGamesResponse = await api.get(`/sessions/${session.id}/games`); const playingGameEntry = sessionGamesResponse.data.find(g => g.status === 'playing'); if (playingGameEntry) { setPlayingGame(playingGameEntry); } else { setPlayingGame(null); } setHasPlayedGames(sessionGamesResponse.data.some(g => g.status === 'played')); } catch (err) { console.error('Failed to load playing game', err); } } } catch (err) { setError('Failed to load session data'); } finally { setLoading(false); } }, []); useEffect(() => { // Wait for auth to finish loading before checking authentication if (authLoading) return; if (!isAuthenticated) { navigate('/login'); return; } loadData(); }, [isAuthenticated, authLoading, navigate, loadData]); // Fallback poll for session status — WebSocket events handle most updates useEffect(() => { if (!isAuthenticated || authLoading) return; const interval = setInterval(() => { checkActiveSession(); }, 60000); return () => clearInterval(interval); }, [isAuthenticated, authLoading, checkActiveSession]); // Close manual game dropdown when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (showManualDropdown && !event.target.closest('.manual-search-container')) { setShowManualDropdown(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [showManualDropdown]); useEffect(() => { if (pollActive) { const start = pollStartedAtRef.current ? new Date(pollStartedAtRef.current).getTime() : Date.now(); pollTimerRef.current = setInterval(() => { setPollElapsed(Date.now() - start); }, 10); } else { clearInterval(pollTimerRef.current); setPollElapsed(0); pollStartedAtRef.current = null; } return () => clearInterval(pollTimerRef.current); }, [pollActive]); useEffect(() => { if (pollEndingAt) { const tick = () => { const remaining = Math.max(0, Math.ceil((new Date(pollEndingAt).getTime() - Date.now()) / 1000)); setPollCountdown(remaining); if (remaining <= 0) { clearInterval(pollCountdownRef.current); } }; tick(); pollCountdownRef.current = setInterval(tick, 250); return () => clearInterval(pollCountdownRef.current); } clearInterval(pollCountdownRef.current); setPollCountdown(null); }, [pollEndingAt]); const formatElapsed = (ms) => { const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); const centiseconds = Math.floor((ms % 1000) / 10); return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(centiseconds).padStart(2, '0')}`; }; const formatCountdown = (totalSeconds) => { const m = Math.floor(totalSeconds / 60); const s = totalSeconds % 60; return `${m}:${String(s).padStart(2, '0')}`; }; const handleCreateSession = async () => { try { const newSession = await api.post('/sessions', {}); setActiveSession(newSession.data); setSessionEnded(false); setError(''); } catch (err) { setError('Failed to create session'); } }; const leadingGameRef = useRef(leadingGame); leadingGameRef.current = leadingGame; const pollActiveRef = useRef(pollActive); pollActiveRef.current = pollActive; const handleStartPolling = async () => { pollStartedAtRef.current = new Date().toISOString(); setPollActive(true); setPollResult(null); try { await api.post(`/sessions/${activeSession.id}/voting/start`); } catch (err) { console.error('Failed to start polling', err); setPollActive(false); pollStartedAtRef.current = null; } }; const handleEndPolling = async (delay = 0) => { setShowEndPollOptions(false); setCustomDelay(''); if (delay === 0) { const winner = leadingGameRef.current; setPollActive(false); setLeadingGame(null); setPollEndingAt(null); if (winner) { setPollResult(winner); } try { await api.post(`/sessions/${activeSession.id}/voting/end`, { delay: 0 }); } catch (err) { console.error('Failed to end polling', err); setPollActive(true); setLeadingGame(winner); setPollResult(null); } } else { try { const res = await api.post(`/sessions/${activeSession.id}/voting/end`, { delay }); setPollEndingAt(res.data.endsAt); } catch (err) { console.error('Failed to schedule poll end', err); } } }; const handleCancelPollEnd = async () => { setPollEndingAt(null); try { await api.post(`/sessions/${activeSession.id}/voting/cancel-end`); } catch (err) { console.error('Failed to cancel poll end', err); } }; const [gameSource, setGameSource] = useState('dice'); const handleUsePollResult = () => { if (pollResult) { const game = allGames.find(g => g.id === pollResult.gameId); if (game) { setSelectedGame(game); setGameSource('poll'); if (activeSession) { api.post(`/sessions/${activeSession.id}/game-selection`, { game_id: game.id, source: 'poll' }).catch(() => {}); } } } setPollResult(null); }; const handleIgnorePollResult = () => { setPollResult(null); }; const handleDismissGame = () => { setSelectedGame(null); setGameSource('dice'); if (activeSession) { api.delete(`/sessions/${activeSession.id}/game-selection`).catch(() => {}); } }; const loadEligibleGames = async () => { try { const params = new URLSearchParams(); params.append('enabled', 'true'); if (playerCount) { params.append('playerCount', playerCount); } if (drawingFilter !== 'both') { params.append('drawing', drawingFilter); } if (lengthFilter) { params.append('length', lengthFilter); } if (familyFriendlyFilter) { params.append('familyFriendly', familyFriendlyFilter); } let games = await api.get(`/games?${params.toString()}`); let eligibleGamesList = games.data; // Apply session-based exclusions if needed if (activeSession && excludePlayedGames) { // Get all played games in this session const sessionGamesResponse = await api.get(`/sessions/${activeSession.id}/games`); const playedGameIds = sessionGamesResponse.data.map(g => g.game_id); // Filter out played games eligibleGamesList = eligibleGamesList.filter(game => !playedGameIds.includes(game.id)); } else if (activeSession) { // Default behavior: exclude last 2 games const sessionGamesResponse = await api.get(`/sessions/${activeSession.id}/games`); const recentGames = sessionGamesResponse.data.slice(-2); const recentGameIds = recentGames.map(g => g.game_id); eligibleGamesList = eligibleGamesList.filter(game => !recentGameIds.includes(game.id)); } setEligibleGames(eligibleGamesList); } catch (err) { console.error('Failed to load eligible games', err); } }; const handlePickGame = async () => { if (!activeSession) return; setPicking(true); setError(''); try { const response = await api.post('/pick', { sessionId: activeSession.id, playerCount: playerCount ? parseInt(playerCount) : undefined, drawing: drawingFilter !== 'both' ? drawingFilter : undefined, length: lengthFilter || undefined, familyFriendly: familyFriendlyFilter ? familyFriendlyFilter === 'yes' : undefined, excludePlayed: excludePlayedGames }); setSelectedGame(response.data.game); setGameSource('dice'); api.post(`/sessions/${activeSession.id}/game-selection`, { game_id: response.data.game.id, source: 'dice' }).catch(() => {}); } catch (err) { setError(err.response?.data?.error || 'Failed to pick a game'); setSelectedGame(null); setGameSource('dice'); } finally { setPicking(false); } }; const handleAcceptGame = async () => { if (!selectedGame || !activeSession) return; // Show room code modal setPendingGameAction({ type: 'accept', game: selectedGame, source: gameSource }); setShowRoomCodeModal(true); }; const handleRoomCodeConfirm = async (roomCode) => { if (!pendingGameAction || !activeSession) return; try { const { type, game, gameId, source } = pendingGameAction; if (type === 'accept' || type === 'version') { const response = await api.post(`/sessions/${activeSession.id}/games`, { game_id: gameId || game.id, manually_added: false, room_code: roomCode, source: source || 'dice' }); // Set the newly added game as playing setPlayingGame(response.data); } else if (type === 'manual') { const response = await api.post(`/sessions/${activeSession.id}/games`, { game_id: gameId, manually_added: true, room_code: roomCode, source: 'manual' }); setManualGameId(''); setShowManualSelect(false); // Set the newly added game as playing setPlayingGame(response.data); } // Close all modals and clear selected game after adding to session setSelectedGame(null); setGameSource('dice'); setShowGamePool(false); // Trigger games list refresh setGamesUpdateTrigger(prev => prev + 1); setError(''); } catch (err) { setError('Failed to add game to session'); } finally { setShowRoomCodeModal(false); setPendingGameAction(null); } }; const handleRoomCodeCancel = () => { setShowRoomCodeModal(false); setPendingGameAction(null); }; const handleAddManualGame = async () => { if (!manualGameId || !activeSession) return; // Show room code modal const game = allGames.find(g => g.id === parseInt(manualGameId)); setPendingGameAction({ type: 'manual', gameId: parseInt(manualGameId), game: game }); setShowRoomCodeModal(true); // Reset search setManualSearchQuery(''); setShowManualDropdown(false); setManualGameId(''); }; // Handle manual search input with filtering const handleManualSearchChange = useCallback((e) => { const query = e.target.value; setManualSearchQuery(query); if (query.trim().length === 0) { setFilteredManualGames([]); setShowManualDropdown(false); setManualGameId(''); return; } // Filter games by query (non-blocking) const lowerQuery = query.toLowerCase(); const filtered = allGames.filter(game => game.title.toLowerCase().includes(lowerQuery) || game.pack_name.toLowerCase().includes(lowerQuery) ).slice(0, 50); // Limit to 50 results for performance setFilteredManualGames(filtered); setShowManualDropdown(filtered.length > 0); }, [allGames]); // Handle selecting a game from the dropdown const handleSelectManualGame = useCallback((game) => { setManualGameId(game.id.toString()); setManualSearchQuery(`${game.title} (${game.pack_name})`); setShowManualDropdown(false); }, []); const handleSelectVersion = async (gameId) => { if (!activeSession) return; // Show room code modal const game = allGames.find(g => g.id === gameId); setPendingGameAction({ type: 'version', gameId: gameId, game: game }); setShowRoomCodeModal(true); }; // Find similar versions of a game based on title patterns const findSimilarVersions = (game) => { if (!game) return []; // Extract base name by removing common version patterns const baseName = game.title .replace(/\s*\d+$/, '') // Remove trailing numbers (e.g., "Game 2" -> "Game") .replace(/\s*:\s*.*$/, '') // Remove subtitle after colon .replace(/\s*\(.*?\)$/, '') // Remove parenthetical .trim(); // Find games with similar base names (but not the exact same game) return allGames.filter(g => { if (g.id === game.id) return false; // Exclude the current game if (!g.enabled) return false; // Only show enabled games const otherBaseName = g.title .replace(/\s*\d+$/, '') .replace(/\s*:\s*.*$/, '') .replace(/\s*\(.*?\)$/, '') .trim(); // Match if base names are the same (case insensitive) return otherBaseName.toLowerCase() === baseName.toLowerCase(); }); }; const similarVersions = React.useMemo(() => { return findSimilarVersions(selectedGame); }, [selectedGame, allGames]); if (authLoading || loading) { return (
Loading...
); } if (!activeSession) { return (
{sessionEnded ? ( <>

⚠️ Session Ended

The active session has been ended. To continue picking games, you'll need to create a new session.

) : ( <>

No Active Session

There is no active game session. Create a new session to start picking games.

)}
); } return (

Game Picker

{/* Picker Controls Panel */}
{/* Main Action Buttons - Above filters on mobile */}
{/* Exclude played games checkbox */}
{/* Filters */}
{/* Mobile: Collapsible header */} {/* Desktop: Always show title */}

Filters

{/* Filter content - collapsible on mobile */}
setPlayerCount(e.target.value)} className="w-full px-3 py-2 pr-8 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm text-center [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" placeholder="Any" /> {playerCount && ( )}
{/* Compact Toggle Filters */}
{/* Game Pool Modal */} {showGamePool && ( setShowGamePool(false)} /> )} {/* Room Code Modal */} {/* Results Panel */}
{error && (
{error}
)} {/* Poll Leader Indicator */} {leadingGame && (
Poll Leader {leadingGame.label}
{leadingGame.votes} votes
)} {/* Currently Playing Game Card */} {playingGame && (
🎮 Playing Now

{playingGame.title}

{playingGame.pack_name}

Players: {playingGame.min_players}-{playingGame.max_players}
Length: {playingGame.length_minutes ? `${playingGame.length_minutes} min` : 'Unknown'}
Type: {playingGame.game_type || 'N/A'}
Room Code: {playingGame.room_code || 'N/A'}
)} {/* Poll Control Card */} {pollResult ? (

Poll Winner: {pollResult.label}

{pollResult.votes} votes — Use as the next game?

) : pollActive && pollEndingAt ? (

Poll Ending

Voting will close automatically.

Ending in {pollCountdown !== null ? formatCountdown(pollCountdown) : '--:--'}
) : pollActive ? (

Voting In Progress

End the current poll when ready to pick the next game.

{showEndPollOptions && (

{[5, 10, 30].map(s => ( ))}
setCustomDelay(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { const val = parseInt(customDelay); if (val >= 1 && val <= 300) handleEndPolling(val); } }} className="w-20 px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" /> max 5m
)}
) : (

Ready to Vote

Start a new poll for the next game.

)} {/* Selected Game Card (from dice roll) */} {selectedGame && (
{/* Close/Dismiss Button */}

{selectedGame.title}

{selectedGame.pack_name}

Players: {selectedGame.min_players}-{selectedGame.max_players}
Length: {selectedGame.length_minutes ? `${selectedGame.length_minutes} min` : 'Unknown'}
Type: {selectedGame.game_type || 'N/A'}
Family Friendly: {selectedGame.family_friendly ? 'Yes' : 'No'}
Play Count: {selectedGame.play_count}
Popularity:
{/* Other Versions Suggestion */} {similarVersions.length > 0 && (

🔄 Other Versions Available

This game has multiple versions. You can choose a different one:

{similarVersions.map((version) => ( ))}
)}
)} {showManualSelect && (

Manual Game Selection

{ if (filteredManualGames.length > 0) { setShowManualDropdown(true); } }} placeholder="Type to search games..." className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400" /> {/* Autocomplete dropdown - above on mobile, below on desktop */} {showManualDropdown && filteredManualGames.length > 0 && (
{filteredManualGames.map((game) => ( ))}
)} {/* No results message - above on mobile, below on desktop */} {manualSearchQuery.trim() && filteredManualGames.length === 0 && !showManualDropdown && (
No games found matching "{manualSearchQuery}"
)}
)} {/* Session info and games */}
); } function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame, setHasPlayedGames, setLeadingGame, setPollActive, pollActiveRef, setPollResult, pollStartedAtRef, setSelectedGame, setGameSource }) { const { isAuthenticated, token } = useAuth(); const [games, setGames] = useState([]); const [loading, setLoading] = useState(true); const [confirmingRemove, setConfirmingRemove] = useState(null); const [showPopularity, setShowPopularity] = useState(true); const [editingRoomCode, setEditingRoomCode] = useState(null); const [newRoomCode, setNewRoomCode] = useState(''); const [showRepeatRoomCodeModal, setShowRepeatRoomCodeModal] = useState(false); const [repeatGameData, setRepeatGameData] = useState(null); const [wsConnection, setWsConnection] = useState(null); const [editingPlayerCount, setEditingPlayerCount] = useState(null); const [newPlayerCount, setNewPlayerCount] = useState(''); const playingGameRef = useRef(playingGame); playingGameRef.current = playingGame; const loadGames = useCallback(async () => { try { const response = await api.get(`/sessions/${sessionId}/games`); const freshGames = response.data; setGames(freshGames.reverse()); const currentPlaying = freshGames.find(g => g.status === 'playing'); const prev = playingGameRef.current; if (currentPlaying) { if (!prev || prev.id !== currentPlaying.id || prev.player_count !== currentPlaying.player_count) { setPlayingGame(currentPlaying); } } else if (prev) { const still = freshGames.find(g => g.id === prev.id); if (!still || still.status !== 'playing') { setPlayingGame(null); } } setHasPlayedGames(freshGames.some(g => g.status === 'played')); } catch (err) { console.error('Failed to load session games'); } finally { setLoading(false); } }, [sessionId, setPlayingGame]); useEffect(() => { loadGames(); }, [sessionId, onGamesUpdate, loadGames]); // Fallback polling — WebSocket events handle most updates; this is a safety net useEffect(() => { const interval = setInterval(() => { loadGames(); }, 60000); return () => clearInterval(interval); }, [loadGames]); // Setup WebSocket connection for real-time session updates (with ping + auto-reconnect) const wsRef = useRef(null); const pingIntervalRef = useRef(null); const reconnectTimeoutRef = useRef(null); const connectWs = useCallback(() => { if (!token) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.hostname}:${window.location.port || (window.location.protocol === 'https:' ? 443 : 80)}/api/sessions/live`; try { const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { console.log('[WebSocket] Connected, authenticating...'); ws.send(JSON.stringify({ type: 'auth', token })); }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'auth_success') { console.log('[WebSocket] Authenticated, subscribing to session', sessionId); ws.send(JSON.stringify({ type: 'subscribe', sessionId: parseInt(sessionId) })); clearInterval(pingIntervalRef.current); pingIntervalRef.current = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); return; } const reloadEvents = [ 'room.connected', 'lobby.player-joined', 'lobby.updated', 'game.started', 'game.ended', 'room.disconnected', 'player-count.updated', 'game.added', 'game.status', ]; if (message.type === 'poll.start') { pollStartedAtRef.current = message.data.pollStartedAt || new Date().toISOString(); setPollActive(true); setPollResult(null); setLeadingGame(null); setPollEndingAt(null); setShowEndPollOptions(false); return; } if (message.type === 'poll.ending') { setPollEndingAt(message.data.endsAt); setShowEndPollOptions(false); return; } if (message.type === 'poll.ending.cancelled') { setPollEndingAt(null); return; } if (message.type === 'poll.leading') { if (pollActiveRef.current) { setLeadingGame(message.data); } return; } if (message.type === 'voting.ended') { setLeadingGame(null); setPollActive(false); setPollEndingAt(null); setShowEndPollOptions(false); if (message.data.winnerGameId) { setPollResult({ gameId: message.data.winnerGameId, label: message.data.winnerLabel, votes: message.data.winnerVotes }); } } if (message.type === 'game.started') { setLeadingGame(null); setPollActive(false); setPollEndingAt(null); } if (message.type === 'game.picked') { setSelectedGame(message.data.game); setGameSource(message.data.source || 'dice'); return; } if (message.type === 'game.dismissed') { setSelectedGame(null); setGameSource('dice'); return; } if (reloadEvents.includes(message.type)) { console.log(`[WebSocket] ${message.type}:`, message.data); if (message.type === 'game.added') { setSelectedGame(null); setGameSource('dice'); } loadGames(); } } catch (error) { console.error('[WebSocket] Error parsing message:', error); } }; ws.onerror = (error) => { console.error('[WebSocket] Error:', error); }; ws.onclose = () => { console.log('[WebSocket] Disconnected, reconnecting in 3s...'); clearInterval(pingIntervalRef.current); reconnectTimeoutRef.current = setTimeout(connectWs, 3000); }; setWsConnection(ws); } catch (error) { console.error('[WebSocket] Failed to connect:', error); reconnectTimeoutRef.current = setTimeout(connectWs, 3000); } }, [sessionId, token, loadGames, setPollActive, setPollResult, setLeadingGame, pollActiveRef, pollStartedAtRef, setSelectedGame, setGameSource]); useEffect(() => { connectWs(); return () => { clearTimeout(reconnectTimeoutRef.current); clearInterval(pingIntervalRef.current); if (wsRef.current) { wsRef.current.onclose = null; wsRef.current.close(); } }; }, [connectWs]); const handleUpdateStatus = async (gameId, newStatus) => { try { await api.patch(`/sessions/${sessionId}/games/${gameId}/status`, { status: newStatus }); // If we're changing the playing game's status, clear it from the playing card if (playingGame && playingGame.id === gameId && newStatus !== 'playing') { setPlayingGame(null); } loadGames(); // Reload to get updated statuses } catch (err) { console.error('Failed to update game status', err); } }; const handleRemoveClick = (gameId) => { if (confirmingRemove === gameId) { // Second click - actually remove handleRemoveGame(gameId); } else { // First click - show confirmation setConfirmingRemove(gameId); // Reset after 3 seconds setTimeout(() => setConfirmingRemove(null), 3000); } }; const handleRemoveGame = async (gameId) => { try { await api.delete(`/sessions/${sessionId}/games/${gameId}`); // If we're removing the playing game, clear it from the playing card if (playingGame && playingGame.id === gameId) { setPlayingGame(null); } setConfirmingRemove(null); loadGames(); // Reload after deletion } catch (err) { console.error('Failed to remove game', err); setConfirmingRemove(null); } }; const handleEditRoomCode = (gameId, currentCode) => { setEditingRoomCode(gameId); setNewRoomCode(currentCode || ''); }; const handleRoomCodeChange = (e) => { const value = e.target.value.toUpperCase(); const filtered = value.replace(/[^A-Z0-9]/g, '').slice(0, 4); setNewRoomCode(filtered); }; const handleSaveRoomCode = async (gameId) => { if (newRoomCode.length !== 4) { return; } try { await api.patch(`/sessions/${sessionId}/games/${gameId}/room-code`, { room_code: newRoomCode }); setEditingRoomCode(null); setNewRoomCode(''); loadGames(); // Reload to show updated code } catch (err) { console.error('Failed to update room code', err); } }; const handleCancelEditRoomCode = () => { setEditingRoomCode(null); setNewRoomCode(''); }; const handleRepeatGame = (game) => { // Store the game data and open the room code modal setRepeatGameData(game); setShowRepeatRoomCodeModal(true); }; const handleRepeatRoomCodeConfirm = async (roomCode) => { if (!repeatGameData) return; try { const response = await api.post(`/sessions/${sessionId}/games`, { game_id: repeatGameData.game_id, manually_added: false, room_code: roomCode }); // Set the newly added game as playing setPlayingGame(response.data); setShowRepeatRoomCodeModal(false); setRepeatGameData(null); loadGames(); // Reload to show the new game } catch (err) { console.error('Failed to repeat game', err); } }; const handleRepeatRoomCodeCancel = () => { setShowRepeatRoomCodeModal(false); setRepeatGameData(null); }; const handleStopPlayerCountCheck = async (gameId) => { try { await api.post(`/sessions/${sessionId}/games/${gameId}/stop-player-check`); loadGames(); // Reload to show updated status } catch (err) { console.error('Failed to stop player count check', err); } }; const handleRetryPlayerCount = async (gameId, roomCode) => { if (!roomCode) return; try { await api.post(`/sessions/${sessionId}/games/${gameId}/start-player-check`); loadGames(); // Reload to show checking status } catch (err) { console.error('Failed to start player count check', err); } }; const handleEditPlayerCount = (gameId, currentCount) => { setEditingPlayerCount(gameId); setNewPlayerCount(currentCount?.toString() || ''); }; const handleSavePlayerCount = async (gameId) => { const count = parseInt(newPlayerCount); if (isNaN(count) || count < 0) { return; } try { await api.patch(`/sessions/${sessionId}/games/${gameId}/player-count`, { player_count: count }); setEditingPlayerCount(null); setNewPlayerCount(''); loadGames(); // Reload to show updated count } catch (err) { console.error('Failed to update player count', err); } }; const handleCancelEditPlayerCount = () => { setEditingPlayerCount(null); setNewPlayerCount(''); }; const getStatusBadge = (status) => { if (status === 'playing') { return ( 🎮 Playing ); } if (status === 'skipped') { return ( ⏭️ Skipped ); } return null; }; return ( <> {/* Room Code Modal for Repeat Game */}

Games Played This Session ({games.length})

{loading ? (

Loading...

) : games.length === 0 ? (

No games played yet. Pick a game to get started!

) : (
{games.map((game, index) => { const displayNumber = games.length - index; return (
{displayNumber}. {game.title} {getStatusBadge(game.status)} {game.source === 'manual' || (game.manually_added === 1 && game.source !== 'poll') ? ( Manual ) : null} {game.source === 'poll' && ( Poll )} {game.room_code && (
{editingRoomCode === game.id ? (
) : ( <> 🎮 {game.room_code} {isAuthenticated && ( )} )} {/* Player Count Display */} {game.player_count_check_status && game.player_count_check_status !== 'not_started' && (
{game.player_count_check_status === 'monitoring' && !game.player_count && ( 📡 Monitoring... )} {(game.player_count_check_status === 'checking' || (game.player_count_check_status === 'monitoring' && game.player_count)) && ( 📡 {game.player_count ? `${game.player_count} players` : 'Monitoring...'} )} {game.player_count_check_status === 'completed' && game.player_count && ( <> {editingPlayerCount === game.id ? (
setNewPlayerCount(e.target.value)} className="w-12 px-2 py-1 text-xs text-center border border-green-400 dark:border-green-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-green-500" min="0" autoFocus />
) : ( <> ✓ {game.player_count} players {isAuthenticated && ( )} )} )} {game.player_count_check_status === 'failed' && ( <> {editingPlayerCount === game.id ? (
setNewPlayerCount(e.target.value)} className="w-12 px-2 py-1 text-xs text-center border border-orange-400 dark:border-orange-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-500" min="0" autoFocus />
) : ( <> {isAuthenticated && ( )} )} )} {/* Stop button for active checks */} {isAuthenticated && (game.player_count_check_status === 'monitoring' || game.player_count_check_status === 'checking') && ( )}
)}
)} {showPopularity && ( )}
{game.pack_name} • {formatLocalTime(game.played_at)}
{/* Action buttons for admins */} {isAuthenticated && (
{game.status !== 'playing' && ( )} {game.status === 'playing' && ( )} {game.status !== 'skipped' && ( )}
)}
); })}
)}
); } export default Picker;