feat: implement draft grid and reorganize draft room layout
This commit is contained in:
parent
045e0f2cab
commit
64d85c135a
2 changed files with 202 additions and 19 deletions
155
app/components/draft/DraftGrid.tsx
Normal file
155
app/components/draft/DraftGrid.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,9 @@ import {
|
||||||
findDraftSlotsBySeasonId,
|
findDraftSlotsBySeasonId,
|
||||||
getDraftPicks,
|
getDraftPicks,
|
||||||
getSeasonTimers,
|
getSeasonTimers,
|
||||||
|
findSeasonWithSportsSeasons,
|
||||||
} from "~/models";
|
} from "~/models";
|
||||||
|
import { DraftGrid } from "~/components/draft/DraftGrid";
|
||||||
|
|
||||||
export async function loader(args: LoaderFunctionArgs) {
|
export async function loader(args: LoaderFunctionArgs) {
|
||||||
const { userId } = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
|
|
@ -33,6 +35,12 @@ export async function loader(args: LoaderFunctionArgs) {
|
||||||
throw new Response("No active season found", { status: 404 });
|
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
|
// Get teams and draft order
|
||||||
const teams = await findTeamsBySeasonId(season.id);
|
const teams = await findTeamsBySeasonId(season.id);
|
||||||
const draftSlots = await findDraftSlotsBySeasonId(season.id);
|
const draftSlots = await findDraftSlotsBySeasonId(season.id);
|
||||||
|
|
@ -67,6 +75,7 @@ export async function loader(args: LoaderFunctionArgs) {
|
||||||
isCommissioner: isUserCommissioner,
|
isCommissioner: isUserCommissioner,
|
||||||
userTeam,
|
userTeam,
|
||||||
draftPicks,
|
draftPicks,
|
||||||
|
participants,
|
||||||
timers,
|
timers,
|
||||||
currentPickNumber,
|
currentPickNumber,
|
||||||
totalPicks,
|
totalPicks,
|
||||||
|
|
@ -79,6 +88,9 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
||||||
const {
|
const {
|
||||||
league,
|
league,
|
||||||
season,
|
season,
|
||||||
|
teams,
|
||||||
|
draftPicks,
|
||||||
|
participants,
|
||||||
isCommissioner,
|
isCommissioner,
|
||||||
userTeam,
|
userTeam,
|
||||||
currentPickNumber,
|
currentPickNumber,
|
||||||
|
|
@ -90,7 +102,7 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="border-b bg-card">
|
<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 className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">{league.name} Draft</h1>
|
<h1 className="text-2xl font-bold">{league.name} Draft</h1>
|
||||||
|
|
@ -115,7 +127,7 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="container mx-auto px-4 py-6">
|
<main className="px-6 py-6">
|
||||||
{isDraftComplete ? (
|
{isDraftComplete ? (
|
||||||
<div className="rounded-lg border bg-card p-8 text-center">
|
<div className="rounded-lg border bg-card p-8 text-center">
|
||||||
<h2 className="text-2xl font-bold">Draft Complete!</h2>
|
<h2 className="text-2xl font-bold">Draft Complete!</h2>
|
||||||
|
|
@ -124,29 +136,31 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-6 lg:grid-cols-[1fr_400px]">
|
<div className="space-y-6">
|
||||||
{/* Left Column - Draft Grid & Player List */}
|
{/* Draft Board - Full Width */}
|
||||||
<div className="space-y-6">
|
<div className="rounded-lg border bg-card p-6">
|
||||||
{/* Draft Grid Placeholder */}
|
<h2 className="mb-4 text-xl font-bold">Draft Board</h2>
|
||||||
<div className="rounded-lg border bg-card p-6">
|
<DraftGrid
|
||||||
<h2 className="mb-4 text-xl font-bold">Draft Board</h2>
|
teams={teams}
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
draftPicks={draftPicks}
|
||||||
Draft grid will be implemented in Phase 5
|
participants={participants}
|
||||||
</div>
|
draftRounds={season.draftRounds}
|
||||||
</div>
|
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">
|
<div className="rounded-lg border bg-card p-6">
|
||||||
<h2 className="mb-4 text-xl font-bold">Available Players</h2>
|
<h2 className="mb-4 text-xl font-bold">Available Players</h2>
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||||
Player list will be implemented in Phase 6
|
Player list will be implemented in Phase 6
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Queue & Controls */}
|
{/* Your Queue */}
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Queue Placeholder */}
|
|
||||||
<div className="rounded-lg border bg-card p-6">
|
<div className="rounded-lg border bg-card p-6">
|
||||||
<h2 className="mb-4 text-xl font-bold">Your Queue</h2>
|
<h2 className="mb-4 text-xl font-bold">Your Queue</h2>
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timer Placeholder */}
|
{/* Draft Timer */}
|
||||||
<div className="rounded-lg border bg-card p-6">
|
<div className="rounded-lg border bg-card p-6">
|
||||||
<h2 className="mb-4 text-xl font-bold">Draft Timer</h2>
|
<h2 className="mb-4 text-xl font-bold">Draft Timer</h2>
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Commissioner Controls */}
|
{/* Commissioner Controls */}
|
||||||
{isCommissioner && (
|
{isCommissioner ? (
|
||||||
<div className="rounded-lg border bg-card p-6">
|
<div className="rounded-lg border bg-card p-6">
|
||||||
<h2 className="mb-4 text-xl font-bold">Commissioner Controls</h2>
|
<h2 className="mb-4 text-xl font-bold">Commissioner Controls</h2>
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||||
Controls will be implemented in Phase 9
|
Controls will be implemented in Phase 9
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue