brackt/app/components/draft/QueueSection.tsx
chrisp 841760541d
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m16s
🚀 Deploy / 🔍 Lint (push) Successful in 1m55s
🚀 Deploy / 🐳 Build (push) Successful in 14m23s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Fix snake draft Discord pick numbering and small UX tweaks (#2)
## Summary

- **Discord snake draft fix**: pick notifications now show the sequential pick-in-round (e.g. "Round 2, Pick 9") rather than the snake-adjusted slot position ("Round 2, Pick 5"). Also removes the now-unused \`pickInRound\` param from \`notifyPickMadeOnDiscord\` and adds a regression test for the 13-team case.
- **League card**: league name in draft-in-progress cards is now a link to the league homepage (the Enter Draft button still goes to the draft room).
- **Overnight pause label**: shortened to "🌙 Pause" — the "Resumes 4:00 AM" line below already provides context, so "Overnight" was just causing wrapping.
- **Queue & Picks backgrounds**: items use \`bg-card\` instead of \`bg-muted\` for better visual separation from the panel background.

## Test plan

- [x] Discord: in a snake draft, verify pick #22 of 13 shows "Round 2, Pick 9" in Discord
- [x] League card: dashboard with a draft-in-progress league — click name → league homepage, click Enter Draft → draft room
- [x] Overnight pause: pause a draft overnight — cell shows "🌙 Pause" on one line, "Resumes X:XX" below
- [x] Queue/Picks: open draft room and confirm queue items and recent picks visually pop from the sidebar background
- [x] Unit tests: `npm run test:run` passes (13 discord tests)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #2
2026-05-22 04:18:11 +00:00

199 lines
5.4 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 { memo, useCallback, useMemo } from "react";
import { GripVertical } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
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 };
}>;
canPick: boolean;
onRemoveFromQueue: (queueId: string) => void;
onReorder: (participantIds: string[]) => void;
onMakePick?: (participantId: string) => void;
emptyMessage?: string;
}
// Sortable queue item component
const SortableQueueItem = memo(function SortableQueueItem({
item,
index,
participantName,
sportName,
canPick,
onRemove,
onDraft,
}: {
item: { id: string; participantId: string };
index: number;
participantName: string;
sportName?: string;
canPick: boolean;
onRemove: (queueId: string) => void;
onDraft?: (participantId: string) => void;
}) {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
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}
{...attributes}
className={`p-2 rounded-lg ${
canPick ? "bg-electric/10 border border-electric/40" : "bg-white/[0.06]"
}`}
>
<div className="flex items-center gap-2">
<div
ref={setActivatorNodeRef}
{...listeners}
className="w-10 flex items-center gap-2 flex-shrink-0 cursor-grab active:cursor-grabbing touch-none"
>
<GripVertical className="w-4 h-4 text-muted-foreground" />
<Badge variant="default" className="text-xs">{index + 1}</Badge>
</div>
<div className="min-w-0 flex-1">
<p className="font-semibold text-sm truncate">{participantName}</p>
{sportName && <p className="text-xs text-muted-foreground">{sportName}</p>}
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 flex-shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => onRemove(item.id)}
title="Remove from queue"
>
<span className="text-lg">×</span>
</Button>
</div>
{canPick && onDraft && (
<div className="mt-1.5 pl-10">
<Button
variant="default"
size="sm"
className="h-7 text-xs w-full bg-electric text-background hover:bg-electric/90"
onClick={() => onDraft(item.participantId)}
>
Draft
</Button>
</div>
)}
</div>
);
});
export const QueueSection = memo(function QueueSection({
queue,
availableParticipants,
canPick,
onRemoveFromQueue,
onReorder,
onMakePick,
emptyMessage = "Click participants in Available to add to your queue",
}: QueueSectionProps) {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const participantMap = useMemo(
() => new Map(availableParticipants.map((p) => [p.id, p])),
[availableParticipants]
);
const queueIds = useMemo(() => queue.map((item) => item.id), [queue]);
const handleDragEnd = useCallback(
(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);
onReorder(reorderedQueue.map((item) => item.participantId));
}
},
[queue, onReorder]
);
return (
<div className="p-4">
{/* Queue List */}
{queue.length === 0 ? (
<p className="text-muted-foreground text-sm text-center py-8">
{emptyMessage}
</p>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={queueIds}
strategy={verticalListSortingStrategy}
>
<div className="space-y-1.5">
{queue.map((item, index) => {
const participant = participantMap.get(item.participantId);
return (
<SortableQueueItem
key={item.id}
item={item}
index={index}
participantName={participant?.name ?? "Unknown"}
sportName={participant?.sport.name}
canPick={canPick}
onRemove={onRemoveFromQueue}
onDraft={onMakePick}
/>
);
})}
</div>
</SortableContext>
</DndContext>
)}
</div>
);
});