Show visual dividers in the available participants list indicating where your future draft picks fall relative to the current pick position. Dividers are suppressed when any filter is active (search, sport filter, hide drafted, etc.) since positional references are meaningless in a filtered view. - Add getProjectedPicks() to draft-order.ts for computing future pick positions in a snake draft - Interleave divider items into the virtualized list in AvailableParticipantsSection - Extract ListItem type to module scope - Add tests for getProjectedPicks, divider rendering, and filter suppression
This commit is contained in:
parent
01dacbce27
commit
3566a97a1e
5 changed files with 494 additions and 9 deletions
|
|
@ -9,9 +9,9 @@ vi.mock("@tanstack/react-virtual", () => ({
|
||||||
estimateSize,
|
estimateSize,
|
||||||
}: {
|
}: {
|
||||||
count: number;
|
count: number;
|
||||||
estimateSize: () => number;
|
estimateSize: (index: number) => number;
|
||||||
}) => {
|
}) => {
|
||||||
const size = estimateSize();
|
const size = estimateSize(0);
|
||||||
const items = Array.from({ length: count }, (_, i) => ({
|
const items = Array.from({ length: count }, (_, i) => ({
|
||||||
index: i,
|
index: i,
|
||||||
start: i * size,
|
start: i * size,
|
||||||
|
|
@ -171,6 +171,154 @@ describe("AvailableParticipantsSection", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Projected Pick Dividers", () => {
|
||||||
|
const manyParticipants = Array.from({ length: 30 }, (_, i) => ({
|
||||||
|
id: String(i + 1),
|
||||||
|
name: `Player ${i + 1}`,
|
||||||
|
sport: { id: "s1", name: "NFL" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const manyRanks = new Map(
|
||||||
|
Array.from({ length: 30 }, (_, i) => [
|
||||||
|
String(i + 1),
|
||||||
|
{ overallRank: i + 1, sportRank: i + 1 },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
it("renders projected pick divider at correct position", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
projectedPicks={[
|
||||||
|
{ round: 3, picksFromNow: 10 },
|
||||||
|
{ round: 4, picksFromNow: 20 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Projected Round 4 Pick")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render dividers when projectedPicks is undefined", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render dividers when projectedPicks is empty", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
projectedPicks={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips dividers where picksFromNow exceeds list length", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
projectedPicks={[
|
||||||
|
{ round: 3, picksFromNow: 10 },
|
||||||
|
{ round: 10, picksFromNow: 100 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Projected Round 10 Pick")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips dividers where picksFromNow is zero or negative", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
projectedPicks={[
|
||||||
|
{ round: 1, picksFromNow: 0 },
|
||||||
|
{ round: 2, picksFromNow: 5 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText("Projected Round 1 Pick")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Projected Round 2 Pick")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still renders all participants alongside dividers", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getAllByText("Player 1").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Player 10").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Player 30").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses dividers when search query is active", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
searchQuery="Player"
|
||||||
|
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses dividers when sport filter is active", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
sportFilters={["NFL"]}
|
||||||
|
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses dividers when hideDrafted is active", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
participants={manyParticipants}
|
||||||
|
participantRanks={manyRanks}
|
||||||
|
hideDrafted
|
||||||
|
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Sport Filter - sheet/popover", () => {
|
describe("Sport Filter - sheet/popover", () => {
|
||||||
it("opens and shows checkboxes for each sport", async () => {
|
it("opens and shows checkboxes for each sport", async () => {
|
||||||
render(<AvailableParticipantsSection {...defaultProps} />);
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ import {
|
||||||
} from "~/components/ui/sheet";
|
} from "~/components/ui/sheet";
|
||||||
import { ListPlus, ListX, ChevronDown } from "lucide-react";
|
import { ListPlus, ListX, ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
type ListItem =
|
||||||
|
| { type: "participant"; index: number; key: string }
|
||||||
|
| { type: "divider"; round: number; key: string };
|
||||||
|
|
||||||
function getParticipantState(
|
function getParticipantState(
|
||||||
participant: { id: string; sport: { id: string } },
|
participant: { id: string; sport: { id: string } },
|
||||||
draftedParticipantIds: Set<string>,
|
draftedParticipantIds: Set<string>,
|
||||||
|
|
@ -157,6 +161,7 @@ interface AvailableParticipantsSectionProps {
|
||||||
onMakePick: (participantId: string) => void;
|
onMakePick: (participantId: string) => void;
|
||||||
onAddToQueue: (participantId: string) => void;
|
onAddToQueue: (participantId: string) => void;
|
||||||
onRemoveFromQueue: (queueId: string) => void;
|
onRemoveFromQueue: (queueId: string) => void;
|
||||||
|
projectedPicks?: Array<{ round: number; picksFromNow: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AvailableParticipantsSection = memo(function AvailableParticipantsSection({
|
export const AvailableParticipantsSection = memo(function AvailableParticipantsSection({
|
||||||
|
|
@ -183,6 +188,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
onMakePick,
|
onMakePick,
|
||||||
onAddToQueue,
|
onAddToQueue,
|
||||||
onRemoveFromQueue,
|
onRemoveFromQueue,
|
||||||
|
projectedPicks,
|
||||||
}: AvailableParticipantsSectionProps) {
|
}: AvailableParticipantsSectionProps) {
|
||||||
const queueMap = useMemo(
|
const queueMap = useMemo(
|
||||||
() => new Map(queue.map((item) => [item.participantId, item.id])),
|
() => new Map(queue.map((item) => [item.participantId, item.id])),
|
||||||
|
|
@ -247,13 +253,41 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
|
|
||||||
const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports;
|
const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports;
|
||||||
|
|
||||||
|
const anyFilterActive = searchQuery !== "" || sportFilters.length > 0 || hideDrafted || hideIneligible || hideCompletedSports;
|
||||||
|
|
||||||
|
const listItems = useMemo(() => {
|
||||||
|
const items: ListItem[] = [];
|
||||||
|
if (!projectedPicks || projectedPicks.length === 0 || anyFilterActive) {
|
||||||
|
for (let i = 0; i < participants.length; i++) {
|
||||||
|
items.push({ type: "participant", index: i, key: participants[i].id });
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
const dividersByAfterIndex = new Map<number, number>();
|
||||||
|
for (const pp of projectedPicks) {
|
||||||
|
if (pp.picksFromNow <= 0) continue;
|
||||||
|
const afterIndex = pp.picksFromNow - 1;
|
||||||
|
if (afterIndex >= 0 && afterIndex < participants.length - 1) {
|
||||||
|
dividersByAfterIndex.set(afterIndex, pp.round);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < participants.length; i++) {
|
||||||
|
items.push({ type: "participant", index: i, key: participants[i].id });
|
||||||
|
const dividerRound = dividersByAfterIndex.get(i);
|
||||||
|
if (dividerRound !== undefined) {
|
||||||
|
items.push({ type: "divider", round: dividerRound, key: `divider-rd${dividerRound}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, [participants, projectedPicks, anyFilterActive]);
|
||||||
|
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: participants.length,
|
count: listItems.length,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 72,
|
estimateSize: (index) => listItems[index]?.type === "divider" ? 36 : 72,
|
||||||
overscan: 5,
|
overscan: 5,
|
||||||
getItemKey: (index) => participants[index]?.id ?? index,
|
getItemKey: (index) => listItems[index].key,
|
||||||
});
|
});
|
||||||
|
|
||||||
const desktopGridClass = hasTeam
|
const desktopGridClass = hasTeam
|
||||||
|
|
@ -400,10 +434,37 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||||
const participant = participants[virtualItem.index];
|
const listItem = listItems[virtualItem.index];
|
||||||
|
|
||||||
|
if (listItem.type === "divider") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={virtualItem.key}
|
||||||
|
data-index={virtualItem.index}
|
||||||
|
ref={virtualizer.measureElement}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: virtualItem.start,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2">
|
||||||
|
<div className="flex-1 border-t-2 border-dashed border-electric/40" />
|
||||||
|
<span className="text-sm font-semibold text-electric whitespace-nowrap">
|
||||||
|
Projected Round {listItem.round} Pick
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 border-t-2 border-dashed border-electric/40" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const participant = participants[listItem.index];
|
||||||
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
||||||
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
||||||
const rank = participantRanks.get(participant.id);
|
const rank = participantRanks.get(participant.id);
|
||||||
|
const followsDivider = listItems[virtualItem.index - 1]?.type === "divider";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -510,7 +571,9 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`hidden md:grid ${desktopGridClass} items-center px-4 transition-colors border-t ${
|
className={`hidden md:grid ${desktopGridClass} items-center px-4 transition-colors ${
|
||||||
|
followsDivider ? "" : "border-t"
|
||||||
|
} ${
|
||||||
isDrafted
|
isDrafted
|
||||||
? "bg-muted/50 opacity-60"
|
? "bg-muted/50 opacity-60"
|
||||||
: !isEligible
|
: !isEligible
|
||||||
|
|
|
||||||
210
app/lib/__tests__/draft-order.test.ts
Normal file
210
app/lib/__tests__/draft-order.test.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { getTeamForPick, getProjectedPicks } from "~/lib/draft-order";
|
||||||
|
|
||||||
|
function makeSlots(teamCount: number) {
|
||||||
|
return Array.from({ length: teamCount }, (_, i) => ({
|
||||||
|
draftOrder: i + 1,
|
||||||
|
teamId: `team-${i + 1}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getTeamForPick", () => {
|
||||||
|
const slots = makeSlots(4);
|
||||||
|
|
||||||
|
it("returns correct team for round 1 (forward)", () => {
|
||||||
|
expect(getTeamForPick(1, slots)?.teamId).toBe("team-1");
|
||||||
|
expect(getTeamForPick(2, slots)?.teamId).toBe("team-2");
|
||||||
|
expect(getTeamForPick(3, slots)?.teamId).toBe("team-3");
|
||||||
|
expect(getTeamForPick(4, slots)?.teamId).toBe("team-4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct team for round 2 (snake reversed)", () => {
|
||||||
|
expect(getTeamForPick(5, slots)?.teamId).toBe("team-4");
|
||||||
|
expect(getTeamForPick(6, slots)?.teamId).toBe("team-3");
|
||||||
|
expect(getTeamForPick(7, slots)?.teamId).toBe("team-2");
|
||||||
|
expect(getTeamForPick(8, slots)?.teamId).toBe("team-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct team for round 3 (forward again)", () => {
|
||||||
|
expect(getTeamForPick(9, slots)?.teamId).toBe("team-1");
|
||||||
|
expect(getTeamForPick(10, slots)?.teamId).toBe("team-2");
|
||||||
|
expect(getTeamForPick(11, slots)?.teamId).toBe("team-3");
|
||||||
|
expect(getTeamForPick(12, slots)?.teamId).toBe("team-4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for empty slots", () => {
|
||||||
|
expect(getTeamForPick(1, [])).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectedPicks", () => {
|
||||||
|
it("returns empty array when no draft slots", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: [],
|
||||||
|
userTeamId: "team-1",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 10,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array when user has no team in slots", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: makeSlots(4),
|
||||||
|
userTeamId: "team-999",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 10,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("4-team snake draft, user at position 1", () => {
|
||||||
|
const slots = makeSlots(4);
|
||||||
|
|
||||||
|
it("computes picks from start of draft", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: slots,
|
||||||
|
userTeamId: "team-1",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 1, pickNumber: 1, picksFromNow: 0 },
|
||||||
|
{ round: 2, pickNumber: 8, picksFromNow: 7 },
|
||||||
|
{ round: 3, pickNumber: 9, picksFromNow: 8 },
|
||||||
|
{ round: 4, pickNumber: 16, picksFromNow: 15 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes picks from mid-draft, skipping past picks", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: slots,
|
||||||
|
userTeamId: "team-1",
|
||||||
|
currentPick: 5,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [
|
||||||
|
{ pickNumber: 1, teamId: "team-1" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 2, pickNumber: 8, picksFromNow: 3 },
|
||||||
|
{ round: 3, pickNumber: 9, picksFromNow: 4 },
|
||||||
|
{ round: 4, pickNumber: 16, picksFromNow: 11 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips rounds already picked even if current pick is before them", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: slots,
|
||||||
|
userTeamId: "team-1",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [
|
||||||
|
{ pickNumber: 1, teamId: "team-1" },
|
||||||
|
{ pickNumber: 8, teamId: "team-1" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 3, pickNumber: 9, picksFromNow: 8 },
|
||||||
|
{ round: 4, pickNumber: 16, picksFromNow: 15 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("4-team snake draft, user at position 3", () => {
|
||||||
|
const slots = makeSlots(4);
|
||||||
|
|
||||||
|
it("computes correct snake positions", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: slots,
|
||||||
|
userTeamId: "team-3",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 1, pickNumber: 3, picksFromNow: 2 },
|
||||||
|
{ round: 2, pickNumber: 6, picksFromNow: 5 },
|
||||||
|
{ round: 3, pickNumber: 11, picksFromNow: 10 },
|
||||||
|
{ round: 4, pickNumber: 14, picksFromNow: 13 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("4-team snake draft, user at position 4 (last)", () => {
|
||||||
|
const slots = makeSlots(4);
|
||||||
|
|
||||||
|
it("picks first in round 2 due to snake reversal", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: slots,
|
||||||
|
userTeamId: "team-4",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 1, pickNumber: 4, picksFromNow: 3 },
|
||||||
|
{ round: 2, pickNumber: 5, picksFromNow: 4 },
|
||||||
|
{ round: 3, pickNumber: 12, picksFromNow: 11 },
|
||||||
|
{ round: 4, pickNumber: 13, picksFromNow: 12 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty when all picks are in the past", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: makeSlots(4),
|
||||||
|
userTeamId: "team-1",
|
||||||
|
currentPick: 17,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [
|
||||||
|
{ pickNumber: 1, teamId: "team-1" },
|
||||||
|
{ pickNumber: 8, teamId: "team-1" },
|
||||||
|
{ pickNumber: 9, teamId: "team-1" },
|
||||||
|
{ pickNumber: 16, teamId: "team-1" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles 8-team snake draft correctly", () => {
|
||||||
|
const slots = makeSlots(8);
|
||||||
|
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: slots,
|
||||||
|
userTeamId: "team-5",
|
||||||
|
currentPick: 1,
|
||||||
|
totalRounds: 3,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 1, pickNumber: 5, picksFromNow: 4 },
|
||||||
|
{ round: 2, pickNumber: 12, picksFromNow: 11 },
|
||||||
|
{ round: 3, pickNumber: 21, picksFromNow: 20 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes picks before currentPick", () => {
|
||||||
|
const result = getProjectedPicks({
|
||||||
|
draftSlots: makeSlots(4),
|
||||||
|
userTeamId: "team-1",
|
||||||
|
currentPick: 9,
|
||||||
|
totalRounds: 4,
|
||||||
|
existingPicks: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ round: 3, pickNumber: 9, picksFromNow: 0 },
|
||||||
|
{ round: 4, pickNumber: 16, picksFromNow: 7 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -16,3 +16,52 @@ export function getTeamForPick<T extends { draftOrder: number }>(
|
||||||
}
|
}
|
||||||
return draftSlots.find((slot) => slot.draftOrder === pickInRound);
|
return draftSlots.find((slot) => slot.draftOrder === pickInRound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectedPick {
|
||||||
|
round: number;
|
||||||
|
pickNumber: number;
|
||||||
|
picksFromNow: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectedPicks(options: {
|
||||||
|
draftSlots: Array<{ draftOrder: number; teamId: string }>;
|
||||||
|
userTeamId: string;
|
||||||
|
currentPick: number;
|
||||||
|
totalRounds: number;
|
||||||
|
existingPicks: Array<{ pickNumber: number; teamId: string }>;
|
||||||
|
}): ProjectedPick[] {
|
||||||
|
const { draftSlots, userTeamId, currentPick, totalRounds, existingPicks } = options;
|
||||||
|
const totalTeams = draftSlots.length;
|
||||||
|
if (totalTeams === 0) return [];
|
||||||
|
|
||||||
|
const userSlot = draftSlots.find((slot) => slot.teamId === userTeamId);
|
||||||
|
if (!userSlot) return [];
|
||||||
|
|
||||||
|
const userDraftOrder = userSlot.draftOrder;
|
||||||
|
const pickedNumbersForTeam = new Set(
|
||||||
|
existingPicks
|
||||||
|
.filter((p) => p.teamId === userTeamId)
|
||||||
|
.map((p) => p.pickNumber)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result: ProjectedPick[] = [];
|
||||||
|
|
||||||
|
for (let round = 1; round <= totalRounds; round++) {
|
||||||
|
const isEvenRound = round % 2 === 0;
|
||||||
|
const pickInRound = isEvenRound
|
||||||
|
? totalTeams - userDraftOrder + 1
|
||||||
|
: userDraftOrder;
|
||||||
|
const pickNumber = (round - 1) * totalTeams + pickInRound;
|
||||||
|
|
||||||
|
if (pickNumber < currentPick) continue;
|
||||||
|
if (pickedNumbersForTeam.has(pickNumber)) continue;
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
round,
|
||||||
|
pickNumber,
|
||||||
|
picksFromNow: pickNumber - currentPick,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay";
|
||||||
import { AuthRecoveryOverlay } from "~/components/draft/AuthRecoveryOverlay";
|
import { AuthRecoveryOverlay } from "~/components/draft/AuthRecoveryOverlay";
|
||||||
import { ParticipantSelectionDialog } from "~/components/draft/ParticipantSelectionDialog";
|
import { ParticipantSelectionDialog } from "~/components/draft/ParticipantSelectionDialog";
|
||||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||||
import { getTeamForPick } from "~/lib/draft-order";
|
import { getTeamForPick, getProjectedPicks } from "~/lib/draft-order";
|
||||||
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
||||||
import { NotificationSettings } from "~/components/NotificationSettings";
|
import { NotificationSettings } from "~/components/NotificationSettings";
|
||||||
import { AutodraftBadgeWithPopover } from "~/components/AutodraftSettings";
|
import { AutodraftBadgeWithPopover } from "~/components/AutodraftSettings";
|
||||||
|
|
@ -857,6 +857,20 @@ export default function DraftRoom() {
|
||||||
return grid;
|
return grid;
|
||||||
}, [picks, draftSlots, season.draftRounds]);
|
}, [picks, draftSlots, season.draftRounds]);
|
||||||
|
|
||||||
|
const projectedPicks = useMemo(() => {
|
||||||
|
if (!userTeam || season.status !== "draft" || isDraftComplete) return [];
|
||||||
|
return getProjectedPicks({
|
||||||
|
draftSlots,
|
||||||
|
userTeamId: userTeam.id,
|
||||||
|
currentPick,
|
||||||
|
totalRounds: season.draftRounds,
|
||||||
|
existingPicks: picks.map((p) => ({
|
||||||
|
pickNumber: p.pickNumber,
|
||||||
|
teamId: p.team.id,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}, [userTeam, season.status, isDraftComplete, draftSlots, currentPick, season.draftRounds, picks]);
|
||||||
|
|
||||||
// Get drafted participant IDs for filtering
|
// Get drafted participant IDs for filtering
|
||||||
const draftedParticipantIds = useMemo(
|
const draftedParticipantIds = useMemo(
|
||||||
() => new Set(picks.map((p) => p.participant.id)),
|
() => new Set(picks.map((p) => p.participant.id)),
|
||||||
|
|
@ -974,7 +988,8 @@ export default function DraftRoom() {
|
||||||
onMakePick: handleMakePick,
|
onMakePick: handleMakePick,
|
||||||
onAddToQueue: handleAddToQueue,
|
onAddToQueue: handleAddToQueue,
|
||||||
onRemoveFromQueue: handleRemoveFromQueue,
|
onRemoveFromQueue: handleRemoveFromQueue,
|
||||||
}), [filteredParticipants, participantRanks, miniDraftGrid, searchQuery, sportFilters, hideDrafted, hideIneligible, uniqueSports, draftedParticipantIds, queue, eligibility, canPick, userTeam, hideCompletedSports, userDraftedSportNames, handleHideCompletedSportsChange, handleMakePick, handleAddToQueue, handleRemoveFromQueue, setSearchQuery, setSportFilters, setHideDrafted, setHideIneligible]);
|
projectedPicks,
|
||||||
|
}), [filteredParticipants, participantRanks, miniDraftGrid, searchQuery, sportFilters, hideDrafted, hideIneligible, uniqueSports, draftedParticipantIds, queue, eligibility, canPick, userTeam, hideCompletedSports, userDraftedSportNames, handleHideCompletedSportsChange, handleMakePick, handleAddToQueue, handleRemoveFromQueue, setSearchQuery, setSportFilters, setHideDrafted, setHideIneligible, projectedPicks]);
|
||||||
|
|
||||||
const draftGridSectionProps = {
|
const draftGridSectionProps = {
|
||||||
draftSlots,
|
draftSlots,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue