import { useLoaderData, Link } from "react-router"; import { eq, asc, and, inArray } from "drizzle-orm"; import { getAuth } from "@clerk/react-router/server"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { DraftGrid } from "~/components/DraftGrid"; import { useDraftSocket } from "~/hooks/useDraftSocket"; import { useState, useEffect } from "react"; import { buildOwnerMap } from "~/lib/owner-map"; import { Button } from "~/components/ui/button"; import { ArrowLeft } from "lucide-react"; import { calculateFantasyPoints, calculateBracketPoints } from "~/models/scoring-rules"; import type { CoronaState } from "~/components/draft/DraftPickCell"; import type { Route } from "./+types/$leagueId.draft-board.$seasonId"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Draft Board — ${data?.season?.league?.name ?? "League"} - Brackt` }]; } export async function loader(args: Route.LoaderArgs) { const { params } = args; const { leagueId, seasonId } = params; if (!seasonId) { throw new Response("Season ID is required", { status: 400 }); } const db = database(); // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), with: { league: true, }, }); if (!season) { throw new Response("Season not found", { status: 404 }); } // Validate that the season actually belongs to the league in the URL if (season.leagueId !== leagueId) { throw new Response("Season not found", { status: 404 }); } // Check access: public boards are accessible to everyone without auth if (!season.league.isPublicDraftBoard) { // Not public - check if the user is a league member or commissioner const { userId } = await getAuth(args); if (!userId) { throw new Response("This draft board is not public", { status: 403 }); } // Check if user is a commissioner const isCommissioner = await db.query.commissioners.findFirst({ where: and( eq(schema.commissioners.leagueId, season.leagueId), eq(schema.commissioners.userId, userId) ), }); // Check if user has a team in this season const hasTeam = await db.query.teams.findFirst({ where: and( eq(schema.teams.seasonId, seasonId), eq(schema.teams.ownerId, userId) ), }); if (!isCommissioner && !hasTeam) { throw new Response("You don't have access to this draft board", { status: 403 }); } } // Get draft slots (draft order) 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 with participant and sport info const draftPicks = await db .select({ id: schema.draftPicks.id, pickNumber: schema.draftPicks.pickNumber, round: schema.draftPicks.round, pickInRound: schema.draftPicks.pickInRound, team: schema.teams, participant: schema.participants, sport: schema.sports, scoringPattern: schema.sportsSeasons.scoringPattern, }) .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)); const ownerMap = await buildOwnerMap(draftSlots); const coronaStates: Record = {}; if (draftPicks.length > 0) { const participantIds = draftPicks.map((p) => p.participant.id); const sportsSeasonIds = [ ...new Set(draftPicks.map((p) => p.participant.sportsSeasonId)), ]; const results = await db .select({ participantId: schema.participantResults.participantId, sportsSeasonId: schema.participantResults.sportsSeasonId, finalPosition: schema.participantResults.finalPosition, isPartialScore: schema.participantResults.isPartialScore, }) .from(schema.participantResults) .where(inArray(schema.participantResults.participantId, participantIds)); const resultByParticipant = new Map( results.map((r) => [r.participantId, r]) ); const maxPoints = season.pointsFor1st; const bracketTemplateBySportsSeason = new Map(); if (sportsSeasonIds.length > 0) { const events = await db .select({ sportsSeasonId: schema.scoringEvents.sportsSeasonId, bracketTemplateId: schema.scoringEvents.bracketTemplateId, }) .from(schema.scoringEvents) .where(inArray(schema.scoringEvents.sportsSeasonId, sportsSeasonIds)); for (const ev of events) { if (!bracketTemplateBySportsSeason.has(ev.sportsSeasonId)) { bracketTemplateBySportsSeason.set( ev.sportsSeasonId, ev.bracketTemplateId ); } } } const scoringRules = { pointsFor1st: season.pointsFor1st, pointsFor2nd: season.pointsFor2nd, pointsFor3rd: season.pointsFor3rd, pointsFor4th: season.pointsFor4th, pointsFor5th: season.pointsFor5th, pointsFor6th: season.pointsFor6th, pointsFor7th: season.pointsFor7th, pointsFor8th: season.pointsFor8th, }; for (const pick of draftPicks) { const result = resultByParticipant.get(pick.participant.id); if (!result || result.finalPosition === null) { coronaStates[pick.participant.id] = { type: "pending" }; continue; } if (result.finalPosition === 0 && !result.isPartialScore) { coronaStates[pick.participant.id] = { type: "eliminated", points: 0 }; continue; } if (result.finalPosition > 0) { const isBracket = pick.scoringPattern === "playoff_bracket"; const templateId = isBracket ? bracketTemplateBySportsSeason.get( pick.participant.sportsSeasonId ) ?? null : null; const points = isBracket ? calculateBracketPoints( result.finalPosition, scoringRules, templateId ) : calculateFantasyPoints(result.finalPosition, scoringRules); const brightness = maxPoints > 0 ? Math.min(points / maxPoints, 1) : 0; coronaStates[pick.participant.id] = { type: "scored", brightness, points, }; continue; } coronaStates[pick.participant.id] = { type: "pending" }; } } return { season, draftSlots, draftPicks, ownerMap, coronaStates, }; } export default function DraftBoard() { const { season, draftSlots, draftPicks: initialPicks, ownerMap, coronaStates } = useLoaderData(); const { isConnected, on, off } = useDraftSocket(season.id); const [picks, setPicks] = useState(initialPicks); const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1); // Listen for new picks (only if draft is still active) useEffect(() => { if (season.status !== "draft") return; type PickShape = (typeof initialPicks)[number]; const handlePickMade = (data: unknown) => { const pickData = data as { pick: PickShape; nextPickNumber: number }; setPicks((prev) => [...prev, pickData.pick]); setCurrentPick(pickData.nextPickNumber); }; on("pick-made", handlePickMade); return () => { off("pick-made", handlePickMade); }; }, [on, off, season.status]); // Generate draft grid const totalTeams = draftSlots.length; const totalRounds = season.draftRounds || 1; type PickItem = (typeof initialPicks)[number]; const draftGrid: Array> = []; for (let round = 0; round < totalRounds; round++) { const roundPicks: Array = []; for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) { const pickNumber = round * totalTeams + teamIndex + 1; const pick = picks.find((p) => p.pickNumber === pickNumber); roundPicks.push(pick || null); } draftGrid.push(roundPicks); } const isDraftActive = season.status === "draft"; const currentRound = totalTeams > 0 ? Math.ceil(currentPick / totalTeams) : 1; return (
Brackt

{season.league.name} Draft Board

{isDraftActive && (
Round {currentRound} Pick {currentPick}
{isConnected ? "Connected" : "Disconnected"}
)}
{/* Draft Grid */}
); }