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

1112 lines
37 KiB
TypeScript

import { useLoaderData, Link } from "react-router";
import { eq, and, asc, desc, inArray } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { useDraftSocket } from "~/hooks/useDraftSocket";
import { useEffect, useState, useMemo } from "react";
import { Card } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { getAuth } from "@clerk/react-router/server";
import { getTeamQueue } from "~/models/draft-queue";
import { DraftSidebar } from "~/components/DraftSidebar";
import { TeamsDraftedGrid } from "~/components/draft/TeamsDraftedGrid";
import { QueueSection } from "~/components/draft/QueueSection";
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks";
import { DraftGridSection } from "~/components/draft/DraftGridSection";
import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay";
import { calculateDraftEligibility, getEligibilitySummary } from "~/lib/draft-eligibility";
import { toast } from "sonner";
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 sports seasons for this season
const seasonSports = await db.query.seasonSports.findMany({
where: eq(schema.seasonSports.seasonId, seasonId),
});
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
// Get ALL participants (both drafted and undrafted) - sorted by EV desc, then name
// Filtering will be done on the client side based on the "Show Drafted" toggle
let availableParticipants: any[] = [];
if (sportsSeasonIds.length > 0) {
availableParticipants = await db
.select({
id: schema.participants.id,
name: schema.participants.name,
expectedValue: schema.participants.expectedValue,
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(
sportsSeasonIds.length === 1
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
)
.orderBy(
desc(schema.participants.expectedValue),
asc(schema.participants.name)
);
}
// Load user's team queue if they have a team
const userQueue = userTeam ? await getTeamQueue(userTeam.id) : [];
// Load draft timers for all teams
const timers = await db.query.draftTimers.findMany({
where: eq(schema.draftTimers.seasonId, seasonId),
});
// Load autodraft settings for all teams
const autodraftSettings = await db.query.autodraftSettings.findMany({
where: eq(schema.autodraftSettings.seasonId, seasonId),
});
// Load user's autodraft settings if they have a team
const userAutodraftSettings = userTeam
? await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, seasonId),
eq(schema.autodraftSettings.teamId, userTeam.id)
),
})
: null;
return {
season,
draftSlots,
draftPicks,
availableParticipants,
userTeam,
userQueue,
timers,
autodraftSettings,
userAutodraftSettings,
isCommissioner: !!isCommissioner,
currentUserId: userId,
};
}
export default function DraftRoom() {
const {
season,
draftSlots,
draftPicks,
availableParticipants,
userTeam,
userQueue,
timers,
autodraftSettings,
userAutodraftSettings,
isCommissioner,
} = useLoaderData<typeof loader>();
const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id);
const [picks, setPicks] = useState(draftPicks);
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
const [searchQuery, setSearchQuery] = useState("");
const [hideDrafted, setHideDrafted] = useState(true);
const [sportFilter, setSportFilter] = useState<string>("all");
const [queue, setQueue] = useState(userQueue);
const [isPaused, setIsPaused] = useState(season.draftPaused || false);
const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active");
// Sidebar and tabs state
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
if (typeof window === "undefined") return false;
const stored = localStorage.getItem("draftSidebarCollapsed");
return stored ? JSON.parse(stored) : false;
});
const [activeTab, setActiveTab] = useState<"participants" | "board" | "teams">("participants");
// Track autodraft status for all teams
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, boolean>>(() => {
const status: Record<string, boolean> = {};
autodraftSettings.forEach((setting: any) => {
status[setting.teamId] = setting.isEnabled;
});
return status;
});
// Track connected teams
const [connectedTeams, setConnectedTeams] = useState<Set<string>>(new Set());
// Track user's autodraft settings
const [userAutodraft, setUserAutodraft] = useState({
isEnabled: userAutodraftSettings?.isEnabled || false,
mode: (userAutodraftSettings?.mode || "next_pick") as "next_pick" | "while_on",
});
const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
// Initialize with timers from loader data, or use initial time if draft hasn't started
const timersMap: Record<string, number> = {};
const initialTime = season.draftInitialTime || 120;
if (timers.length > 0) {
// Draft has started, use actual timers
timers.forEach((timer: any) => {
timersMap[timer.teamId] = timer.timeRemaining;
});
} else {
// Draft hasn't started, show initial time for all teams
draftSlots.forEach((slot) => {
timersMap[slot.team.id] = initialTime;
});
}
return timersMap;
});
const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
const [selectedPickSlot, setSelectedPickSlot] = useState<{
pickNumber: number;
teamId: string;
} | null>(null);
// Calculate draft eligibility for current user's team
const eligibility = useMemo(() => {
if (!userTeam) return null;
// Transform picks data to match eligibility function signature
const transformedPicks = picks.map((p: any) => ({
teamId: p.team.id,
participant: {
id: p.participant.id,
sport: {
id: p.sport.id,
name: p.sport.name,
},
},
}));
const userTeamPicks = transformedPicks.filter(
(p: any) => p.teamId === userTeam.id
);
// Transform participants to match signature
const transformedParticipants = availableParticipants.map((p: any) => ({
id: p.id,
sport: {
id: p.sport.id,
name: p.sport.name,
},
}));
// Get unique sports from availableParticipants
const sportsMap = new Map();
availableParticipants.forEach((p: any) => {
if (!sportsMap.has(p.sport.id)) {
sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name });
}
});
const seasonSportsData = Array.from(sportsMap.values());
return calculateDraftEligibility(
userTeam.id,
userTeamPicks,
transformedPicks,
transformedParticipants,
seasonSportsData,
season.draftRounds,
season.teams
);
}, [userTeam, picks, availableParticipants, season]);
// Calculate eligibility for force manual pick dialog (selected team)
const forcePickEligibility = useMemo(() => {
if (!selectedPickSlot) return null;
const transformedPicks = picks.map((p: any) => ({
teamId: p.team.id,
participant: {
id: p.participant.id,
sport: {
id: p.sport.id,
name: p.sport.name,
},
},
}));
const selectedTeamPicks = transformedPicks.filter(
(p: any) => p.teamId === selectedPickSlot.teamId
);
const transformedParticipants = availableParticipants.map((p: any) => ({
id: p.id,
sport: {
id: p.sport.id,
name: p.sport.name,
},
}));
const sportsMap = new Map();
availableParticipants.forEach((p: any) => {
if (!sportsMap.has(p.sport.id)) {
sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name });
}
});
const seasonSportsData = Array.from(sportsMap.values());
return calculateDraftEligibility(
selectedPickSlot.teamId,
selectedTeamPicks,
transformedPicks,
transformedParticipants,
seasonSportsData,
season.draftRounds,
season.teams
);
}, [selectedPickSlot, picks, availableParticipants, season]);
// 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);
};
const handleTimerUpdate = (data: any) => {
// Update the specific team's timer
setTeamTimers((prev) => ({
...prev,
[data.teamId]: data.timeRemaining,
}));
};
const handleDraftPaused = () => {
setIsPaused(true);
};
const handleDraftResumed = () => {
setIsPaused(false);
};
const handleDraftCompleted = () => {
setIsDraftComplete(true);
};
const handleAutodraftUpdated = (data: { teamId: string; isEnabled: boolean; mode: "next_pick" | "while_on" }) => {
console.log("Autodraft updated:", data);
setAutodraftStatus((prev) => ({
...prev,
[data.teamId]: data.isEnabled,
}));
// Update user's local state if it's their team
if (userTeam && data.teamId === userTeam.id) {
setUserAutodraft({
isEnabled: data.isEnabled,
mode: data.mode,
});
}
};
const handleTeamConnected = (data: { teamId: string }) => {
console.log("Team connected:", data.teamId);
setConnectedTeams((prev) => new Set(prev).add(data.teamId));
};
const handleTeamDisconnected = (data: { teamId: string }) => {
console.log("Team disconnected:", data.teamId);
setConnectedTeams((prev) => {
const newSet = new Set(prev);
newSet.delete(data.teamId);
return newSet;
});
};
const handleConnectedTeamsList = (data: { teamIds: string[] }) => {
console.log("Received connected teams list:", data.teamIds);
setConnectedTeams(new Set(data.teamIds));
};
const handleParticipantRemovedFromQueues = (data: { participantId: string }) => {
console.log("Participant removed from queues:", data.participantId);
setQueue((prev: any) =>
prev.filter((item: any) => item.participantId !== data.participantId)
);
};
on("pick-made", handlePickMade);
on("timer-update", handleTimerUpdate);
on("draft-paused", handleDraftPaused);
on("draft-resumed", handleDraftResumed);
on("draft-completed", handleDraftCompleted);
on("autodraft-updated", handleAutodraftUpdated);
on("team-connected", handleTeamConnected);
on("team-disconnected", handleTeamDisconnected);
on("connected-teams-list", handleConnectedTeamsList);
on("participant-removed-from-queues", handleParticipantRemovedFromQueues);
return () => {
off("pick-made", handlePickMade);
off("timer-update", handleTimerUpdate);
off("draft-paused", handleDraftPaused);
off("draft-resumed", handleDraftResumed);
off("draft-completed", handleDraftCompleted);
off("autodraft-updated", handleAutodraftUpdated);
off("team-connected", handleTeamConnected);
off("team-disconnected", handleTeamDisconnected);
off("connected-teams-list", handleConnectedTeamsList);
off("participant-removed-from-queues", handleParticipantRemovedFromQueues);
};
}, [on, off, currentPick, userTeam]);
// Persist sidebar collapsed state
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("draftSidebarCollapsed", JSON.stringify(sidebarCollapsed));
}
}, [sidebarCollapsed]);
// Queue handlers
const handleAddToQueue = async (participantId: string) => {
if (!userTeam) return;
// Check if participant is already in queue (client-side)
const alreadyInQueue = queue.some((item: any) => item.participantId === participantId);
if (alreadyInQueue) {
toast.error("This participant is already in your queue");
return;
}
// Optimistic update - create a temporary queue item
const tempQueueItem = {
id: `temp-${Date.now()}`, // Temporary ID
participantId,
queuePosition: queue.length + 1,
};
// Immediately update the UI
setQueue((prev: any) => [...prev, tempQueueItem]);
const formData = new FormData();
formData.append("seasonId", season.id);
formData.append("teamId", userTeam.id);
formData.append("participantId", participantId);
try {
const response = await fetch("/api/queue/add", {
method: "POST",
body: formData,
});
if (response.ok) {
const data = await response.json();
// Replace temp item with real item from server
setQueue((prev: any) =>
prev.map((item: any) =>
item.id === tempQueueItem.id ? data.queueItem : item
)
);
} else {
// Revert the optimistic update on error
setQueue((prev: any) =>
prev.filter((item: any) => item.id !== tempQueueItem.id)
);
const error = await response.json();
toast.error(error.error || "Failed to add to queue");
}
} catch (error) {
// Revert the optimistic update on network error
setQueue((prev: any) =>
prev.filter((item: any) => item.id !== tempQueueItem.id)
);
toast.error("Network error - failed to add to queue");
}
};
const handleRemoveFromQueue = async (queueId: string) => {
if (!userTeam) return;
const formData = new FormData();
formData.append("queueId", queueId);
formData.append("teamId", userTeam.id);
const response = await fetch("/api/queue/remove", {
method: "POST",
body: formData,
});
if (response.ok) {
const data = await response.json();
setQueue(data.queue);
}
};
const handleReorderQueue = async (participantIds: string[]) => {
if (!userTeam) return;
const formData = new FormData();
formData.append("teamId", userTeam.id);
formData.append("seasonId", season.id);
formData.append("participantIds", JSON.stringify(participantIds));
const response = await fetch("/api/queue/reorder", {
method: "POST",
body: formData,
});
if (response.ok) {
const data = await response.json();
setQueue(data.queue);
}
};
const handleStartDraft = async () => {
const formData = new FormData();
formData.append("seasonId", season.id);
const response = await fetch("/api/draft/start", {
method: "POST",
body: formData,
});
if (response.ok) {
window.location.reload(); // Reload to get updated season status
}
};
const handlePauseDraft = async () => {
const formData = new FormData();
formData.append("seasonId", season.id);
const response = await fetch("/api/draft/pause", {
method: "POST",
body: formData,
});
if (response.ok) {
setIsPaused(true);
}
};
const handleResumeDraft = async () => {
const formData = new FormData();
formData.append("seasonId", season.id);
const response = await fetch("/api/draft/resume", {
method: "POST",
body: formData,
});
if (response.ok) {
setIsPaused(false);
}
};
const handleMakePick = async (participantId: string) => {
const formData = new FormData();
formData.append("seasonId", season.id);
formData.append("participantId", participantId);
const response = await fetch("/api/draft/make-pick", {
method: "POST",
body: formData,
});
if (response.ok) {
// Pick will be broadcast via socket, no need to manually update
} else {
const error = await response.json();
alert(error.error || "Failed to make pick");
}
};
const handleForceAutopick = async (pickNumber: number, teamId: string) => {
const formData = new FormData();
formData.append("seasonId", season.id);
formData.append("teamId", teamId);
formData.append("pickNumber", pickNumber.toString());
const response = await fetch("/api/draft/force-autopick", {
method: "POST",
body: formData,
});
if (response.ok) {
// Pick will be broadcast via socket
} else {
const error = await response.json();
alert(error.error || "Failed to force autopick");
}
};
const handleForceManualPick = async (participantId: string) => {
if (!selectedPickSlot) return;
const formData = new FormData();
formData.append("seasonId", season.id);
formData.append("teamId", selectedPickSlot.teamId);
formData.append("participantId", participantId);
formData.append("pickNumber", selectedPickSlot.pickNumber.toString());
const response = await fetch("/api/draft/force-manual-pick", {
method: "POST",
body: formData,
});
if (response.ok) {
setForcePickDialogOpen(false);
setSelectedPickSlot(null);
} else {
const error = await response.json();
alert(error.error || "Failed to force manual pick");
}
};
// Calculate current round
const currentRound = Math.ceil(currentPick / draftSlots.length);
// Determine whose turn it is
const totalTeams = draftSlots.length;
const isSnakeDraft = true;
const isEvenRound = currentRound % 2 === 0;
let pickInRound = ((currentPick - 1) % totalTeams) + 1;
if (isSnakeDraft && isEvenRound) {
pickInRound = totalTeams - pickInRound + 1;
}
const currentDraftSlot = draftSlots.find(
(slot) => slot.draftOrder === pickInRound
);
const isMyTurn = !!(
userTeam && currentDraftSlot && currentDraftSlot.team.id === userTeam.id
);
const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn
// Format timer display
const formatTime = (seconds: number | null) => {
if (seconds === null) return "--:--";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}
return `${minutes}:${String(secs).padStart(2, "0")}`;
};
// Get timer color based on time remaining
const getTimerColor = (seconds: number | null) => {
if (seconds === null) return "text-muted-foreground";
if (seconds > 60) return "text-green-600 dark:text-green-400";
if (seconds > 30) return "text-yellow-600 dark:text-yellow-400";
if (seconds > 10) return "text-red-600 dark:text-red-400";
return "text-red-600 dark:text-red-400 animate-pulse";
};
// 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();
// Get drafted participant IDs for filtering
const draftedParticipantIds = new Set(
picks.map((p: any) => p.participant.id)
);
// Get unique sports for filter dropdown
const uniqueSports = Array.from(
new Set(availableParticipants.map((p: any) => p.sport.name))
).sort();
// Filter participants based on search, sport, and drafted status
const filteredParticipants = availableParticipants.filter(
(participant: any) => {
// Drafted filter - hide drafted participants by default
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
return false;
}
// Search filter
if (
searchQuery &&
!participant.name.toLowerCase().includes(searchQuery.toLowerCase())
) {
return false;
}
// Sport filter
if (sportFilter !== "all" && participant.sport.name !== sportFilter) {
return false;
}
return true;
}
);
return (
<div className="h-screen bg-background flex flex-col overflow-hidden">
{/* Draft Completion Banner */}
{isDraftComplete && (
<div className="bg-green-500 text-white px-4 py-3 text-center font-semibold flex-shrink-0">
🎉 Draft Complete! The season is now active.
{season.league.isPublicDraftBoard && (
<>
{" "}
<a
href={`/leagues/${season.leagueId}/draft-board/${season.id}`}
className="underline hover:text-green-100"
>
View Draft Board
</a>
</>
)}
</div>
)}
{/* Standalone Header - No Main Nav */}
<div className="border-b bg-card flex-shrink-0">
<div className="w-full px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* Mobile menu button - only show if user has team and sidebar is collapsed */}
{userTeam && sidebarCollapsed && (
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarCollapsed(false)}
className="lg:hidden"
aria-label="Open sidebar"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</Button>
)}
<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>
<div className="flex items-center gap-3">
{/* Commissioner Controls */}
{isCommissioner && season.status === "pre_draft" && (
<Button onClick={handleStartDraft}>Start Draft</Button>
)}
{isCommissioner && season.status === "draft" && (
<>
{isPaused ? (
<Button onClick={handleResumeDraft} variant="default">
Resume Draft
</Button>
) : (
<Button onClick={handlePauseDraft} variant="outline">
Pause Draft
</Button>
)}
</>
)}
<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 - Flex Row: Sidebar + Tabs */}
<div className="flex-1 flex overflow-hidden">
{/* Sidebar */}
{userTeam && (
<DraftSidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
queueSection={
<QueueSection
queue={queue}
availableParticipants={availableParticipants}
eligibility={eligibility}
seasonId={season.id}
teamId={userTeam.id}
isMyTurn={isMyTurn}
userAutodraft={userAutodraft}
onRemoveFromQueue={handleRemoveFromQueue}
onAutodraftUpdate={(isEnabled, mode) => {
setUserAutodraft({ isEnabled, mode });
}}
onReorder={handleReorderQueue}
/>
}
recentPicksSection={
<SidebarRecentPicks picks={picks} />
}
/>
)}
{/* Main Tabbed Content */}
<div className="flex-1 overflow-hidden">
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as "participants" | "board" | "teams")
}
className="h-full flex flex-col"
>
<TabsList className="mx-4 mt-4">
<TabsTrigger value="participants">Available Participants</TabsTrigger>
<TabsTrigger value="board">Draft Board</TabsTrigger>
<TabsTrigger value="teams">Teams Drafted</TabsTrigger>
</TabsList>
<TabsContent value="participants" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<AvailableParticipantsSection
participants={filteredParticipants}
searchQuery={searchQuery}
sportFilter={sportFilter}
hideDrafted={hideDrafted}
uniqueSports={uniqueSports}
draftedParticipantIds={draftedParticipantIds}
queue={queue}
eligibility={eligibility}
canPick={canPick}
hasTeam={!!userTeam}
onSearchChange={setSearchQuery}
onSportFilterChange={setSportFilter}
onHideDraftedChange={setHideDrafted}
onMakePick={handleMakePick}
onAddToQueue={handleAddToQueue}
onRemoveFromQueue={handleRemoveFromQueue}
/>
</div>
</TabsContent>
<TabsContent value="board" className="flex-1 overflow-hidden m-0">
<DraftGridSection
draftSlots={draftSlots}
draftGrid={draftGrid}
currentPick={currentPick}
teamTimers={teamTimers}
autodraftStatus={autodraftStatus}
connectedTeams={connectedTeams}
isCommissioner={isCommissioner}
onForceAutopick={handleForceAutopick}
onForceManualPickOpen={(pickNumber, teamId) => {
setSelectedPickSlot({ pickNumber, teamId });
setForcePickDialogOpen(true);
}}
/>
</TabsContent>
<TabsContent value="teams" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<TeamsDraftedGrid
draftSlots={draftSlots}
picks={picks}
availableParticipants={availableParticipants}
season={{
id: season.id,
name: `${season.year} Season`,
year: season.year,
numFlexPicks: season.flexSpots,
}}
/>
</div>
</TabsContent>
</Tabs>
</div>
</div>
{/* Force Manual Pick Dialog */}
{forcePickDialogOpen && selectedPickSlot && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={() => setForcePickDialogOpen(false)}
>
<Card
className="w-full max-w-2xl max-h-[80vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<h2 className="text-2xl font-bold mb-4">Force Manual Pick</h2>
<p className="text-sm text-muted-foreground mb-4">
Select a participant to draft for Pick #
{selectedPickSlot.pickNumber}
</p>
{/* Search and Filter */}
<div className="flex gap-2 mb-4">
<input
type="text"
placeholder="Search participants..."
className="flex-1 px-3 py-2 border rounded-md"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<select
className="px-3 py-2 border rounded-md"
value={sportFilter}
onChange={(e) => setSportFilter(e.target.value)}
>
<option value="all">All Sports</option>
{uniqueSports.map((sport) => (
<option key={sport} value={sport}>
{sport}
</option>
))}
</select>
</div>
{/* Participant List */}
<div className="max-h-96 overflow-y-auto border rounded-lg">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
<th className="text-left p-3 font-semibold">
Participant
</th>
<th className="text-left p-3 font-semibold">Sport</th>
<th className="text-right p-3 font-semibold">EV</th>
<th className="text-right p-3 font-semibold">Action</th>
</tr>
</thead>
<tbody>
{filteredParticipants
.filter((p: any) => !draftedParticipantIds.has(p.id))
.map((participant: any) => {
// Check eligibility for force pick team
const isEligible = forcePickEligibility
? forcePickEligibility.eligibleSportIds.has(participant.sport.id)
: true;
const ineligibleReason = forcePickEligibility?.ineligibleReasons[participant.sport.id];
return (
<tr
key={participant.id}
className={`border-t ${
isEligible
? "hover:bg-muted/50"
: "bg-red-50/30 dark:bg-red-950/20 opacity-60"
}`}
title={!isEligible ? ineligibleReason : undefined}
>
<td className="p-3">
<div className="flex items-center gap-2">
<span className={`font-medium ${!isEligible ? "text-muted-foreground" : ""}`}>
{participant.name}
</span>
{!isEligible && (
<Badge
variant="destructive"
className="text-xs"
title={ineligibleReason}
>
Ineligible
</Badge>
)}
</div>
</td>
<td className="p-3 text-muted-foreground">
{participant.sport.name}
</td>
<td className="p-3 text-right font-mono font-semibold">
{participant.expectedValue}
</td>
<td className="p-3 text-right">
<Button
size="sm"
onClick={() =>
handleForceManualPick(participant.id)
}
disabled={!isEligible}
title={!isEligible ? ineligibleReason : undefined}
>
Draft
</Button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="mt-4 flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setForcePickDialogOpen(false)}
>
Cancel
</Button>
</div>
</div>
</Card>
</div>
)}
{/* Connection Overlay - blocks interaction until socket connects */}
<ConnectionOverlay
isConnected={isConnected}
isReconnecting={isReconnecting}
connectionError={connectionError}
/>
</div>
);
}