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

336 lines
12 KiB
TypeScript
Raw Permalink Normal View History

Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
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` }];
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
}
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 });
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
}
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 SetPreDraftQueue() {
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
const { season, userTeam, availableParticipants, userQueue } =
useLoaderData<typeof loader>();
const { leagueId } = useParams<{ leagueId: string }>();
const { revalidate } = useRevalidator();
const [localQueue, setLocalQueue] = useState<QueueItem[]>(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<string>());
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
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)) return;
if (pendingRemovesRef.current.has(participantId)) {
toast("Player is being removed — try again in a moment");
return;
}
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
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));
// Temp IDs haven't been written to the DB yet — nothing to delete server-side.
if (queueId.startsWith("temp-")) return;
if (participantId) pendingRemovesRef.current.add(participantId);
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
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);
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
}
},
[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(
() => (
<div className="space-y-1">
{availableParticipants.map((p) => {
const inQueue = queuedParticipantIds.has(p.id);
return (
<div
key={p.id}
className="flex items-center gap-3 rounded-lg bg-muted/50 px-3 py-2"
>
<div className="min-w-0 flex-1">
<p className="font-medium text-sm truncate">{p.name}</p>
<p className="text-xs text-muted-foreground">{p.sport.name}</p>
</div>
{inQueue ? (
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoveByParticipantId(p.id)}
className="h-7 text-xs shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10"
>
Remove
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => handleAdd(p.id)}
className="h-7 text-xs shrink-0"
>
+ Add
</Button>
)}
</div>
);
})}
{availableParticipants.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-12">
No participants have been added to this season yet.
</p>
)}
</div>
),
[availableParticipants, queuedParticipantIds, handleAdd, handleRemoveByParticipantId]
);
const rankingsPanel = useMemo(
() => (
<>
<div className="rounded-lg border bg-card">
<QueueSection
queue={localQueue}
availableParticipants={availableParticipants}
canPick={false}
onRemoveFromQueue={handleRemove}
onReorder={handleReorder}
emptyMessage="Add players from All Players to set your ranking order."
/>
</div>
{localQueue.length > 0 && (
<p className="text-xs text-muted-foreground mt-2 text-center">
Drag to reorder · Autopick follows this order
</p>
)}
</>
),
[localQueue, availableParticipants, handleRemove, handleReorder]
);
// ── Render ──────────────────────────────────────────────────────────────────
return (
<div className="container mx-auto py-8 px-4">
{/* Header */}
<div className="mb-6">
<Link
to={`/leagues/${leagueId}`}
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-3"
>
<ArrowLeft className="h-4 w-4" />
Back to {season.league.name}
</Link>
<div className="flex items-center gap-2 mb-1">
<ListOrdered className="h-6 w-6" />
<h1 className="text-2xl font-bold">Set Pre-Draft Queue</h1>
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
</div>
<p className="text-sm text-muted-foreground">
Rank the players you want. Your rankings carry into the live draft as your autopick
priority order.
</p>
</div>
{/* Mobile: tabs */}
<div className="lg:hidden">
<Tabs defaultValue="players">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="players" className="flex items-center gap-1.5 text-xs">
<Users className="h-4 w-4" />
All Players
</TabsTrigger>
<TabsTrigger value="rankings" className="flex items-center gap-1.5 text-xs">
<ListOrdered className="h-4 w-4" />
My Rankings
{localQueue.length > 0 && (
<Badge variant="secondary" className="ml-1 h-4 px-1 text-[10px]">
{localQueue.length}
</Badge>
)}
</TabsTrigger>
</TabsList>
<TabsContent value="players">
{allPlayersPanel}
</TabsContent>
<TabsContent value="rankings">{rankingsPanel}</TabsContent>
</Tabs>
</div>
{/* Desktop: side-by-side */}
<div className="hidden lg:grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<div className="mb-3">
Add pre-draft queue builder so users can rank players before draft order is set (#435) * Add pre-draft queue builder so users can rank players before draft order is set Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team owners browse participants by VORP and build their autopick queue during the pre_draft phase. The queue uses the existing draftQueue table so it carries seamlessly into the live draft room. Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft order has not been set yet (replaced by "Enter Draft Room" once it is). https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked layout that buries the queue below a long participant list. On the All Players list, the button now toggles between Add and Remove so users never need to switch tabs just to drop someone. Desktop keeps the side-by-side panel layout. Also renames the feature throughout from "queue builder" to "pre-draft rankings" and the DraftInfoCard button to "Set Pre-Draft Rankings". https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix all code review issues in pre-draft rankings feature - Replace useFetcher with direct fetch() for add/remove/reorder so rapid clicks no longer cancel in-flight requests - Add toast.error() on all failure paths (matching live draft room pattern) and revert optimistic state when operations fail - Add useRef to give handleReorder a stable reference without a localQueue closure dependency, preventing unnecessary QueueSection re-renders - Convert allPlayersPanel and rankingsPanel to useMemo - Remove leagueId from loader return; read from useParams() instead - Extract queueBuilderHref to a variable in the league home component - Add emptyMessage prop to QueueSection with a correct message for the pre-draft context ("Add players from All Players...") - Add draft-queue-access.test.ts covering all loader access control paths: 401/403 errors, status-based redirects, and redirect URL correctness https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG * Fix lint: use !== null check instead of != null oxlint enforces eqeqeq; replace != null with !== null && !== undefined. https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 17:26:36 -07:00
<h2 className="font-semibold">All Players</h2>
</div>
{allPlayersPanel}
</div>
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold">My Rankings</h2>
<Badge variant="secondary">{localQueue.length}</Badge>
</div>
{rankingsPanel}
</div>
</div>
</div>
);
}