feat: add draft room route and entry button from league home page
This commit is contained in:
parent
e2b06be6b6
commit
137c4eb0fd
3 changed files with 248 additions and 4 deletions
|
|
@ -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"),
|
||||
|
|
|
|||
231
app/routes/leagues/$leagueId.draft.$seasonId.tsx
Normal file
231
app/routes/leagues/$leagueId.draft.$seasonId.tsx
Normal file
|
|
@ -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<typeof loader>();
|
||||
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 (
|
||||
<div className="container mx-auto p-4">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{season.league.name} - {season.year} Draft
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
isConnected ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{isConnected ? "Live" : "Disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 text-sm text-gray-600">
|
||||
<span>Round: {currentRound}</span>
|
||||
<span>Pick: {currentPick}</span>
|
||||
<span>Status: {season.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Column: Draft Board */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="p-4">
|
||||
<h2 className="text-xl font-semibold mb-4">Draft Board</h2>
|
||||
|
||||
{/* Current Pick */}
|
||||
{currentDraftSlot && (
|
||||
<div className="mb-6 p-4 bg-blue-50 border-2 border-blue-500 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Now Picking:</p>
|
||||
<p className="text-2xl font-bold">{currentDraftSlot.team.name}</p>
|
||||
</div>
|
||||
<Badge variant="default" className="text-lg px-4 py-2">
|
||||
Pick #{currentPick}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Picks */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold mb-2">Recent Picks</h3>
|
||||
{picks.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">No picks yet...</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{picks
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, 10)
|
||||
.map((pick: any) => (
|
||||
<div
|
||||
key={pick.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline">#{pick.pickNumber}</Badge>
|
||||
<div>
|
||||
<p className="font-semibold">
|
||||
{pick.participant.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{pick.sport.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{pick.team.name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Round {pick.round}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Available Participants */}
|
||||
<div>
|
||||
<Card className="p-4">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Available Participants ({availableParticipants.length})
|
||||
</h2>
|
||||
<div className="space-y-2 max-h-[600px] overflow-y-auto">
|
||||
{availableParticipants.map((participant: any) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer transition-colors"
|
||||
>
|
||||
<p className="font-semibold">{participant.name}</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{participant.sport.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -300,10 +300,19 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
{season && (season.status === "pre_draft" || season.status === "draft") && draftSlots.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Draft Order</CardTitle>
|
||||
<CardDescription>
|
||||
{season.status === "pre_draft" ? "Upcoming draft order" : "Current draft order"}
|
||||
</CardDescription>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Draft Order</CardTitle>
|
||||
<CardDescription>
|
||||
{season.status === "pre_draft" ? "Upcoming draft order" : "Current draft order"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button asChild size="sm">
|
||||
<Link to={`/leagues/${league.id}/draft/${season.id}`}>
|
||||
Enter Draft Room
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue