import { useLoaderData, Link } from "react-router"; import { eq, and, asc, desc, inArray } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { useDraftSocket } from "~/hooks/useDraftSocket"; import { useEffect, useState, useMemo } from "react"; import { Card } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { getAuth } from "@clerk/react-router/server"; import { getTeamQueue } from "~/models/draft-queue"; import { DraftSidebar } from "~/components/DraftSidebar"; import { TeamsDraftedGrid } from "~/components/draft/TeamsDraftedGrid"; import { QueueSection } from "~/components/draft/QueueSection"; import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection"; import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks"; import { DraftGridSection } from "~/components/draft/DraftGridSection"; import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { toast } from "sonner"; export async function loader(args: any) { const { params } = args; const { seasonId, leagueId } = params; const auth = await getAuth(args); const userId = (auth as any).userId as string | null; if (!seasonId) { throw new Response("Season ID is required", { status: 400 }); } if (!userId) { throw new Response("You must be logged in to view the draft room", { status: 401, }); } const db = database(); // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), with: { league: true, teams: true, }, }); if (!season) { throw new Response("Season not found", { status: 404 }); } // Verify user access: must be team owner or commissioner const userTeam = season.teams.find((team: any) => team.ownerId === userId); const isCommissioner = await db.query.commissioners.findFirst({ where: and( eq(schema.commissioners.leagueId, leagueId), eq(schema.commissioners.userId, userId) ), }); if (!userTeam && !isCommissioner) { throw new Response("You do not have access to this draft room", { status: 403, }); } // Get draft slots (draft order) - using select instead of query builder const draftSlots = await db .select({ id: schema.draftSlots.id, draftOrder: schema.draftSlots.draftOrder, team: schema.teams, }) .from(schema.draftSlots) .innerJoin(schema.teams, eq(schema.draftSlots.teamId, schema.teams.id)) .where(eq(schema.draftSlots.seasonId, seasonId)) .orderBy(asc(schema.draftSlots.draftOrder)); // Get all draft picks - using select instead of query builder const draftPicks = await db .select({ id: schema.draftPicks.id, pickNumber: schema.draftPicks.pickNumber, round: schema.draftPicks.round, pickInRound: schema.draftPicks.pickInRound, timeUsed: schema.draftPicks.timeUsed, team: schema.teams, participant: schema.participants, sport: schema.sports, }) .from(schema.draftPicks) .innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id)) .innerJoin( schema.participants, eq(schema.draftPicks.participantId, schema.participants.id) ) .innerJoin( schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id) ) .innerJoin( schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id) ) .where(eq(schema.draftPicks.seasonId, seasonId)) .orderBy(asc(schema.draftPicks.pickNumber)); // Get sports seasons for this season const seasonSports = await db.query.seasonSports.findMany({ where: eq(schema.seasonSports.seasonId, seasonId), }); const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId); // Get ALL participants (both drafted and undrafted) - sorted by EV desc, then name // Filtering will be done on the client side based on the "Show Drafted" toggle let availableParticipants: any[] = []; if (sportsSeasonIds.length > 0) { availableParticipants = await db .select({ id: schema.participants.id, name: schema.participants.name, sport: schema.sports, }) .from(schema.participants) .innerJoin( schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id) ) .innerJoin( schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id) ) .where( sportsSeasonIds.length === 1 ? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]) : inArray(schema.participants.sportsSeasonId, sportsSeasonIds) ) .orderBy( desc(schema.participants.expectedValue), asc(schema.participants.name) ); } // Load user's team queue if they have a team const userQueue = userTeam ? await getTeamQueue(userTeam.id) : []; // Load draft timers for all teams const timers = await db.query.draftTimers.findMany({ where: eq(schema.draftTimers.seasonId, seasonId), }); // Load autodraft settings for all teams const autodraftSettings = await db.query.autodraftSettings.findMany({ where: eq(schema.autodraftSettings.seasonId, seasonId), }); // Load user's autodraft settings if they have a team const userAutodraftSettings = userTeam ? await db.query.autodraftSettings.findFirst({ where: and( eq(schema.autodraftSettings.seasonId, seasonId), eq(schema.autodraftSettings.teamId, userTeam.id) ), }) : null; return { season, draftSlots, draftPicks, availableParticipants, userTeam, userQueue, timers, autodraftSettings, userAutodraftSettings, isCommissioner: !!isCommissioner, currentUserId: userId, }; } export default function DraftRoom() { const { season, draftSlots, draftPicks, availableParticipants, userTeam, userQueue, timers, autodraftSettings, userAutodraftSettings, isCommissioner, } = useLoaderData(); const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id); const [picks, setPicks] = useState(draftPicks); const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1); const [searchQuery, setSearchQuery] = useState(""); const [hideDrafted, setHideDrafted] = useState(true); const [hideIneligible, setHideIneligible] = useState(true); const [sportFilter, setSportFilter] = useState("all"); const [queue, setQueue] = useState(userQueue); const [isPaused, setIsPaused] = useState(season.draftPaused || false); const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active"); // Sidebar and tabs state const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { if (typeof window === "undefined") return false; const stored = localStorage.getItem("draftSidebarCollapsed"); return stored ? JSON.parse(stored) : false; }); const [activeTab, setActiveTab] = useState<"participants" | "board" | "teams">("participants"); // Track autodraft status for all teams const [autodraftStatus, setAutodraftStatus] = useState>(() => { const status: Record = {}; autodraftSettings.forEach((setting: any) => { status[setting.teamId] = setting.isEnabled; }); return status; }); // Track connected teams const [connectedTeams, setConnectedTeams] = useState>(new Set()); // Track user's autodraft settings const [userAutodraft, setUserAutodraft] = useState({ isEnabled: userAutodraftSettings?.isEnabled || false, mode: (userAutodraftSettings?.mode || "next_pick") as "next_pick" | "while_on", }); const [teamTimers, setTeamTimers] = useState>(() => { // Initialize with timers from loader data, or use initial time if draft hasn't started const timersMap: Record = {}; const initialTime = season.draftInitialTime || 120; if (timers.length > 0) { // Draft has started, use actual timers timers.forEach((timer: any) => { timersMap[timer.teamId] = timer.timeRemaining; }); } else { // Draft hasn't started, show initial time for all teams draftSlots.forEach((slot) => { timersMap[slot.team.id] = initialTime; }); } return timersMap; }); const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false); const [selectedPickSlot, setSelectedPickSlot] = useState<{ pickNumber: number; teamId: string; } | null>(null); // Calculate draft eligibility for current user's team const eligibility = useMemo(() => { if (!userTeam) return null; // Transform picks data to match eligibility function signature const transformedPicks = picks.map((p: any) => ({ teamId: p.team.id, participant: { id: p.participant.id, sport: { id: p.sport.id, name: p.sport.name, }, }, })); const userTeamPicks = transformedPicks.filter( (p: any) => p.teamId === userTeam.id ); // Transform participants to match signature const transformedParticipants = availableParticipants.map((p: any) => ({ id: p.id, sport: { id: p.sport.id, name: p.sport.name, }, })); // Get unique sports from availableParticipants const sportsMap = new Map(); availableParticipants.forEach((p: any) => { if (!sportsMap.has(p.sport.id)) { sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name }); } }); const seasonSportsData = Array.from(sportsMap.values()); return calculateDraftEligibility( userTeam.id, userTeamPicks, transformedPicks, transformedParticipants, seasonSportsData, season.draftRounds, season.teams ); }, [userTeam, picks, availableParticipants, season]); // Calculate eligibility for force manual pick dialog (selected team) const forcePickEligibility = useMemo(() => { if (!selectedPickSlot) return null; const transformedPicks = picks.map((p: any) => ({ teamId: p.team.id, participant: { id: p.participant.id, sport: { id: p.sport.id, name: p.sport.name, }, }, })); const selectedTeamPicks = transformedPicks.filter( (p: any) => p.teamId === selectedPickSlot.teamId ); const transformedParticipants = availableParticipants.map((p: any) => ({ id: p.id, sport: { id: p.sport.id, name: p.sport.name, }, })); const sportsMap = new Map(); availableParticipants.forEach((p: any) => { if (!sportsMap.has(p.sport.id)) { sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name }); } }); const seasonSportsData = Array.from(sportsMap.values()); return calculateDraftEligibility( selectedPickSlot.teamId, selectedTeamPicks, transformedPicks, transformedParticipants, seasonSportsData, season.draftRounds, season.teams ); }, [selectedPickSlot, picks, availableParticipants, season]); // Listen for new picks from other users useEffect(() => { const handlePickMade = (data: any) => { console.log("Pick made:", data); setPicks((prev: any) => [...prev, data.pick]); setCurrentPick(data.nextPickNumber); }; const handleTimerUpdate = (data: any) => { // Update the specific team's timer setTeamTimers((prev) => ({ ...prev, [data.teamId]: data.timeRemaining, })); }; const handleDraftPaused = () => { setIsPaused(true); }; const handleDraftResumed = () => { setIsPaused(false); }; const handleDraftCompleted = () => { setIsDraftComplete(true); }; const handleAutodraftUpdated = (data: { teamId: string; isEnabled: boolean; mode: "next_pick" | "while_on" }) => { console.log("Autodraft updated:", data); setAutodraftStatus((prev) => ({ ...prev, [data.teamId]: data.isEnabled, })); // Update user's local state if it's their team if (userTeam && data.teamId === userTeam.id) { setUserAutodraft({ isEnabled: data.isEnabled, mode: data.mode, }); } }; const handleTeamConnected = (data: { teamId: string }) => { console.log("Team connected:", data.teamId); setConnectedTeams((prev) => new Set(prev).add(data.teamId)); }; const handleTeamDisconnected = (data: { teamId: string }) => { console.log("Team disconnected:", data.teamId); setConnectedTeams((prev) => { const newSet = new Set(prev); newSet.delete(data.teamId); return newSet; }); }; const handleConnectedTeamsList = (data: { teamIds: string[] }) => { console.log("Received connected teams list:", data.teamIds); setConnectedTeams(new Set(data.teamIds)); }; const handleParticipantRemovedFromQueues = (data: { participantId: string }) => { console.log("Participant removed from queues:", data.participantId); setQueue((prev: any) => prev.filter((item: any) => item.participantId !== data.participantId) ); }; on("pick-made", handlePickMade); on("timer-update", handleTimerUpdate); on("draft-paused", handleDraftPaused); on("draft-resumed", handleDraftResumed); on("draft-completed", handleDraftCompleted); on("autodraft-updated", handleAutodraftUpdated); on("team-connected", handleTeamConnected); on("team-disconnected", handleTeamDisconnected); on("connected-teams-list", handleConnectedTeamsList); on("participant-removed-from-queues", handleParticipantRemovedFromQueues); return () => { off("pick-made", handlePickMade); off("timer-update", handleTimerUpdate); off("draft-paused", handleDraftPaused); off("draft-resumed", handleDraftResumed); off("draft-completed", handleDraftCompleted); off("autodraft-updated", handleAutodraftUpdated); off("team-connected", handleTeamConnected); off("team-disconnected", handleTeamDisconnected); off("connected-teams-list", handleConnectedTeamsList); off("participant-removed-from-queues", handleParticipantRemovedFromQueues); }; }, [on, off, currentPick, userTeam]); // Persist sidebar collapsed state useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("draftSidebarCollapsed", JSON.stringify(sidebarCollapsed)); } }, [sidebarCollapsed]); // Queue handlers const handleAddToQueue = async (participantId: string) => { if (!userTeam) return; // Check if participant is already in queue (client-side) const alreadyInQueue = queue.some((item: any) => item.participantId === participantId); if (alreadyInQueue) { toast.error("This participant is already in your queue"); return; } // Optimistic update - create a temporary queue item const tempQueueItem = { id: `temp-${Date.now()}`, // Temporary ID participantId, queuePosition: queue.length + 1, }; // Immediately update the UI setQueue((prev: any) => [...prev, tempQueueItem]); const formData = new FormData(); formData.append("seasonId", season.id); formData.append("teamId", userTeam.id); formData.append("participantId", participantId); try { const response = await fetch("/api/queue/add", { method: "POST", body: formData, }); if (response.ok) { const data = await response.json(); // Replace temp item with real item from server setQueue((prev: any) => prev.map((item: any) => item.id === tempQueueItem.id ? data.queueItem : item ) ); } else { // Revert the optimistic update on error setQueue((prev: any) => prev.filter((item: any) => item.id !== tempQueueItem.id) ); const error = await response.json(); toast.error(error.error || "Failed to add to queue"); } } catch (error) { // Revert the optimistic update on network error setQueue((prev: any) => prev.filter((item: any) => item.id !== tempQueueItem.id) ); toast.error("Network error - failed to add to queue"); } }; const handleRemoveFromQueue = async (queueId: string) => { if (!userTeam) return; const formData = new FormData(); formData.append("queueId", queueId); formData.append("teamId", userTeam.id); const response = await fetch("/api/queue/remove", { method: "POST", body: formData, }); if (response.ok) { const data = await response.json(); setQueue(data.queue); } }; const handleReorderQueue = async (participantIds: string[]) => { if (!userTeam) return; const formData = new FormData(); formData.append("teamId", userTeam.id); formData.append("seasonId", season.id); formData.append("participantIds", JSON.stringify(participantIds)); const response = await fetch("/api/queue/reorder", { method: "POST", body: formData, }); if (response.ok) { const data = await response.json(); setQueue(data.queue); } }; const handleStartDraft = async () => { const formData = new FormData(); formData.append("seasonId", season.id); const response = await fetch("/api/draft/start", { method: "POST", body: formData, }); if (response.ok) { window.location.reload(); // Reload to get updated season status } }; const handlePauseDraft = async () => { const formData = new FormData(); formData.append("seasonId", season.id); const response = await fetch("/api/draft/pause", { method: "POST", body: formData, }); if (response.ok) { setIsPaused(true); } }; const handleResumeDraft = async () => { const formData = new FormData(); formData.append("seasonId", season.id); const response = await fetch("/api/draft/resume", { method: "POST", body: formData, }); if (response.ok) { setIsPaused(false); } }; const handleMakePick = async (participantId: string) => { const formData = new FormData(); formData.append("seasonId", season.id); formData.append("participantId", participantId); const response = await fetch("/api/draft/make-pick", { method: "POST", body: formData, }); if (response.ok) { // Pick will be broadcast via socket, no need to manually update } else { const error = await response.json(); alert(error.error || "Failed to make pick"); } }; const handleForceAutopick = async (pickNumber: number, teamId: string) => { const formData = new FormData(); formData.append("seasonId", season.id); formData.append("teamId", teamId); formData.append("pickNumber", pickNumber.toString()); const response = await fetch("/api/draft/force-autopick", { method: "POST", body: formData, }); if (response.ok) { // Pick will be broadcast via socket } else { const error = await response.json(); alert(error.error || "Failed to force autopick"); } }; const handleForceManualPick = async (participantId: string) => { if (!selectedPickSlot) return; const formData = new FormData(); formData.append("seasonId", season.id); formData.append("teamId", selectedPickSlot.teamId); formData.append("participantId", participantId); formData.append("pickNumber", selectedPickSlot.pickNumber.toString()); const response = await fetch("/api/draft/force-manual-pick", { method: "POST", body: formData, }); if (response.ok) { setForcePickDialogOpen(false); setSelectedPickSlot(null); } else { const error = await response.json(); alert(error.error || "Failed to force manual pick"); } }; // Calculate current round const currentRound = Math.ceil(currentPick / draftSlots.length); // Determine whose turn it is const totalTeams = draftSlots.length; const isSnakeDraft = true; const isEvenRound = currentRound % 2 === 0; let pickInRound = ((currentPick - 1) % totalTeams) + 1; if (isSnakeDraft && isEvenRound) { pickInRound = totalTeams - pickInRound + 1; } const currentDraftSlot = draftSlots.find( (slot) => slot.draftOrder === pickInRound ); const isMyTurn = !!( userTeam && currentDraftSlot && currentDraftSlot.team.id === userTeam.id ); const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn // Generate snake draft grid structure const generateDraftGrid = () => { const teamCount = draftSlots.length; const rounds = season.draftRounds; const grid: Array< Array<{ pickNumber: number; round: number; pickInRound: number; teamId: string; pick?: any; }> > = []; for (let round = 1; round <= rounds; round++) { const roundPicks = []; const isOddRound = round % 2 === 1; for (let i = 0; i < teamCount; i++) { const pickInRound = i + 1; const teamIndex = isOddRound ? i : teamCount - 1 - i; const pickNumber = (round - 1) * teamCount + pickInRound; const teamId = draftSlots[teamIndex]?.team.id; // Find if this pick has been made const pick = picks.find((p: any) => p.pickNumber === pickNumber); roundPicks.push({ pickNumber, round, pickInRound, teamId, pick, }); } grid.push(roundPicks); } return grid; }; const draftGrid = generateDraftGrid(); // Get drafted participant IDs for filtering const draftedParticipantIds = new Set( picks.map((p: any) => p.participant.id) ); // Get unique sports for filter dropdown const uniqueSports = Array.from( new Set(availableParticipants.map((p: any) => p.sport.name)) ).sort(); // Filter participants based on search, sport, drafted status, and eligibility const filteredParticipants = availableParticipants.filter( (participant: any) => { // Drafted filter - hide drafted participants by default if (hideDrafted && draftedParticipantIds.has(participant.id)) { return false; } // Eligibility filter - hide ineligible participants by default (if user has a team) if (hideIneligible && eligibility) { const isEligible = eligibility.eligibleSportIds.has(participant.sport.id); if (!isEligible) { return false; } } // Search filter if ( searchQuery && !participant.name.toLowerCase().includes(searchQuery.toLowerCase()) ) { return false; } // Sport filter if (sportFilter !== "all" && participant.sport.name !== sportFilter) { return false; } return true; } ); return (
{/* Draft Completion Banner */} {isDraftComplete && (
🎉 Draft Complete! The season is now active. {season.league.isPublicDraftBoard && ( <> {" "} View Draft Board )}
)} {/* Standalone Header - No Main Nav */}
{/* Mobile menu button - only show if user has team and sidebar is collapsed */} {userTeam && sidebarCollapsed && ( )}

{season.league.name} - {season.year} Draft

Round: {currentRound} Pick: {currentPick} {season.status.replace("_", " ")}
{/* Commissioner Controls */} {isCommissioner && season.status === "pre_draft" && ( )} {isCommissioner && season.status === "draft" && ( <> {isPaused ? ( ) : ( )} )}
{isConnected ? "Live" : "Disconnected"}
{/* Main Content - Flex Row: Sidebar + Tabs */}
{/* Sidebar */} {userTeam && ( { setUserAutodraft({ isEnabled, mode }); }} onReorder={handleReorderQueue} /> } recentPicksSection={ } /> )} {/* Main Tabbed Content */}
setActiveTab(value as "participants" | "board" | "teams") } className="h-full flex flex-col" > Available Participants Draft Board Teams Drafted
{ setSelectedPickSlot({ pickNumber, teamId }); setForcePickDialogOpen(true); }} />
{/* Force Manual Pick Dialog */} {forcePickDialogOpen && selectedPickSlot && (
setForcePickDialogOpen(false)} > e.stopPropagation()} >

Force Manual Pick

Select a participant to draft for Pick # {selectedPickSlot.pickNumber}

{/* Search and Filter */}
setSearchQuery(e.target.value)} />
{/* Participant List */}
{filteredParticipants .filter((p: any) => !draftedParticipantIds.has(p.id)) .map((participant: any) => { // Check eligibility for force pick team const isEligible = forcePickEligibility ? forcePickEligibility.eligibleSportIds.has(participant.sport.id) : true; const ineligibleReason = forcePickEligibility?.ineligibleReasons[participant.sport.id]; return ( ); })}
Participant Sport Action
{participant.name} {!isEligible && ( Ineligible )}
{participant.sport.name}
)} {/* Connection Overlay - blocks interaction until socket connects */}
); }