import { redirect, useLoaderData, useRevalidator, Link, useParams } from "react-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, ListOrdered, Users } from "lucide-react"; import { toast } from "sonner"; import { auth } from "~/lib/auth.server"; import { findSeasonWithTeamsAndLeague } from "~/models/season"; import { getDraftParticipants } from "~/models/season-participant"; import { getTeamQueue } from "~/models/draft-queue"; import { QueueSection } from "~/components/draft/QueueSection"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import type { Route } from "./+types/$leagueId.draft-queue.$seasonId"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Set Pre-Draft Queue — ${data?.season?.league?.name ?? "League"} - Brackt` }]; } export async function loader(args: Route.LoaderArgs) { const { params } = args; const { seasonId, leagueId } = params; const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; if (!seasonId) { throw new Response("Season ID is required", { status: 400 }); } const season = await findSeasonWithTeamsAndLeague(seasonId); if (!season) { throw new Response("Season not found", { status: 404 }); } // Redirect if draft has started — send them to the appropriate page if (season.status === "draft") { return redirect(`/leagues/${leagueId}/draft/${seasonId}`); } if (season.status === "active" || season.status === "completed") { return redirect(`/leagues/${leagueId}/draft-board/${seasonId}`); } if (!userId) { throw new Response("You must be logged in to set pre-draft queue", { status: 401 }); } const userTeam = season.teams.find((t) => t.ownerId === userId); if (!userTeam) { throw new Response("You do not have a team in this season", { status: 403 }); } const [availableParticipants, userQueue] = await Promise.all([ getDraftParticipants(seasonId), getTeamQueue(userTeam.id), ]); return { season, userTeam, availableParticipants, userQueue, }; } type QueueItem = { id: string; participantId: string }; export default function PreDraftRankings() { const { season, userTeam, availableParticipants, userQueue } = useLoaderData(); const { leagueId } = useParams<{ leagueId: string }>(); const { revalidate } = useRevalidator(); const [localQueue, setLocalQueue] = useState(userQueue); // Sync local state when server data refreshes useEffect(() => { setLocalQueue(userQueue); }, [userQueue]); // Always-current ref so reorder callback stays stable (no localQueue dependency) const queueRef = useRef(localQueue); // Track in-flight removes to prevent add racing ahead of a pending remove const pendingRemovesRef = useRef(new Set()); useEffect(() => { queueRef.current = localQueue; }, [localQueue]); const queuedParticipantIds = useMemo( () => new Set(localQueue.map((q) => q.participantId)), [localQueue] ); const handleAdd = useCallback( async (participantId: string) => { if (queuedParticipantIds.has(participantId) || pendingRemovesRef.current.has(participantId)) return; const tempId = `temp-${Date.now()}-${participantId}`; setLocalQueue((prev) => [...prev, { id: tempId, participantId }]); const formData = new FormData(); formData.append("seasonId", season.id); formData.append("teamId", userTeam.id); formData.append("participantId", participantId); try { const res = await fetch("/api/queue/add", { method: "POST", body: formData }); const data = await res.json(); if (data.error) { setLocalQueue((prev) => prev.filter((item) => item.id !== tempId)); toast.error(data.error || "Failed to add to rankings"); return; } revalidate(); // swap temp ID for the real server-assigned ID } catch { setLocalQueue((prev) => prev.filter((item) => item.id !== tempId)); toast.error("Network error — failed to add to rankings"); } }, [queuedParticipantIds, season.id, userTeam.id, revalidate] ); const handleRemove = useCallback( async (queueId: string) => { const item = queueRef.current.find((q) => q.id === queueId); const participantId = item?.participantId; setLocalQueue((prev) => prev.filter((q) => q.id !== queueId)); if (participantId) pendingRemovesRef.current.add(participantId); const formData = new FormData(); formData.append("queueId", queueId); formData.append("teamId", userTeam.id); try { const res = await fetch("/api/queue/remove", { method: "POST", body: formData }); const data = await res.json(); if (data.error) { toast.error(data.error || "Failed to remove from rankings"); revalidate(); } } catch { toast.error("Network error — failed to remove from rankings"); revalidate(); } finally { if (participantId) pendingRemovesRef.current.delete(participantId); } }, [userTeam.id, revalidate] ); // Remove by participantId — used from the All Players list const handleRemoveByParticipantId = useCallback( (participantId: string) => { const item = queueRef.current.find((q) => q.participantId === participantId); if (item) handleRemove(item.id); }, [handleRemove] ); const handleReorder = useCallback( async (participantIds: string[]) => { const previousQueue = queueRef.current; setLocalQueue((prev) => { const byParticipantId = new Map(prev.map((item) => [item.participantId, item])); return participantIds .map((pid) => byParticipantId.get(pid)) .filter((item): item is QueueItem => item !== undefined); }); const formData = new FormData(); formData.append("teamId", userTeam.id); formData.append("seasonId", season.id); formData.append("participantIds", JSON.stringify(participantIds)); try { const res = await fetch("/api/queue/reorder", { method: "POST", body: formData }); const data = await res.json(); if (data.error) { setLocalQueue(previousQueue); toast.error(data.error || "Failed to reorder rankings"); } } catch { setLocalQueue(previousQueue); toast.error("Network error — failed to reorder rankings"); } }, [userTeam.id, season.id] ); // ── Shared panel content ──────────────────────────────────────────────────── const allPlayersPanel = useMemo( () => (
{availableParticipants.map((p) => { const inQueue = queuedParticipantIds.has(p.id); return (

{p.name}

{p.sport.name}

{inQueue ? ( ) : ( )}
); })} {availableParticipants.length === 0 && (

No participants have been added to this season yet.

)}
), [availableParticipants, queuedParticipantIds, handleAdd, handleRemoveByParticipantId] ); const rankingsPanel = useMemo( () => ( <>
{localQueue.length > 0 && (

Drag to reorder · Autopick follows this order

)} ), [localQueue, availableParticipants, handleRemove, handleReorder] ); // ── Render ────────────────────────────────────────────────────────────────── return (
{/* Header */}
Back to {season.league.name}

Set Pre-Draft Queue

Rank the players you want. Your rankings carry into the live draft as your autopick priority order.

{/* Mobile: tabs */}
All Players My Rankings {localQueue.length > 0 && ( {localQueue.length} )} {allPlayersPanel} {rankingsPanel}
{/* Desktop: side-by-side */}

All Players

{allPlayersPanel}

My Rankings

{localQueue.length}
{rankingsPanel}
); }