'use client' import { Suspense, use, useState, useEffect } 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 { toast } from 'sonner' import { ArrowLeft, Play, Pause, Square, Clock, Users, Zap, GripVertical, AlertCircle, ExternalLink, RefreshCw, } 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' 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) // Fetch session data const { data: sessionData, isLoading, refetch } = trpc.liveVoting.getSession.useQuery( { roundId }, { refetchInterval: 2000 } // Poll every 2 seconds ) // 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 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.roundProjects.map((rp) => rp.project.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.roundProjects.map((rp) => rp.project) 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 {sessionData.currentVotes.length === 0 ? (

No votes yet

) : (
Total votes {sessionData.currentVotes.length}
Average score {( sessionData.currentVotes.reduce( (sum, v) => sum + v.score, 0 ) / sessionData.currentVotes.length ).toFixed(1)}
)}
{/* Links */} Voting Links Share these links with participants
) } function LiveVotingSkeleton() { return (
) } export default function LiveVotingPage({ params }: PageProps) { const { id } = use(params) return ( }> ) }