From 137c4eb0fd4fc1092800e57daf405c58a5138fa2 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Fri, 17 Oct 2025 12:30:58 -0700 Subject: [PATCH] feat: add draft room route and entry button from league home page --- app/routes.ts | 4 + .../leagues/$leagueId.draft.$seasonId.tsx | 231 ++++++++++++++++++ app/routes/leagues/$leagueId.tsx | 17 +- 3 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 app/routes/leagues/$leagueId.draft.$seasonId.tsx diff --git a/app/routes.ts b/app/routes.ts index c71d701..8212555 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -6,6 +6,10 @@ export default [ route("leagues/new", "routes/leagues/new.tsx"), route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"), route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"), + route( + "leagues/:leagueId/draft/:seasonId", + "routes/leagues/$leagueId.draft.$seasonId.tsx" + ), route("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"), route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"), route("user-profile", "routes/user-profile.tsx"), diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx new file mode 100644 index 0000000..9fc3e2f --- /dev/null +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -0,0 +1,231 @@ +import { useLoaderData } from "react-router"; +import { eq, and, asc, notInArray } 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"; + +export async function loader({ params }: { params: { seasonId: string } }) { + const { 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, + teams: true, + }, + }); + + if (!season) { + throw new Response("Season not found", { status: 404 }); + } + + // 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 available participants (not yet picked) + const pickedParticipantIds = draftPicks.map((p) => p.participant.id); + const 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( + and( + eq(schema.participants.sportsSeasonId, season.templateId!), + pickedParticipantIds.length > 0 + ? notInArray(schema.participants.id, pickedParticipantIds) + : undefined + ) + ) + .orderBy(asc(schema.participants.name)); + + return { + season, + draftSlots, + draftPicks, + availableParticipants, + }; +} + +export default function DraftRoom() { + const { season, draftSlots, draftPicks, availableParticipants } = + useLoaderData(); + const { isConnected, on, off } = useDraftSocket(season.id); + const [picks, setPicks] = useState(draftPicks); + const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1); + + // 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 team on the clock + const currentDraftSlot = draftSlots[(currentPick - 1) % draftSlots.length]; + const currentRound = Math.ceil(currentPick / draftSlots.length); + + return ( +
+ {/* Header */} +
+
+

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

+
+
+ + {isConnected ? "Live" : "Disconnected"} + +
+
+
+ Round: {currentRound} + Pick: {currentPick} + Status: {season.status} +
+
+ +
+ {/* Left Column: Draft Board */} +
+ +

Draft Board

+ + {/* Current Pick */} + {currentDraftSlot && ( +
+
+
+

Now Picking:

+

{currentDraftSlot.team.name}

+
+ + Pick #{currentPick} + +
+
+ )} + + {/* 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 ({availableParticipants.length}) +

+
+ {availableParticipants.map((participant: any) => ( +
+

{participant.name}

+

+ {participant.sport.name} +

+
+ ))} +
+
+
+
+
+ ); +} diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx index aef86b6..4ee5e1d 100644 --- a/app/routes/leagues/$leagueId.tsx +++ b/app/routes/leagues/$leagueId.tsx @@ -300,10 +300,19 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { {season && (season.status === "pre_draft" || season.status === "draft") && draftSlots.length > 0 && ( - Draft Order - - {season.status === "pre_draft" ? "Upcoming draft order" : "Current draft order"} - +
+
+ Draft Order + + {season.status === "pre_draft" ? "Upcoming draft order" : "Current draft order"} + +
+ +