import { useLoaderData, Link } from "react-router"; import { eq, and, asc, desc, notInArray, inArray } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { useDraftSocket } from "~/hooks/useDraftSocket"; import { useEffect, useState } from "react"; import { Card } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { getAuth } from "@clerk/react-router/server"; import { getTeamQueue } from "~/models/draft-queue"; 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 available participants (not yet picked) - sorted by EV desc, then name const pickedParticipantIds = draftPicks.map((p) => p.participant.id); let availableParticipants: any[] = []; if (sportsSeasonIds.length > 0) { availableParticipants = await db .select({ id: schema.participants.id, name: schema.participants.name, expectedValue: schema.participants.expectedValue, 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( and( sportsSeasonIds.length === 1 ? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]) : inArray(schema.participants.sportsSeasonId, sportsSeasonIds), pickedParticipantIds.length > 0 ? notInArray(schema.participants.id, pickedParticipantIds) : undefined ) ) .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) : []; return { season, draftSlots, draftPicks, availableParticipants, userTeam, userQueue, isCommissioner: !!isCommissioner, currentUserId: userId, }; } export default function DraftRoom() { const { season, draftSlots, draftPicks, availableParticipants, userTeam: _userTeam, isCommissioner: _isCommissioner } = useLoaderData(); const { isConnected, on, off } = useDraftSocket(season.id); const [picks, setPicks] = useState(draftPicks); const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1); const [searchQuery, setSearchQuery] = useState(""); const [hideDrafted, setHideDrafted] = useState(true); const [sportFilter, setSportFilter] = useState("all"); // 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); }; on("pick-made", handlePickMade); return () => { off("pick-made", handlePickMade); }; }, [on, off]); // Calculate current round const currentRound = Math.ceil(currentPick / draftSlots.length); // Generate snake draft grid structure const generateDraftGrid = () => { const teamCount = draftSlots.length; const rounds = season.draftRounds; const grid: Array> = []; 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(); // Get all participants (including drafted) for the show/hide drafted toggle const allParticipants = [...availableParticipants]; // Add drafted participants if we're showing them if (!hideDrafted) { picks.forEach((pick: any) => { if (!allParticipants.find((p: any) => p.id === pick.participant.id)) { allParticipants.push({ id: pick.participant.id, name: pick.participant.name, expectedValue: pick.participant.expectedValue || 0, sport: pick.sport, }); } }); // Re-sort after adding drafted players allParticipants.sort((a, b) => { if (b.expectedValue !== a.expectedValue) { return b.expectedValue - a.expectedValue; } return a.name.localeCompare(b.name); }); } // Filter participants based on search, sport, and drafted status const filteredParticipants = allParticipants.filter((participant: any) => { // 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 (
{/* Standalone Header - No Main Nav */}

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

Round: {currentRound} Pick: {currentPick} {season.status.replace("_", " ")}
{isConnected ? "Live" : "Disconnected"}
{/* Main Content */}
{/* Draft Grid - Horizontally Scrollable */}

Draft Grid

{/* Team Headers */}
{draftSlots.map((slot) => (
{slot.team.name}
Pick {slot.draftOrder}
))}
{/* Draft Grid Rows */}
{draftGrid.map((roundPicks, roundIndex) => { const round = roundIndex + 1; const isEvenRound = round % 2 === 0; const displayPicks = isEvenRound ? [...roundPicks].reverse() : roundPicks; return (
{displayPicks.map((cell) => { const isCurrent = cell.pickNumber === currentPick; const isPicked = !!cell.pick; return (
{cell.round}.{String(cell.pickInRound).padStart(2, "0")}
{isPicked ? (
{cell.pick.participant.name}
{cell.pick.sport.name}
) : isCurrent ? (
On Clock
) : null}
); })}
); })}
{/* Recent Picks */}

Recent Picks

{picks.length === 0 ? (

No picks yet...

) : (
{picks .slice() .reverse() .slice(0, 10) .map((pick: any) => (
#{pick.pickNumber}

{pick.participant.name}

{pick.sport.name}

{pick.team.name}

Round {pick.round}

))}
)}
{/* Right Column: Available Participants */}

Available Participants ({filteredParticipants.length})

{/* Search Input */} setSearchQuery(e.target.value)} className="w-full px-3 py-2 border rounded-md text-sm" />
{/* Sport Filter Dropdown */} {/* Show/Hide Drafted Toggle */}
{/* Table */}
{filteredParticipants.length === 0 ? ( ) : ( filteredParticipants.map((participant: any) => { const isDrafted = draftedParticipantIds.has(participant.id); return ( { if (!isDrafted) { // TODO: Add to queue functionality (Phase 7) console.log("Add to queue:", participant.name); } }} > ); }) )}
Participant Sport EV
No participants found
{participant.name} {isDrafted && ( Drafted )}
{participant.sport.name} {participant.expectedValue}
); }