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

187 lines
5.7 KiB
TypeScript
Raw Normal View History

import { useLoaderData } from "react-router";
import { eq, asc, and } 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";
export async function loader(args: any) {
const { params } = args;
const { seasonId } = 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 });
}
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 });
}
// Check access: allow if public OR user is a league member/commissioner
let hasAccess = season.league.isPublicDraftBoard;
if (!hasAccess && userId) {
// 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)
),
});
hasAccess = !!(isCommissioner || hasTeam);
}
if (!hasAccess) {
throw new Response("This draft board is not public", { 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,
})
.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));
return {
season,
draftSlots,
draftPicks,
};
}
export default function DraftBoard() {
const { season, draftSlots, draftPicks: initialPicks } = useLoaderData<typeof loader>();
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;
const handlePickMade = (data: any) => {
setPicks((prev: any) => [...prev, data.pick]);
setCurrentPick(data.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;
const draftGrid: any[][] = [];
for (let round = 0; round < totalRounds; round++) {
const roundPicks: any[] = [];
for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) {
const pickNumber = round * totalTeams + teamIndex + 1;
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
roundPicks.push(pick || null);
}
draftGrid.push(roundPicks);
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<div className="border-b bg-card sticky top-0 z-10">
<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 Board
</h1>
<div className="flex gap-4 text-sm text-muted-foreground mt-1">
<span>Round: {totalTeams > 0 ? Math.ceil(currentPick / totalTeams) : 1}</span>
<span>Pick: {currentPick}</span>
<span className="capitalize">
{season.status.replace("_", " ")}
</span>
</div>
</div>
{season.status === "draft" && (
<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>
</div>
{/* Draft Grid */}
<div className="w-full px-4 py-4">
<DraftGrid
draftSlots={draftSlots}
draftGrid={draftGrid}
currentPick={currentPick}
/>
</div>
</div>
);
}