diff --git a/frontend/src/pages/Picker.jsx b/frontend/src/pages/Picker.jsx index 5b82647..72c6625 100644 --- a/frontend/src/pages/Picker.jsx +++ b/frontend/src/pages/Picker.jsx @@ -14,7 +14,14 @@ function Picker() { const [activeSession, setActiveSession] = useState(null); const [allGames, setAllGames] = useState([]); const [selectedGame, setSelectedGame] = useState(null); - const [playingGame, setPlayingGame] = useState(null); // Currently playing game + 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 [loading, setLoading] = useState(true); const [picking, setPicking] = useState(false); const [error, setError] = useState(''); @@ -92,8 +99,21 @@ function Picker() { const gamesResponse = await api.get('/games?enabled=true'); setAllGames(gamesResponse.data); - // Load currently playing game if session exists + // 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, + }); + } + } + try { const sessionGamesResponse = await api.get(`/sessions/${session.id}/games`); const playingGameEntry = sessionGamesResponse.data.find(g => g.status === 'playing'); @@ -102,6 +122,7 @@ function Picker() { } else { setPlayingGame(null); } + setHasPlayedGames(sessionGamesResponse.data.some(g => g.status === 'played')); } catch (err) { console.error('Failed to load playing game', err); } @@ -147,6 +168,29 @@ function Picker() { 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]); + + 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 handleCreateSession = async () => { try { const newSession = await api.post('/sessions', {}); @@ -158,6 +202,49 @@ function Picker() { } }; + const leadingGameRef = useRef(leadingGame); + leadingGameRef.current = leadingGame; + + const handleStartPolling = async () => { + try { + await api.post(`/sessions/${activeSession.id}/voting/start`); + pollStartedAtRef.current = new Date().toISOString(); + setPollActive(true); + setPollResult(null); + } catch (err) { + console.error('Failed to start polling', err); + } + }; + + const handleEndPolling = async () => { + try { + await api.post(`/sessions/${activeSession.id}/voting/end`); + setPollActive(false); + setTimeout(() => { + if (leadingGameRef.current) { + setPollResult(leadingGameRef.current); + } + setLeadingGame(null); + }, 1500); + } catch (err) { + console.error('Failed to end polling', err); + } + }; + + const handleUsePollResult = () => { + if (pollResult) { + const game = allGames.find(g => g.id === pollResult.gameId); + if (game) { + setSelectedGame(game); + } + } + setPollResult(null); + }; + + const handleIgnorePollResult = () => { + setPollResult(null); + }; + const loadEligibleGames = async () => { try { const params = new URLSearchParams(); @@ -687,6 +774,25 @@ function Picker() { )} + {/* Poll Leader Indicator */} + {leadingGame && ( +
+
+
+ + Poll Leader + + + {leadingGame.label} + +
+ + {leadingGame.votes} votes + +
+
+ )} + {/* Currently Playing Game Card */} {playingGame && (
@@ -744,6 +850,77 @@ function Picker() {
)} + {/* Poll Control Card */} + {pollResult ? ( +
+
+
+

+ Poll Winner: {pollResult.label} +

+

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

+
+
+ + +
+
+
+ ) : pollActive ? ( +
+
+
+

+ Voting In Progress +

+

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

+
+ +
+
+ ) : ( +
+
+
+

+ Ready to Vote +

+

+ Start a new poll for the next game. +

+
+ +
+
+ )} + {/* Selected Game Card (from dice roll) */} {selectedGame && (
@@ -931,6 +1108,9 @@ function Picker() { onGamesUpdate={gamesUpdateTrigger} playingGame={playingGame} setPlayingGame={setPlayingGame} + setHasPlayedGames={setHasPlayedGames} + setLeadingGame={setLeadingGame} + setPollActive={setPollActive} />
@@ -938,7 +1118,7 @@ function Picker() { ); } -function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame }) { +function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame, setHasPlayedGames, setLeadingGame, setPollActive }) { const { isAuthenticated, token } = useAuth(); const [games, setGames] = useState([]); const [loading, setLoading] = useState(true); @@ -973,6 +1153,8 @@ function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame }) setPlayingGame(null); } } + + setHasPlayedGames(freshGames.some(g => g.status === 'played')); } catch (err) { console.error('Failed to load session games'); } finally { @@ -1030,6 +1212,17 @@ function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame }) 'game.status', ]; + if (message.type === 'poll.leading') { + setLeadingGame(message.data); + setPollActive(true); + return; + } + + if (message.type === 'voting.ended' || message.type === 'game.started') { + setLeadingGame(null); + setPollActive(false); + } + if (reloadEvents.includes(message.type)) { console.log(`[WebSocket] ${message.type}:`, message.data); loadGames();