brackt/app/components/draft/QueueSection.tsx
Chris Parsons 1ba50828f7
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint

Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field

Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs

Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock

Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons

Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states

https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB

* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests

- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
  (line 488) — was dead code since the column is NOT NULL, but semantically wrong
  and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
  constraint: empty queue, all items drafted, partial queue skip, and EV fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add missing queueOnly prop to AutodraftSettings test fixtures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: rewrite AutodraftSettings tests for three-state button group UI

The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00

222 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { AutodraftSettings } from "~/components/AutodraftSettings";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import type { DragEndEvent } from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface QueueSectionProps {
queue: Array<{
id: string;
participantId: string;
}>;
availableParticipants: Array<{
id: string;
name: string;
sport: { name: string };
}>;
seasonId: string;
teamId: string;
isMyTurn: boolean;
canPick: boolean;
userAutodraft: {
isEnabled: boolean;
mode: "next_pick" | "while_on";
queueOnly: boolean;
};
onRemoveFromQueue: (queueId: string) => void;
onAutodraftUpdate: (isEnabled: boolean, mode: "next_pick" | "while_on", queueOnly: boolean) => void;
onReorder: (participantIds: string[]) => void;
onMakePick?: (participantId: string) => void;
}
// Sortable queue item component
function SortableQueueItem({
item,
index,
participantName,
sportName,
canPick,
onRemove,
onDraft,
}: {
item: { id: string; participantId: string };
index: number;
participantName: string;
sportName?: string;
canPick: boolean;
onRemove: () => void;
onDraft?: () => void;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
className={`flex items-center justify-between p-2 rounded-lg touch-none ${
canPick ? "bg-electric/10 border border-electric/40" : "bg-muted"
}`}
>
<div className="flex items-center gap-2 flex-1 min-w-0" {...attributes} {...listeners}>
<div className="cursor-grab active:cursor-grabbing flex-shrink-0">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground"
>
<line x1="5" y1="9" x2="19" y2="9"></line>
<line x1="5" y1="15" x2="19" y2="15"></line>
</svg>
</div>
<Badge variant="default" className="text-xs flex-shrink-0">{index + 1}</Badge>
<div className="min-w-0">
<p className="font-semibold text-sm truncate">{participantName}</p>
{sportName && <p className="text-xs text-muted-foreground">{sportName}</p>}
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-2">
{canPick && onDraft && (
<Button
variant="default"
size="sm"
className="h-7 text-xs bg-electric text-background hover:bg-electric/90"
onClick={onDraft}
>
Draft
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={onRemove}
title="Remove from queue"
>
<span className="text-lg">×</span>
</Button>
</div>
</div>
);
}
export function QueueSection({
queue,
availableParticipants,
seasonId,
teamId,
isMyTurn,
canPick,
userAutodraft,
onRemoveFromQueue,
onAutodraftUpdate,
onReorder,
onMakePick,
}: QueueSectionProps) {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = queue.findIndex((item) => item.id === active.id);
const newIndex = queue.findIndex((item) => item.id === over.id);
const reorderedQueue = arrayMove(queue, oldIndex, newIndex);
const participantIds = reorderedQueue.map((item) => item.participantId);
onReorder(participantIds);
}
};
return (
<div className="p-4">
{/* Queue List */}
{queue.length === 0 ? (
<p className="text-muted-foreground text-sm text-center py-8">
Click participants in Available to add to your queue
</p>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={queue.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-1.5 mb-4">
{queue.map((item, index) => {
const participant = availableParticipants.find(
(p) => p.id === item.participantId
);
return (
<SortableQueueItem
key={item.id}
item={item}
index={index}
participantName={participant?.name || "Unknown"}
sportName={participant?.sport.name}
canPick={canPick}
onRemove={() => onRemoveFromQueue(item.id)}
onDraft={onMakePick ? () => onMakePick(item.participantId) : undefined}
/>
);
})}
</div>
</SortableContext>
</DndContext>
)}
{/* Autodraft Settings */}
<AutodraftSettings
seasonId={seasonId}
teamId={teamId}
isEnabled={userAutodraft.isEnabled}
mode={userAutodraft.mode}
queueOnly={userAutodraft.queueOnly}
isMyTurn={isMyTurn}
onUpdate={onAutodraftUpdate}
/>
</div>
);
}