'use client' import { Suspense, use, useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Progress } from '@/components/ui/progress' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { toast } from 'sonner' import { ArrowLeft, Play, Pause, Square, Clock, Users, Zap, GripVertical, AlertCircle, ExternalLink, RefreshCw, QrCode, Settings2, Scale, UserCheck, } from 'lucide-react' import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { useLiveVotingSSE, type VoteUpdate } from '@/hooks/use-live-voting-sse' import { QRCodeDisplay } from '@/components/shared/qr-code-display' interface PageProps { params: Promise<{ id: string }> } interface Project { id: string title: string teamName: string | null } function SortableProject({ project, isActive, isVoting, }: { project: Project isActive: boolean isVoting: boolean }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: project.id }) const style = { transform: CSS.Transform.toString(transform), transition, } return (

{project.title}

{project.teamName && (

{project.teamName}

)}
{isActive && ( {isVoting ? 'Voting' : 'Current'} )}
) } function LiveVotingContent({ roundId }: { roundId: string }) { const utils = trpc.useUtils() const [projectOrder, setProjectOrder] = useState([]) const [countdown, setCountdown] = useState(null) const [votingDuration, setVotingDuration] = useState(30) const [liveVoteCount, setLiveVoteCount] = useState(null) const [liveAvgScore, setLiveAvgScore] = useState(null) // Fetch session data - reduced polling since SSE handles real-time const { data: sessionData, isLoading, refetch } = trpc.liveVoting.getSession.useQuery( { roundId }, { refetchInterval: 5000 } ) // SSE for real-time vote updates const onVoteUpdate = useCallback((data: VoteUpdate) => { setLiveVoteCount(data.totalVotes) setLiveAvgScore(data.averageScore) }, []) const onSessionStatus = useCallback(() => { refetch() }, [refetch]) const onProjectChange = useCallback(() => { setLiveVoteCount(null) setLiveAvgScore(null) refetch() }, [refetch]) const { isConnected } = useLiveVotingSSE( sessionData?.id || null, { onVoteUpdate, onSessionStatus, onProjectChange, } ) // Mutations const setOrder = trpc.liveVoting.setProjectOrder.useMutation({ onSuccess: () => { toast.success('Project order updated') }, onError: (error) => { toast.error(error.message) }, }) const startVoting = trpc.liveVoting.startVoting.useMutation({ onSuccess: () => { toast.success('Voting started') refetch() }, onError: (error) => { toast.error(error.message) }, }) const stopVoting = trpc.liveVoting.stopVoting.useMutation({ onSuccess: () => { toast.success('Voting stopped') refetch() }, onError: (error) => { toast.error(error.message) }, }) const endSession = trpc.liveVoting.endSession.useMutation({ onSuccess: () => { toast.success('Session ended') refetch() }, onError: (error) => { toast.error(error.message) }, }) const updateSessionConfig = trpc.liveVoting.updateSessionConfig.useMutation({ onSuccess: () => { toast.success('Session config updated') refetch() }, onError: (error) => { toast.error(error.message) }, }) const updatePresentationSettings = trpc.liveVoting.updatePresentationSettings.useMutation({ onSuccess: () => { toast.success('Presentation settings updated') refetch() }, onError: (error) => { toast.error(error.message) }, }) const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ) // Initialize project order useEffect(() => { if (sessionData) { const storedOrder = (sessionData.projectOrderJson as string[]) || [] if (storedOrder.length > 0) { setProjectOrder(storedOrder) } else { setProjectOrder(sessionData.round.projects.map((p) => p.id)) } } }, [sessionData]) // Countdown timer useEffect(() => { if (!sessionData?.votingEndsAt || sessionData.status !== 'IN_PROGRESS') { setCountdown(null) return } const updateCountdown = () => { const remaining = new Date(sessionData.votingEndsAt!).getTime() - Date.now() setCountdown(Math.max(0, Math.floor(remaining / 1000))) } updateCountdown() const interval = setInterval(updateCountdown, 1000) return () => clearInterval(interval) }, [sessionData?.votingEndsAt, sessionData?.status]) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (over && active.id !== over.id) { const oldIndex = projectOrder.indexOf(active.id as string) const newIndex = projectOrder.indexOf(over.id as string) const newOrder = arrayMove(projectOrder, oldIndex, newIndex) setProjectOrder(newOrder) if (sessionData) { setOrder.mutate({ sessionId: sessionData.id, projectIds: newOrder, }) } } } const handleStartVoting = (projectId: string) => { if (!sessionData) return startVoting.mutate({ sessionId: sessionData.id, projectId, durationSeconds: votingDuration, }) } const handleStopVoting = () => { if (!sessionData) return stopVoting.mutate({ sessionId: sessionData.id }) } const handleEndSession = () => { if (!sessionData) return endSession.mutate({ sessionId: sessionData.id }) } if (isLoading) { return } if (!sessionData) { return ( Error Failed to load session ) } const projects = sessionData.round.projects const sortedProjects = projectOrder .map((id) => projects.find((p) => p.id === id)) .filter((p): p is Project => !!p) // Add any projects not in the order const missingProjects = projects.filter((p) => !projectOrder.includes(p.id)) const allProjects = [...sortedProjects, ...missingProjects] const isVoting = sessionData.status === 'IN_PROGRESS' const isCompleted = sessionData.status === 'COMPLETED' return (
{/* Header */}

Live Voting

{sessionData.status.replace('_', ' ')}

{sessionData.round.program.name} - {sessionData.round.name}

{/* Main control panel */}
{/* Voting status */} {isVoting && (

Currently Voting

{projects.find((p) => p.id === sessionData.currentProjectId)?.title}

{countdown !== null ? countdown : '--'}s

remaining

{countdown !== null && ( )}
)} {/* Project order */} Presentation Order Drag to reorder projects. Click "Start Voting" to begin voting for a project. {allProjects.length === 0 ? (

No finalist projects found for this round

) : ( p.id)} strategy={verticalListSortingStrategy} >
{allProjects.map((project) => (
))}
)}
{/* Sidebar */}
{/* Controls */} Controls
setVotingDuration(parseInt(e.target.value) || 30) } className="w-20 px-2 py-1 border rounded text-center" disabled={isVoting} /> seconds
{/* Live stats */} Current Votes {isConnected && ( )} {(() => { const voteCount = liveVoteCount ?? sessionData.currentVotes.length const avgScore = liveAvgScore ?? ( sessionData.currentVotes.length > 0 ? sessionData.currentVotes.reduce((sum, v) => sum + v.score, 0) / sessionData.currentVotes.length : null ) if (voteCount === 0) { return (

No votes yet

) } return (
Total votes {voteCount}
Average score {avgScore !== null ? avgScore.toFixed(1) : '--'}
) })()}
{/* Session Configuration */} Session Config
{ updateSessionConfig.mutate({ sessionId: sessionData.id, allowAudienceVotes: checked, }) }} disabled={isCompleted} />
{sessionData.allowAudienceVotes && (
{ updateSessionConfig.mutate({ sessionId: sessionData.id, audienceVoteWeight: parseInt(e.target.value) / 100, }) }} className="flex-1" disabled={isCompleted} /> {Math.round((sessionData.audienceVoteWeight || 0) * 100)}%
)}
{/* QR Codes & Links */} Voting Links Share these links with participants
) } function LiveVotingSkeleton() { return (
) } export default function LiveVotingPage({ params }: PageProps) { const { id } = use(params) return ( }> ) }