feat: implement draft grid and reorganize draft room layout

This commit is contained in:
Chris Parsons 2025-10-16 01:12:50 -07:00
parent 045e0f2cab
commit 64d85c135a
2 changed files with 202 additions and 19 deletions

View file

@ -0,0 +1,155 @@
import { useMemo } from "react";
interface Team {
id: string;
name: string;
draftOrder: number;
ownerId: string | null;
}
interface DraftPick {
id: string;
teamId: string;
participantId: string;
pickNumber: number;
round: number;
pickInRound: number;
pickedByType: "owner" | "commissioner" | "auto";
}
interface Participant {
id: string;
name: string;
sport: string;
}
interface DraftGridProps {
teams: Team[];
draftPicks: DraftPick[];
participants: Participant[];
draftRounds: number;
currentPickNumber: number;
userTeamId?: string;
}
export function DraftGrid({
teams,
draftPicks,
participants,
draftRounds,
currentPickNumber,
userTeamId,
}: DraftGridProps) {
// Create a map of participant IDs to participant data for quick lookup
const participantMap = useMemo(() => {
return new Map(participants.map((p) => [p.id, p]));
}, [participants]);
// Create a map of pick numbers to draft picks
const pickMap = useMemo(() => {
return new Map(draftPicks.map((p) => [p.pickNumber, p]));
}, [draftPicks]);
// Calculate pick number for a given round and team
const getPickNumber = (round: number, teamIndex: number): number => {
const isOddRound = round % 2 === 1;
const pickInRound = isOddRound ? teamIndex + 1 : teams.length - teamIndex;
return (round - 1) * teams.length + pickInRound;
};
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full">
{/* Header Row - Team Names */}
<div className="flex border-b-2 border-border bg-muted/50 sticky top-0 z-10">
<div className="w-16 flex-shrink-0 p-2 font-semibold text-sm border-r">
Round
</div>
{teams.map((team) => (
<div
key={team.id}
className={`flex-1 min-w-[140px] p-2 text-center font-semibold text-sm border-r last:border-r-0 ${
team.id === userTeamId ? "bg-primary/10" : ""
}`}
>
<div className="truncate" title={team.name}>
{team.name}
</div>
</div>
))}
</div>
{/* Draft Rounds */}
{Array.from({ length: draftRounds }, (_, roundIndex) => {
const round = roundIndex + 1;
const isOddRound = round % 2 === 1;
return (
<div key={round} className="flex border-b border-border hover:bg-muted/30">
{/* Round Number */}
<div className="w-16 flex-shrink-0 p-2 text-center font-medium text-sm border-r bg-muted/30">
{round}
</div>
{/* Picks for this round */}
{teams.map((team, teamIndex) => {
const pickNumber = getPickNumber(round, teamIndex);
const pick = pickMap.get(pickNumber);
const participant = pick ? participantMap.get(pick.participantId) : null;
const isCurrentPick = pickNumber === currentPickNumber;
const isPastPick = pickNumber < currentPickNumber;
const isUserTeam = team.id === userTeamId;
return (
<div
key={team.id}
className={`flex-1 min-w-[140px] p-2 border-r last:border-r-0 ${
isUserTeam ? "bg-primary/5" : ""
} ${isCurrentPick ? "ring-2 ring-primary ring-inset" : ""}`}
>
<div className="flex flex-col gap-1">
{/* Pick Number */}
<div className="text-xs text-muted-foreground">
{round}.{String(isOddRound ? teamIndex + 1 : teams.length - teamIndex).padStart(2, "0")}
</div>
{/* Participant or Status */}
{participant ? (
<div className="space-y-0.5">
<div className="font-medium text-sm truncate" title={participant.name}>
{participant.name}
</div>
<div className="text-xs text-muted-foreground truncate">
{participant.sport}
</div>
{pick && pick.pickedByType !== "owner" && (
<div className="text-xs text-muted-foreground italic">
({pick.pickedByType})
</div>
)}
</div>
) : isCurrentPick ? (
<div className="text-sm font-medium text-primary">
On the clock
</div>
) : isPastPick ? (
<div className="text-xs text-muted-foreground italic">
Skipped
</div>
) : (
<div className="text-xs text-muted-foreground">
-
</div>
)}
</div>
</div>
);
})}
</div>
);
})}
</div>
</div>
);
}

