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:
parent
3d9f767a8c
commit
fb392c4fdd
2 changed files with 123 additions and 65 deletions
|
|
@ -121,7 +121,7 @@ function DraftInfoCardVariant({
|
||||||
</Button>
|
</Button>
|
||||||
) : queueBuilderHref ? (
|
) : queueBuilderHref ? (
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link to={queueBuilderHref}>Build Your Queue</Link>
|
<Link to={queueBuilderHref}>Set Pre-Draft Rankings</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { redirect, useFetcher, useLoaderData, useRevalidator, Link } from "react-router";
|
import { redirect, useFetcher, useLoaderData, useRevalidator, Link } from "react-router";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
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 { auth } from "~/lib/auth.server";
|
||||||
import { findSeasonWithTeamsAndLeague } from "~/models/season";
|
import { findSeasonWithTeamsAndLeague } from "~/models/season";
|
||||||
import { getDraftParticipants } from "~/models/season-participant";
|
import { getDraftParticipants } from "~/models/season-participant";
|
||||||
|
|
@ -8,10 +8,11 @@ import { getTeamQueue } from "~/models/draft-queue";
|
||||||
import { QueueSection } from "~/components/draft/QueueSection";
|
import { QueueSection } from "~/components/draft/QueueSection";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
import type { Route } from "./+types/$leagueId.draft-queue.$seasonId";
|
import type { Route } from "./+types/$leagueId.draft-queue.$seasonId";
|
||||||
|
|
||||||
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
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) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
|
|
@ -39,7 +40,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
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);
|
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 };
|
type QueueItem = { id: string; participantId: string };
|
||||||
|
|
||||||
export default function PreDraftQueue() {
|
export default function PreDraftRankings() {
|
||||||
const { season, leagueId, userTeam, availableParticipants, userQueue } =
|
const { season, leagueId, userTeam, availableParticipants, userQueue } =
|
||||||
useLoaderData<typeof loader>();
|
useLoaderData<typeof loader>();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
@ -94,7 +95,6 @@ export default function PreDraftQueue() {
|
||||||
const handleAdd = useCallback(
|
const handleAdd = useCallback(
|
||||||
(participantId: string) => {
|
(participantId: string) => {
|
||||||
if (queuedParticipantIds.has(participantId)) return;
|
if (queuedParticipantIds.has(participantId)) return;
|
||||||
// Optimistic update with a temporary ID
|
|
||||||
const tempId = `temp-${Date.now()}-${participantId}`;
|
const tempId = `temp-${Date.now()}-${participantId}`;
|
||||||
setLocalQueue((prev) => [...prev, { id: tempId, participantId }]);
|
setLocalQueue((prev) => [...prev, { id: tempId, participantId }]);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
@ -117,6 +117,15 @@ export default function PreDraftQueue() {
|
||||||
[userTeam.id, removeFetcher]
|
[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(
|
const handleReorder = useCallback(
|
||||||
(participantIds: string[]) => {
|
(participantIds: string[]) => {
|
||||||
setLocalQueue((prev) => {
|
setLocalQueue((prev) => {
|
||||||
|
|
@ -134,35 +143,9 @@ export default function PreDraftQueue() {
|
||||||
[userTeam.id, season.id, reorderFetcher]
|
[userTeam.id, season.id, reorderFetcher]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
// ── Shared panel content ────────────────────────────────────────────────────
|
||||||
<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">Pre-Draft Queue</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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Two-panel layout */}
|
const allPlayersPanel = (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
|
||||||
{/* Participant list */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h2 className="font-semibold">All Participants</h2>
|
|
||||||
<span className="text-xs text-muted-foreground">Sorted by VORP</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{availableParticipants.map((p) => {
|
{availableParticipants.map((p) => {
|
||||||
const inQueue = queuedParticipantIds.has(p.id);
|
const inQueue = queuedParticipantIds.has(p.id);
|
||||||
|
|
@ -180,15 +163,25 @@ export default function PreDraftQueue() {
|
||||||
{Number(p.vorpValue).toFixed(1)}
|
{Number(p.vorpValue).toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{inQueue ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={inQueue ? "secondary" : "outline"}
|
variant="ghost"
|
||||||
disabled={inQueue}
|
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)}
|
onClick={() => handleAdd(p.id)}
|
||||||
className="h-7 text-xs shrink-0"
|
className="h-7 text-xs shrink-0"
|
||||||
>
|
>
|
||||||
{inQueue ? "Queued" : "+ Add"}
|
+ Add
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -198,14 +191,10 @@ export default function PreDraftQueue() {
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
{/* Queue panel */}
|
const rankingsPanel = (
|
||||||
<div>
|
<>
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h2 className="font-semibold">Your Queue</h2>
|
|
||||||
<Badge variant="secondary">{localQueue.length}</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border bg-card">
|
<div className="rounded-lg border bg-card">
|
||||||
<QueueSection
|
<QueueSection
|
||||||
queue={localQueue}
|
queue={localQueue}
|
||||||
|
|
@ -220,6 +209,75 @@ export default function PreDraftQueue() {
|
||||||
Drag to reorder · Autopick follows this order
|
Drag to reorder · Autopick follows this order
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 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">Pre-Draft Rankings</h1>
|
||||||
|
</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">
|
||||||
|
<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 Players</h2>
|
||||||
|
<span className="text-xs text-muted-foreground">Sorted by VORP</span>
|
||||||
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue