diff --git a/app/components/league/DraftInfoCard.tsx b/app/components/league/DraftInfoCard.tsx
index 186f740..f22f1c9 100644
--- a/app/components/league/DraftInfoCard.tsx
+++ b/app/components/league/DraftInfoCard.tsx
@@ -21,6 +21,8 @@ export interface DraftInfoCardProps {
draftDateTime?: string | Date | null;
draftBoardHref: string;
draftRoomHref: string;
+ /** Link to the pre-draft queue builder; shown only when draft order is not yet set and user has a team */
+ queueBuilderHref?: string;
/** 1-based draft position for the current user; omit or undefined to hide the panel */
userDraftPosition?: number;
/** IANA timezone used to display the draft date and time. */
@@ -57,6 +59,7 @@ function DraftInfoCardVariant({
draftDateTime,
draftTimezone,
draftRoomHref,
+ queueBuilderHref,
userDraftPosition,
overnightPauseMode,
overnightPauseStart,
@@ -112,11 +115,15 @@ function DraftInfoCardVariant({
Draft Info
- {isDraftOrderSet && (
+ {isDraftOrderSet ? (
- )}
+ ) : queueBuilderHref ? (
+
+ ) : null}
diff --git a/app/routes.ts b/app/routes.ts
index 9577079..2d055f1 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -20,6 +20,10 @@ export default [
"leagues/:leagueId/draft/:seasonId",
"routes/leagues/$leagueId.draft.$seasonId.tsx"
),
+ route(
+ "leagues/:leagueId/draft-queue/:seasonId",
+ "routes/leagues/$leagueId.draft-queue.$seasonId.tsx"
+ ),
route(
"leagues/:leagueId/draft-board/:seasonId",
"routes/leagues/$leagueId.draft-board.$seasonId.tsx"
diff --git a/app/routes/leagues/$leagueId.draft-queue.$seasonId.tsx b/app/routes/leagues/$leagueId.draft-queue.$seasonId.tsx
new file mode 100644
index 0000000..38a97e6
--- /dev/null
+++ b/app/routes/leagues/$leagueId.draft-queue.$seasonId.tsx
@@ -0,0 +1,227 @@
+import { redirect, useFetcher, useLoaderData, useRevalidator, Link } from "react-router";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { ArrowLeft, ListOrdered } from "lucide-react";
+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 type { Route } from "./+types/$leagueId.draft-queue.$seasonId";
+
+export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
+ return [{ title: `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 build a 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,
+ leagueId,
+ userTeam,
+ availableParticipants,
+ userQueue,
+ };
+}
+
+type QueueItem = { id: string; participantId: string };
+
+export default function PreDraftQueue() {
+ const { season, leagueId, userTeam, availableParticipants, userQueue } =
+ useLoaderData();
+ const { revalidate } = useRevalidator();
+
+ const [localQueue, setLocalQueue] = useState(userQueue);
+
+ // Sync local state when server data refreshes
+ useEffect(() => {
+ setLocalQueue(userQueue);
+ }, [userQueue]);
+
+ const addFetcher = useFetcher<{ success: boolean }>();
+ const removeFetcher = useFetcher();
+ const reorderFetcher = useFetcher();
+
+ // After add completes, revalidate to replace the optimistic temp ID with the real one
+ useEffect(() => {
+ if (addFetcher.state === "idle" && addFetcher.data?.success) {
+ revalidate();
+ }
+ }, [addFetcher.state, addFetcher.data, revalidate]);
+
+ const queuedParticipantIds = useMemo(
+ () => new Set(localQueue.map((q) => q.participantId)),
+ [localQueue]
+ );
+
+ const handleAdd = useCallback(
+ (participantId: string) => {
+ if (queuedParticipantIds.has(participantId)) return;
+ // Optimistic update with a temporary ID
+ 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);
+ addFetcher.submit(formData, { method: "POST", action: "/api/queue/add" });
+ },
+ [queuedParticipantIds, season.id, userTeam.id, addFetcher]
+ );
+
+ const handleRemove = useCallback(
+ (queueId: string) => {
+ setLocalQueue((prev) => prev.filter((item) => item.id !== queueId));
+ const formData = new FormData();
+ formData.append("queueId", queueId);
+ formData.append("teamId", userTeam.id);
+ removeFetcher.submit(formData, { method: "POST", action: "/api/queue/remove" });
+ },
+ [userTeam.id, removeFetcher]
+ );
+
+ const handleReorder = useCallback(
+ (participantIds: string[]) => {
+ setLocalQueue((prev) => {
+ const byParticipantId = new Map(prev.map((item) => [item.participantId, item]));
+ return participantIds
+ .map((pid) => byParticipantId.get(pid))
+ .filter((item): item is (typeof prev)[number] => item !== undefined);
+ });
+ const formData = new FormData();
+ formData.append("teamId", userTeam.id);
+ formData.append("seasonId", season.id);
+ formData.append("participantIds", JSON.stringify(participantIds));
+ reorderFetcher.submit(formData, { method: "POST", action: "/api/queue/reorder" });
+ },
+ [userTeam.id, season.id, reorderFetcher]
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ Back to {season.league.name}
+
+
+
+
Pre-Draft Queue
+
+
+ Rank the participants you want. Your queue will carry into the live draft and determine
+ your autopick order.
+