View file

@ -9,7 +9,9 @@ import {
findDraftSlotsBySeasonId,
getDraftPicks,
getSeasonTimers,
findSeasonWithSportsSeasons,
} from "~/models";
import { DraftGrid } from "~/components/draft/DraftGrid";
export async function loader(args: LoaderFunctionArgs) {
const { userId } = await getAuth(args);
@ -33,6 +35,12 @@ export async function loader(args: LoaderFunctionArgs) {
throw new Response("No active season found", { status: 404 });
}
// Get season with sports and participants
const seasonWithSports = await findSeasonWithSportsSeasons(season.id);
const participants = seasonWithSports?.seasonSports?.flatMap(
(ss: any) => ss.sportsSeason?.participants || []
) || [];
// Get teams and draft order
const teams = await findTeamsBySeasonId(season.id);
const draftSlots = await findDraftSlotsBySeasonId(season.id);
@ -67,6 +75,7 @@ export async function loader(args: LoaderFunctionArgs) {
isCommissioner: isUserCommissioner,
userTeam,
draftPicks,
participants,
timers,
currentPickNumber,
totalPicks,
@ -79,6 +88,9 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
const {
league,
season,
teams,
draftPicks,
participants,
isCommissioner,
userTeam,
currentPickNumber,
@ -90,7 +102,7 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-4 py-4">
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{league.name} Draft</h1>
@ -115,7 +127,7 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-6">
<main className="px-6 py-6">
{isDraftComplete ? (
<div className="rounded-lg border bg-card p-8 text-center">
<h2 className="text-2xl font-bold">Draft Complete!</h2>
@ -124,29 +136,31 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
</p>
</div>
) : (
<div className="grid gap-6 lg:grid-cols-[1fr_400px]">
{/* Left Column - Draft Grid & Player List */}
<div className="space-y-6">
{/* Draft Grid Placeholder */}
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Draft Board</h2>
<div className="flex items-center justify-center py-12 text-muted-foreground">
Draft grid will be implemented in Phase 5
</div>
</div>
<div className="space-y-6">
{/* Draft Board - Full Width */}
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Draft Board</h2>
<DraftGrid
teams={teams}
draftPicks={draftPicks}
participants={participants}
draftRounds={season.draftRounds}
currentPickNumber={currentPickNumber}
userTeamId={userTeam?.id}
/>
</div>
{/* Player List Placeholder */}
{/* 2x2 Grid Below */}
<div className="grid gap-6 md:grid-cols-2">
{/* Available Players */}
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Available Players</h2>
<div className="flex items-center justify-center py-12 text-muted-foreground">
Player list will be implemented in Phase 6
</div>
</div>
</div>
{/* Right Column - Queue & Controls */}
<div className="space-y-6">
{/* Queue Placeholder */}
{/* Your Queue */}
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Your Queue</h2>
<div className="flex items-center justify-center py-12 text-muted-foreground">
@ -154,7 +168,7 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
</div>
</div>
{/* Timer Placeholder */}
{/* Draft Timer */}
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Draft Timer</h2>
<div className="flex items-center justify-center py-12 text-muted-foreground">
@ -163,13 +177,27 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
</div>
{/* Commissioner Controls */}
{isCommissioner && (
{isCommissioner ? (
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Commissioner Controls</h2>
<div className="flex items-center justify-center py-12 text-muted-foreground">
Controls will be implemented in Phase 9
</div>
</div>
) : (
<div className="rounded-lg border bg-card p-6">
<h2 className="mb-4 text-xl font-bold">Draft Info</h2>
<div className="space-y-2 text-sm">
<div>
<span className="text-muted-foreground">Current Pick:</span>
<span className="ml-2 font-medium">{currentPickNumber} of {totalPicks}</span>
</div>
<div>
<span className="text-muted-foreground">Draft Rounds:</span>
<span className="ml-2 font-medium">{season.draftRounds}</span>
</div>
</div>
</div>
)}
</div>
</div>