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
This commit is contained in:
Claude 2026-05-15 23:45:34 +00:00
parent 3d9f767a8c
commit fb392c4fdd
No known key found for this signature in database
2 changed files with 123 additions and 65 deletions

View file

@ -121,7 +121,7 @@ function DraftInfoCardVariant({
</Button>
) : queueBuilderHref ? (
<Button asChild variant="outline">
<Link to={queueBuilderHref}>Build Your Queue</Link>
<Link to={queueBuilderHref}>Set Pre-Draft Rankings</Link>
</Button>
) : null}
</div>

View file

@ -1,6 +1,6 @@
import { redirect, useFetcher, useLoaderData, useRevalidator, Link } from "react-router";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ArrowLeft, ListOrdered } from "lucide-react";
import { ArrowLeft, ListOrdered, Users } from "lucide-react";
import { auth } from "~/lib/auth.server";
import { findSeasonWithTeamsAndLeague } from "~/models/season";
import { getDraftParticipants } from "~/models/season-participant";
@ -8,10 +8,11 @@ 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: `Pre-Draft Queue${data?.season?.league?.name ?? "League"} - Brackt` }];
return [{ title: `Pre-Draft Rankings${data?.season?.league?.name ?? "League"} - Brackt` }];
}
export async function loader(args: Route.LoaderArgs) {
@ -39,7 +40,7 @@ export async function loader(args: Route.LoaderArgs) {
}
if (!userId) {
throw new Response("You must be logged in to build a queue", { status: 401 });
throw new Response("You must be logged in to set pre-draft rankings", { status: 401 });
}
const userTeam = season.teams.find((t) => t.ownerId === userId);
@ -63,7 +64,7 @@ export async function loader(args: Route.LoaderArgs) {
type QueueItem = { id: string; participantId: string };
export default function PreDraftQueue() {
export default function PreDraftRankings() {
const { season, leagueId, userTeam, availableParticipants, userQueue } =
useLoaderData<typeof loader>();
const { revalidate } = useRevalidator();
@ -94,7 +95,6 @@ export default function PreDraftQueue() {
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();
@ -117,6 +117,15 @@ export default function PreDraftQueue() {
[userTeam.id, removeFetcher]
);
// Remove by participantId — used from the All Players list
const handleRemoveByParticipantId = useCallback(
(participantId: string) => {
const item = localQueue.find((q) => q.participantId === participantId);
if (item) handleRemove(item.id);
},
[localQueue, handleRemove]
);
const handleReorder = useCallback(
(participantIds: string[]) => {
setLocalQueue((prev) => {
@ -134,6 +143,77 @@ export default function PreDraftQueue() {
[userTeam.id, season.id, reorderFetcher]
);
// ── Shared panel content ────────────────────────────────────────────────────
const allPlayersPanel = (
<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>
{p.vorpValue != null && (
<span className="text-xs text-muted-foreground tabular-nums shrink-0">
{Number(p.vorpValue).toFixed(1)}
</span>
)}
{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>
);
const rankingsPanel = (
<>
<div className="rounded-lg border bg-card">
<QueueSection
queue={localQueue}
availableParticipants={availableParticipants}
canPick={false}
onRemoveFromQueue={handleRemove}
onReorder={handleReorder}
/>
</div>
{localQueue.length > 0 && (
<p className="text-xs text-muted-foreground mt-2 text-center">
Drag to reorder · Autopick follows this order
</p>
)}
</>
);
// ── Render ──────────────────────────────────────────────────────────────────
return (
<div className="container mx-auto py-8 px-4">
{/* Header */}
@ -147,79 +227,57 @@ export default function PreDraftQueue() {
</Link>
<div className="flex items-center gap-2 mb-1">
<ListOrdered className="h-6 w-6" />
<h1 className="text-2xl font-bold">Pre-Draft Queue</h1>
<h1 className="text-2xl font-bold">Pre-Draft Rankings</h1>
</div>
<p className="text-sm text-muted-foreground">
Rank the participants you want. Your queue will carry into the live draft and determine
your autopick order.
Rank the players you want. Your rankings carry into the live draft as your autopick
priority order.
</p>
</div>
{/* Two-panel layout */}
<div className="grid gap-6 lg:grid-cols-3">
{/* Participant list */}
{/* 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">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-muted-foreground">Sorted by VORP</span>
</div>
{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="flex items-center justify-between mb-3">
<h2 className="font-semibold">All Participants</h2>
<h2 className="font-semibold">All Players</h2>
<span className="text-xs text-muted-foreground">Sorted by VORP</span>
</div>
<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>
{p.vorpValue != null && (
<span className="text-xs text-muted-foreground tabular-nums shrink-0">
{Number(p.vorpValue).toFixed(1)}
</span>
)}
<Button
size="sm"
variant={inQueue ? "secondary" : "outline"}
disabled={inQueue}
onClick={() => handleAdd(p.id)}
className="h-7 text-xs shrink-0"
>
{inQueue ? "Queued" : "+ 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>
{allPlayersPanel}
</div>
{/* Queue panel */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold">Your Queue</h2>
<h2 className="font-semibold">My Rankings</h2>
<Badge variant="secondary">{localQueue.length}</Badge>
</div>
<div className="rounded-lg border bg-card">
<QueueSection
queue={localQueue}
availableParticipants={availableParticipants}
canPick={false}
onRemoveFromQueue={handleRemove}
onReorder={handleReorder}
/>
</div>
{localQueue.length > 0 && (
<p className="text-xs text-muted-foreground mt-2 text-center">
Drag to reorder · Autopick follows this order
</p>
)}
{rankingsPanel}
</div>
</div>
</div>