brackt/app/routes/leagues/$leagueId.draft.$seasonId.tsx

366 lines
13 KiB
TypeScript

import { useLoaderData, Link } 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";
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 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));
// 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<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 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<Array<{ pickNumber: number; round: number; pickInRound: number; teamId: string; pick?: any }>> = [];
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();
return (
<div className="min-h-screen bg-background">
{/* Standalone Header - No Main Nav */}
<div className="border-b bg-card">
<div className="w-full px-4 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">
{season.league.name} - {season.year} Draft
</h1>
<div className="flex gap-4 text-sm text-muted-foreground mt-1">
<span>Round: {currentRound}</span>
<span>Pick: {currentPick}</span>
<span className="capitalize">{season.status.replace("_", " ")}</span>
</div>
</div>
<div className="flex items-center gap-3">
<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>
<Button variant="outline" asChild>
<Link to={`/leagues/${season.leagueId}`}>
Exit Draft Room
</Link>
</Button>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="w-full px-4 py-4">
{/* Draft Grid - Horizontally Scrollable */}
<div className="mb-6">
<Card className="p-4">
<h2 className="text-xl font-semibold mb-4">Draft Grid</h2>
<div className="overflow-x-auto">
<div className="w-full">
{/* Team Headers */}
<div className="flex gap-2 mb-2">
{draftSlots.map((slot) => (
<div
key={slot.id}
className="flex-1 min-w-32 text-center"
>
<div className="font-semibold text-sm truncate px-2">
{slot.team.name}
</div>
<div className="text-xs text-muted-foreground">
Pick {slot.draftOrder}
</div>
</div>
))}
</div>
{/* Draft Grid Rows */}
<div className="space-y-2">
{draftGrid.map((roundPicks, roundIndex) => {
const round = roundIndex + 1;
const isEvenRound = round % 2 === 0;
const displayPicks = isEvenRound ? [...roundPicks].reverse() : roundPicks;
return (
<div key={roundIndex} className="flex gap-2">
{displayPicks.map((cell) => {
const isCurrent = cell.pickNumber === currentPick;
const isPicked = !!cell.pick;
return (
<div
key={cell.pickNumber}
className={`flex-1 min-w-32 h-20 border-2 rounded-lg p-2 transition-all ${
isCurrent
? "border-blue-500 bg-blue-50 dark:bg-blue-950 shadow-lg"
: isPicked
? "border-green-500 bg-green-50 dark:bg-green-950"
: "border-gray-300 bg-white dark:bg-gray-900"
}`}
title={`Overall Pick #${cell.pickNumber}`}
>
<div className="text-xs font-mono text-muted-foreground mb-1">
{cell.round}.{String(cell.pickInRound).padStart(2, "0")}
</div>
{isPicked ? (
<div className="text-xs">
<div className="font-semibold truncate">
{cell.pick.participant.name}
</div>
<div className="text-muted-foreground truncate">
{cell.pick.sport.name}
</div>
</div>
) : isCurrent ? (
<div className="text-xs font-semibold text-blue-600 dark:text-blue-400">
On Clock
</div>
) : null}
</div>
);
})}
</div>
);
})}
</div>
</div>
</div>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Picks */}
<div>
<Card className="p-4">
<h2 className="text-xl font-semibold mb-4">Recent Picks</h2>
{picks.length === 0 ? (
<p className="text-muted-foreground text-sm">No picks yet...</p>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{picks
.slice()
.reverse()
.slice(0, 10)
.map((pick: any) => (
<div
key={pick.id}
className="flex items-center justify-between p-3 bg-muted 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-muted-foreground">
{pick.sport.name}
</p>
</div>
</div>
<div className="text-right">
<p className="font-medium">{pick.team.name}</p>
<p className="text-xs text-muted-foreground">
Round {pick.round}
</p>
</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>
</div>
);
}