import React, { useState, useEffect, useCallback } 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); // Currently playing game 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 if session exists if (session && session.id) { 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); } } 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]); const handleCreateSession = async () => { try { const newSession = await api.post('/sessions', {}); setActiveSession(newSession.data); setSessionEnded(false); setError(''); } catch (err) { setError('Failed to create session'); } }; 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); } catch (err) { setError(err.response?.data?.error || 'Failed to pick a game'); setSelectedGame(null); } finally { setPicking(false); } }; const handleAcceptGame = async () => { if (!selectedGame || !activeSession) return; // Show room code modal setPendingGameAction({ type: 'accept', game: selectedGame }); setShowRoomCodeModal(true); }; const handleRoomCodeConfirm = async (roomCode) => { if (!pendingGameAction || !activeSession) return; try { const { type, game, gameId } = 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 }); // 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 }); 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); 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}
)} {/* 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'}
)} {/* 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 }) { 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 loadGames = useCallback(async () => { try { const response = await api.get(`/sessions/${sessionId}/games`); // Reverse chronological order (most recent first) setGames(response.data.reverse()); } catch (err) { console.error('Failed to load session games'); } finally { setLoading(false); } }, [sessionId]); 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 useEffect(() => { 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); 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) })); return; } const reloadEvents = [ 'room.connected', 'lobby.player-joined', 'lobby.updated', 'game.started', 'game.ended', 'room.disconnected', 'player-count.updated', 'game.added', 'game.status', ]; if (reloadEvents.includes(message.type)) { console.log(`[WebSocket] ${message.type}:`, message.data); loadGames(); } } catch (error) { console.error('[WebSocket] Error parsing message:', error); } }; ws.onerror = (error) => { console.error('[WebSocket] Error:', error); }; ws.onclose = () => { console.log('[WebSocket] Disconnected'); }; setWsConnection(ws); return () => { if (ws.readyState === WebSocket.OPEN) { ws.close(); } }; } catch (error) { console.error('[WebSocket] Failed to connect:', error); } }, [sessionId, token, loadGames]); 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.manually_added === 1 && ( Manual )} {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;