diff --git a/app/routes.ts b/app/routes.ts index 081252e..dca08d8 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -42,6 +42,7 @@ export default [ route("api/draft/rollback", "routes/api/draft.rollback.ts"), route("api/draft/adjust-time-bank", "routes/api/draft.adjust-time-bank.ts"), route("api/autodraft/update", "routes/api/autodraft.update.ts"), + route("api/seasons/:seasonId/draft", "routes/api/seasons.$seasonId.draft.ts"), route("user-profile", "routes/user-profile.tsx"), route("how-to-play", "routes/how-to-play.tsx"), route("rules", "routes/rules.tsx"), diff --git a/app/routes/api/seasons.$seasonId.draft.ts b/app/routes/api/seasons.$seasonId.draft.ts new file mode 100644 index 0000000..fd80c9c --- /dev/null +++ b/app/routes/api/seasons.$seasonId.draft.ts @@ -0,0 +1,111 @@ +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq, inArray } from "drizzle-orm"; +import { calculatePickInfo } from "~/models/draft-utils"; + +import type { LoaderFunctionArgs } from "react-router"; + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export async function loader({ params }: LoaderFunctionArgs) { + const { seasonId } = params; + + // Fix #1: validate UUID format up front so malformed IDs return 400, not a DB error + if (!seasonId || !UUID_RE.test(seasonId)) { + return Response.json({ error: "Invalid season ID" }, { status: 400 }); + } + + const db = database(); + + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + }); + + if (!season) { + return Response.json({ error: "Season not found" }, { status: 404 }); + } + + const draftSlots = await db.query.draftSlots.findMany({ + where: eq(schema.draftSlots.seasonId, seasonId), + orderBy: schema.draftSlots.draftOrder, + with: { team: true }, + }); + + const totalTeams = draftSlots.length; + const totalPicks = totalTeams * season.draftRounds; + const currentPickNumber = season.currentPickNumber ?? 1; + const isDraftComplete = season.status === "active" || season.status === "completed"; + + // Fix #4: guard against totalTeams === 0 before calling calculatePickInfo + let onTheClockSlot: (typeof draftSlots)[number] | null = null; + if (season.status === "draft" && totalTeams > 0) { + const { pickInRound } = calculatePickInfo(currentPickNumber, totalTeams); + onTheClockSlot = draftSlots.find((slot) => slot.draftOrder === pickInRound) ?? null; + } + + // All picks with participant + sport + team owner info + const picksRaw = await db + .select({ + pickNumber: schema.draftPicks.pickNumber, + round: schema.draftPicks.round, + teamName: schema.teams.name, + teamOwnerId: schema.teams.ownerId, + participantName: schema.participants.name, + sport: schema.sports.name, + }) + .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(schema.draftPicks.pickNumber); + + // Fix #2: include the on-the-clock owner in the batch so we never make a separate user query + const picksOwnerIds = picksRaw.map((p) => p.teamOwnerId).filter(Boolean) as string[]; + const clockOwnerId = onTheClockSlot?.team.ownerId ?? null; + const allOwnerIds = [...new Set([...picksOwnerIds, ...(clockOwnerId ? [clockOwnerId] : [])])]; + + const usernameByClerkId = new Map(); + if (allOwnerIds.length > 0) { + const owners = await db + .select({ + clerkId: schema.users.clerkId, + username: schema.users.username, + displayName: schema.users.displayName, + }) + .from(schema.users) + .where(inArray(schema.users.clerkId, allOwnerIds)); + for (const owner of owners) { + usernameByClerkId.set(owner.clerkId, owner.username ?? owner.displayName ?? null); + } + } + + // Fix #3: single helper so onTheClock and picks use identical null-fallback logic + const getUsername = (ownerId: string | null) => + ownerId ? (usernameByClerkId.get(ownerId) ?? null) : null; + + const onTheClock = onTheClockSlot + ? { teamName: onTheClockSlot.team.name, username: getUsername(onTheClockSlot.team.ownerId) } + : null; + + const picks = picksRaw.map((p) => ({ + pickNumber: p.pickNumber, + round: p.round, + teamName: p.teamName, + username: getUsername(p.teamOwnerId), + participantName: p.participantName, + sport: p.sport, + })); + + return Response.json({ + seasonId, + status: season.status, + currentPickNumber, + totalPicks, + isDraftComplete, + isPaused: season.draftPaused, + onTheClock, + picks, + }); +}