// Barcode Games Panel // Purpose: Shows barcode game selection, current game status, and player points in the Activities tab. // Scope: Keeps the driver-side UI compact but informative; game-specific rules stay in server game modules. import { useEffect, useMemo, useState } from 'react'; import useBarcodeGameState from '../../barcodeGames/useBarcodeGameState.js'; import CardFrame from '../CardFrame/index.jsx'; function useClock(enabled) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!enabled) return undefined; const timer = window.setInterval(() => { setNow(Date.now()); }, 500); return () => window.clearInterval(timer); }, [enabled]); return now; } function formatTimer(endsAt, now) { if (!Number.isFinite(endsAt)) return null; const totalSeconds = Math.max(0, Math.ceil((endsAt - now) / 1000)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return minutes > 0 ? `${minutes}:${String(seconds).padStart(2, '0')}` : `${seconds}s`; } function GameChoice({ game, disabled, onVote }) { return ( ); } function StatGrid({ stats }) { if (!Array.isArray(stats) || !stats.length) return null; return (
{stats.slice(0, 6).map((stat) => (

{stat.label}

{stat.value}

))}
); } function ParticipantsBlock({ participants }) { const knownParticipants = Array.isArray(participants) ? participants : []; // Participants are server-owned because rover scans, identity lookup, and // score eligibility all happen outside this panel. The UI only formats the // current round list so users can tell whether their rover was counted. const displayNames = knownParticipants .map((participant) => participant?.nickname || participant?.roverId) .filter(Boolean); return (

Participants

{knownParticipants.length}

{displayNames.length ? displayNames.join(', ') : 'Scan rover to join'}

); } function CounterList({ title, entries }) { if (!Array.isArray(entries) || !entries.length) return null; return (

{title}

{entries.slice(0, 4).map((entry) => (
{entry.label || entry.entityId || entry.code} {entry.count || 0}
))}
); } function Leaderboard({ players, ownPlayer }) { const hasOwnPoints = ownPlayer && Number.isFinite(ownPlayer.totalPoints); return (

Your points

{hasOwnPoints ? ownPlayer.totalPoints : 0}

{ownPlayer?.rank ?

Rank {ownPlayer.rank}

: null}

Leaderboard

{Array.isArray(players) && players.length ? (
{players.slice(0, 4).map((player, idx) => (
{idx + 1} {player.nickname} {player.totalPoints}
))}
) : (

No points yet

)}
); } export default function BarcodeGamesPanel() { const { state, voteForGame } = useBarcodeGameState(); const [pendingGameId, setPendingGameId] = useState(null); const activeGame = state.activeGame; const display = activeGame?.display || {}; // Voting should stop once the shared game system has moved past selection. // The joining phase is included because it is still part of starting the // selected game, even though game rules are not running until the countdown. const votingDisabled = state.phase === 'joining' || state.phase === 'starting' || state.phase === 'running'; const timerEndsAt = display.timer?.endsAt; const now = useClock(Number.isFinite(timerEndsAt)); const timerText = formatTimer(timerEndsAt, now); const games = useMemo(() => (Array.isArray(state.games) ? state.games : []), [state.games]); const participants = Array.isArray(state.participants) ? state.participants : []; const handleVote = async (gameId) => { setPendingGameId(gameId); try { await voteForGame(gameId); } finally { setPendingGameId(null); } }; return (
{games.map((game) => ( ))}

{display.title || activeGame?.title || 'No game active'}

{display.primary || 'Vote for a game to start'}

{display.secondary ? (

{display.secondary}

) : null}

{display.timer?.label || 'Time'}

{timerText || '--'}

{timerText ? 'active timer' : 'no timer'}

); }