// User List Panel // Purpose: Defines the User List Panel module and the local helpers/components used in this file. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. import { useEffect, useMemo, useState, useCallback } from 'react'; import { useSessionSelector } from '../../context/SessionContext.jsx'; import NicknameForm from '../NicknameForm/index.jsx'; import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx'; import { roverBadgeStyle, roverNameChromeStyle } from '../../lib/roverColor.js'; import CardFrame from '../CardFrame/index.jsx'; export function NicknameEntryPanel({ compact = false }) { return (
); } export function LinkButtonsPanel() { return ( ); } function roleColors(role) { switch (role) { case 'admin': case 'lockdown': case 'lockdown-admin': return 'text-amber-300'; case 'spectator': return 'text-slate-400'; default: return 'text-sky-300'; } } function formatLabel(user, selfId) { if (!user) return ''; const base = user.nickname || user.socketId?.slice(0, 6) || 'unknown'; if (user.socketId && user.socketId === selfId) { return `${base} (you)`; } return base; } export default function UserListPanel({ hideNicknameForm = false, hideHeader = false, className = '', fillHeight = false, compact = false, showBothTurnsAndUsers = false, }) { const role = useSessionSelector((state) => state.session?.role || null); const users = useSessionSelector((state) => state.session?.users ?? []); const selfId = useSessionSelector((state) => state.session?.socketId || null); const mode = useSessionSelector((state) => state.session?.mode || null); const turnQueues = useSessionSelector((state) => state.session?.turnQueues || {}); const roster = useSessionSelector((state) => state.session?.roster || []); const canSetNickname = role !== 'spectator'; const isTurnsMode = mode === 'turns'; const [turnView, setTurnView] = useState('queues'); useEffect(() => { if (!isTurnsMode) return; setTurnView('queues'); }, [isTurnsMode]); const sorted = useMemo( () => [...users].sort((a, b) => { if (a.socketId === selfId) return -1; if (b.socketId === selfId) return 1; return (a.nickname || '').localeCompare(b.nickname || ''); }), [selfId, users], ); const rosterEntry = useCallback( (roverId) => roster.find((r) => String(r.id) === String(roverId)) || null, [roster], ); const lookupUser = useCallback( (socketId) => users.find((u) => u.socketId === socketId) || { socketId, nickname: null, role: null }, [users], ); const secondsRemaining = useCallback((deadline) => { if (!deadline) return null; const ms = deadline - Date.now(); if (ms <= 0) return 0; return Math.ceil(ms / 1000); }, []); const baseListClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : compact ? 'h-28 overflow-y-auto' : 'h-48 overflow-y-auto'; const turnsListClass = isTurnsMode && fillHeight ? 'max-h-40 overflow-y-auto' : isTurnsMode && compact ? 'max-h-32 overflow-y-auto' : baseListClass; const usersListClass = isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass; const showToggle = isTurnsMode && !showBothTurnsAndUsers; const showQueuesSection = isTurnsMode && (showBothTurnsAndUsers || turnView === 'queues'); const showUsersSection = !isTurnsMode || (showToggle && turnView === 'users'); const showUsersSecondary = isTurnsMode && showBothTurnsAndUsers; const renderUserList = () => sorted.length === 0 ? (

Waiting for users…

) : ( sorted.map((user) => { const isAdmin = user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin'; return (

{formatLabel(user, selfId)}

{user.roverId ? ( rover {rosterEntry(user.roverId)?.name || user.roverId} ) : ( no rover )} {isAdmin && ( Admin )}
); }) ); return ( {!hideNicknameForm && (
{!canSetNickname &&

Spectators cannot set nicknames.

}
)}
{!hideHeader && showToggle ? (
) : null} {showQueuesSection ? (
{Object.keys(turnQueues || {}).length === 0 ? (

No turn queues yet.

) : ( Object.entries(turnQueues).map(([roverId, info]) => { const queue = info?.queue || []; const deadline = info?.idleDeadline || info?.deadline || null; const remaining = secondsRemaining(deadline); const currentId = info?.current || null; const currentIdx = currentId ? queue.findIndex((id) => id === currentId) : -1; const nextId = queue.length > 1 ? currentIdx >= 0 ? queue[(currentIdx + 1) % queue.length] : queue[0] : null; return (

{rosterEntry(roverId)?.name || roverId}

{remaining != null && ( {remaining}s left )}
{queue.length === 0 ? (

No drivers queued.

) : (
{queue.map((socketId, idx) => { const user = lookupUser(socketId); const isCurrent = socketId === currentId; const isNext = Boolean(nextId && socketId === nextId && !isCurrent); const isSelf = Boolean(selfId && socketId === selfId); const isAdmin = user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin'; const highlightClass = isCurrent ? 'bg-sky-600 text-white ring-2 ring-amber-300 animate-pulse' : isNext ? 'bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70' : 'bg-slate-800 text-slate-200'; return ( {formatLabel(user, selfId)} {isAdmin && } {/* {isSelf && YOU} */} {isCurrent && now} {isNext && next} ); })}
)}
); }) )}
) : null} {showUsersSection ? (
{renderUserList()}
) : null} {showUsersSecondary ? (
Users {sorted.length}
{renderUserList()}
) : null}
); }