feat: convert sport filter to multi-select with mobile bottom sheet (#51)
Replaces the single-sport native <select> with a popover (desktop) and bottom sheet (mobile) checkbox multi-select. No sports selected shows all participants; selecting one or more narrows the list. Trigger label adapts to reflect current filter state and a Reset button appears when filters are active. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5f23885097
commit
da0e0f2be8
3 changed files with 388 additions and 53 deletions
206
app/components/__tests__/AvailableParticipantsSection.test.tsx
Normal file
206
app/components/__tests__/AvailableParticipantsSection.test.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
participants: [
|
||||||
|
{ id: "1", name: "Player A", sport: { id: "s1", name: "NFL" } },
|
||||||
|
{ id: "2", name: "Player B", sport: { id: "s2", name: "NBA" } },
|
||||||
|
{ id: "3", name: "Player C", sport: { id: "s1", name: "NFL" } },
|
||||||
|
],
|
||||||
|
searchQuery: "",
|
||||||
|
sportFilters: [] as string[],
|
||||||
|
hideDrafted: false,
|
||||||
|
hideIneligible: false,
|
||||||
|
hideCompletedSports: false,
|
||||||
|
uniqueSports: ["NBA", "NFL"],
|
||||||
|
draftedParticipantIds: new Set<string>(),
|
||||||
|
queue: [] as Array<{ id: string; participantId: string }>,
|
||||||
|
eligibility: null,
|
||||||
|
canPick: false,
|
||||||
|
hasTeam: false,
|
||||||
|
onSearchChange: vi.fn(),
|
||||||
|
onSportFiltersChange: vi.fn(),
|
||||||
|
onHideDraftedChange: vi.fn(),
|
||||||
|
onHideIneligibleChange: vi.fn(),
|
||||||
|
onHideCompletedSportsChange: vi.fn(),
|
||||||
|
onMakePick: vi.fn(),
|
||||||
|
onAddToQueue: vi.fn(),
|
||||||
|
onRemoveFromQueue: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Both the mobile Sheet trigger and desktop Popover trigger are rendered in jsdom
|
||||||
|
// (CSS breakpoints are not applied). Click the first one to open the Sheet.
|
||||||
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
|
||||||
|
function clickFirstSportFilterTrigger(name: string | RegExp) {
|
||||||
|
return user.click(screen.getAllByRole("button", { name })[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AvailableParticipantsSection", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
user = userEvent.setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Rendering", () => {
|
||||||
|
it("renders all participants by name", () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
expect(screen.getAllByText("Player A").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Player B").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Player C").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders search input", () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
expect(
|
||||||
|
screen.getByPlaceholderText("Search participants...")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the Show Drafted toggle", () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
expect(screen.getByText("Show Drafted")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Show Ineligible and Show drafted sports toggles when hasTeam=true", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
hasTeam={true}
|
||||||
|
eligibility={{ eligibleSportIds: new Set(["s1"]), ineligibleReasons: {} }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Show Ineligible")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Show drafted sports")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render Show Ineligible when hasTeam=false", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
hasTeam={false}
|
||||||
|
eligibility={{ eligibleSportIds: new Set(), ineligibleReasons: {} }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("Show Ineligible")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders empty state when no participants", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection {...defaultProps} participants={[]} />
|
||||||
|
);
|
||||||
|
expect(screen.getAllByText("No participants found.").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sport Filter - trigger label", () => {
|
||||||
|
it('shows "All Sports" in aria-label when no filters selected', () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
// Both mobile and desktop triggers share the same label
|
||||||
|
const triggers = screen.getAllByRole("button", {
|
||||||
|
name: "Filter by sport: All Sports",
|
||||||
|
});
|
||||||
|
expect(triggers.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows sport name in aria-label when exactly one filter selected", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
|
||||||
|
);
|
||||||
|
const triggers = screen.getAllByRole("button", {
|
||||||
|
name: "Filter by sport: NFL",
|
||||||
|
});
|
||||||
|
expect(triggers.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows count in aria-label when multiple filters selected", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
sportFilters={["NFL", "NBA"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const triggers = screen.getAllByRole("button", {
|
||||||
|
name: "Filter by sport: 2 sports selected",
|
||||||
|
});
|
||||||
|
expect(triggers.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render active-filter badges", () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
sportFilters={["NFL", "NBA"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByLabelText(/Remove .* filter/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sport Filter - sheet/popover", () => {
|
||||||
|
it("opens and shows checkboxes for each sport", async () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
expect(within(dialog).getByText("NBA")).toBeInTheDocument();
|
||||||
|
expect(within(dialog).getByText("NFL")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSportFiltersChange with sport added when checkbox clicked", async () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
await user.click(within(dialog).getByLabelText("NFL"));
|
||||||
|
expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith(["NFL"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSportFiltersChange with sport removed when unchecked", async () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection
|
||||||
|
{...defaultProps}
|
||||||
|
sportFilters={["NFL", "NBA"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: 2 sports selected");
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
await user.click(within(dialog).getByLabelText("NFL"));
|
||||||
|
expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith(["NBA"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show "Reset" when no filters active', async () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
expect(within(dialog).queryByText("Reset")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "Reset" button when filters exist', async () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
|
||||||
|
);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: NFL");
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
expect(within(dialog).getByText("Reset")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSportFiltersChange([]) when "Reset" clicked', async () => {
|
||||||
|
render(
|
||||||
|
<AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
|
||||||
|
);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: NFL");
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
await user.click(within(dialog).getByText("Reset"));
|
||||||
|
expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Done button closes the sheet", async () => {
|
||||||
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
||||||
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
||||||
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||||
|
await user.click(screen.getByRole("button", { name: "Done" }));
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,21 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
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 { ListPlus, ListX } from "lucide-react";
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetContent,
|
||||||
|
SheetTitle,
|
||||||
|
SheetFooter,
|
||||||
|
SheetClose,
|
||||||
|
} from "~/components/ui/sheet";
|
||||||
|
import { ListPlus, ListX, ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
function getParticipantState(
|
function getParticipantState(
|
||||||
participant: { id: string; sport: { id: string } },
|
participant: { id: string; sport: { id: string } },
|
||||||
|
|
@ -31,7 +45,7 @@ interface AvailableParticipantsSectionProps {
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
sportFilter: string;
|
sportFilters: string[];
|
||||||
hideDrafted: boolean;
|
hideDrafted: boolean;
|
||||||
hideIneligible: boolean;
|
hideIneligible: boolean;
|
||||||
hideCompletedSports: boolean;
|
hideCompletedSports: boolean;
|
||||||
|
|
@ -45,7 +59,7 @@ interface AvailableParticipantsSectionProps {
|
||||||
canPick: boolean;
|
canPick: boolean;
|
||||||
hasTeam: boolean;
|
hasTeam: boolean;
|
||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
onSportFilterChange: (sport: string) => void;
|
onSportFiltersChange: (sports: string[]) => void;
|
||||||
onHideDraftedChange: (hide: boolean) => void;
|
onHideDraftedChange: (hide: boolean) => void;
|
||||||
onHideIneligibleChange: (hide: boolean) => void;
|
onHideIneligibleChange: (hide: boolean) => void;
|
||||||
onHideCompletedSportsChange: (hide: boolean) => void;
|
onHideCompletedSportsChange: (hide: boolean) => void;
|
||||||
|
|
@ -57,7 +71,7 @@ interface AvailableParticipantsSectionProps {
|
||||||
export function AvailableParticipantsSection({
|
export function AvailableParticipantsSection({
|
||||||
participants,
|
participants,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
sportFilter,
|
sportFilters,
|
||||||
hideDrafted,
|
hideDrafted,
|
||||||
hideIneligible,
|
hideIneligible,
|
||||||
hideCompletedSports,
|
hideCompletedSports,
|
||||||
|
|
@ -68,7 +82,7 @@ export function AvailableParticipantsSection({
|
||||||
canPick,
|
canPick,
|
||||||
hasTeam,
|
hasTeam,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSportFilterChange,
|
onSportFiltersChange,
|
||||||
onHideDraftedChange,
|
onHideDraftedChange,
|
||||||
onHideIneligibleChange,
|
onHideIneligibleChange,
|
||||||
onHideCompletedSportsChange,
|
onHideCompletedSportsChange,
|
||||||
|
|
@ -81,15 +95,40 @@ export function AvailableParticipantsSection({
|
||||||
[queue]
|
[queue]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sportFilterSet = useMemo(() => new Set(sportFilters), [sportFilters]);
|
||||||
|
|
||||||
|
const triggerText =
|
||||||
|
sportFilters.length === 0
|
||||||
|
? "All Sports"
|
||||||
|
: sportFilters.length === 1
|
||||||
|
? sportFilters[0]
|
||||||
|
: `${sportFilters.length} sports`;
|
||||||
|
|
||||||
|
const triggerAriaLabel =
|
||||||
|
sportFilters.length === 0
|
||||||
|
? "Filter by sport: All Sports"
|
||||||
|
: sportFilters.length === 1
|
||||||
|
? `Filter by sport: ${sportFilters[0]}`
|
||||||
|
: `Filter by sport: ${sportFilters.length} sports selected`;
|
||||||
|
|
||||||
|
function handleToggleSport(sport: string, checked: boolean | "indeterminate") {
|
||||||
|
if (checked === true) {
|
||||||
|
onSportFiltersChange([...sportFilters, sport]);
|
||||||
|
} else {
|
||||||
|
onSportFiltersChange(sportFilters.filter((s) => s !== sport));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const emptyMessage = useMemo(() => {
|
const emptyMessage = useMemo(() => {
|
||||||
if (participants.length > 0) return null;
|
if (participants.length > 0) return null;
|
||||||
const active: string[] = [];
|
const active: string[] = [];
|
||||||
if (hideDrafted) active.push("drafted players");
|
if (hideDrafted) active.push("drafted players");
|
||||||
if (hideIneligible && eligibility) active.push("ineligible players");
|
if (hideIneligible && eligibility) active.push("ineligible players");
|
||||||
if (hideCompletedSports) active.push("drafted sports");
|
if (hideCompletedSports) active.push("drafted sports");
|
||||||
|
if (sportFilters.length > 0) active.push("other sports");
|
||||||
if (active.length === 0) return "No participants found.";
|
if (active.length === 0) return "No participants found.";
|
||||||
return `No participants found. Try showing ${active.join(", ")}.`;
|
return `No participants found. Try showing ${active.join(", ")}.`;
|
||||||
}, [participants.length, hideDrafted, hideIneligible, hideCompletedSports, eligibility]);
|
}, [participants.length, hideDrafted, hideIneligible, hideCompletedSports, eligibility, sportFilters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
|
|
@ -105,19 +144,109 @@ export function AvailableParticipantsSection({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{/* Sport Filter Dropdown */}
|
{/* Sport Filter Multi-Select */}
|
||||||
<select
|
<div className="flex-1 min-w-[120px]">
|
||||||
value={sportFilter}
|
{/* Mobile: bottom sheet */}
|
||||||
onChange={(e) => onSportFilterChange(e.target.value)}
|
<div className="md:hidden">
|
||||||
className="flex-1 min-w-[120px] px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
<Sheet>
|
||||||
>
|
<SheetTrigger asChild>
|
||||||
<option value="all">All Sports</option>
|
<Button
|
||||||
{uniqueSports.map((sport) => (
|
variant="outline"
|
||||||
<option key={sport} value={sport}>
|
aria-label={triggerAriaLabel}
|
||||||
{sport}
|
className="w-full justify-between text-base font-normal"
|
||||||
</option>
|
>
|
||||||
))}
|
<span className="truncate">{triggerText}</span>
|
||||||
</select>
|
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="bottom" className="max-h-[70vh] pt-12" aria-describedby={undefined}>
|
||||||
|
<SheetTitle className="sr-only">Filter by sport</SheetTitle>
|
||||||
|
<div className="overflow-y-auto flex-1 px-4">
|
||||||
|
{uniqueSports.map((sport) => {
|
||||||
|
const checkboxId = `sheet-sport-filter-${sport.replace(/\s+/g, "-").toLowerCase()}`;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={sport}
|
||||||
|
htmlFor={checkboxId}
|
||||||
|
className="flex items-center gap-4 py-4 border-b last:border-b-0 cursor-pointer text-base"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={checkboxId}
|
||||||
|
checked={sportFilterSet.has(sport)}
|
||||||
|
onCheckedChange={(checked) => handleToggleSport(sport, checked)}
|
||||||
|
/>
|
||||||
|
{sport}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<SheetFooter className="px-4 pb-8 flex-row gap-2">
|
||||||
|
{sportFilters.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => onSportFiltersChange([])}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button className="flex-1">Done</Button>
|
||||||
|
</SheetClose>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop: popover */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
aria-label={triggerAriaLabel}
|
||||||
|
className="w-full justify-between text-sm font-normal"
|
||||||
|
>
|
||||||
|
<span className="truncate">{triggerText}</span>
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-72 p-0" align="start">
|
||||||
|
{sportFilters.length > 0 && (
|
||||||
|
<div className="flex justify-end px-2 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto px-2 py-1 text-xs"
|
||||||
|
onClick={() => onSportFiltersChange([])}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="max-h-64 overflow-y-auto p-2 space-y-0.5">
|
||||||
|
{uniqueSports.map((sport) => {
|
||||||
|
const checkboxId = `popover-sport-filter-${sport.replace(/\s+/g, "-").toLowerCase()}`;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={sport}
|
||||||
|
htmlFor={checkboxId}
|
||||||
|
className="flex items-center gap-2 px-2 py-2 rounded-sm hover:bg-accent cursor-pointer text-sm"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={checkboxId}
|
||||||
|
checked={sportFilterSet.has(sport)}
|
||||||
|
onCheckedChange={(checked) => handleToggleSport(sport, checked)}
|
||||||
|
/>
|
||||||
|
{sport}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Show/Hide Drafted Toggle */}
|
{/* Show/Hide Drafted Toggle */}
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
||||||
|
|
@ -156,6 +285,7 @@ export function AvailableParticipantsSection({
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -434,7 +434,7 @@ export default function DraftRoom() {
|
||||||
const [hideDrafted, setHideDrafted] = useState(true);
|
const [hideDrafted, setHideDrafted] = useState(true);
|
||||||
const [hideIneligible, setHideIneligible] = useState(true);
|
const [hideIneligible, setHideIneligible] = useState(true);
|
||||||
const [hideCompletedSports, setHideCompletedSports] = useState(false);
|
const [hideCompletedSports, setHideCompletedSports] = useState(false);
|
||||||
const [sportFilter, setSportFilter] = useState<string>("all");
|
const [sportFilters, setSportFilters] = useState<string[]>([]);
|
||||||
const [queue, setQueue] = useState<QueueItem[]>(userQueue);
|
const [queue, setQueue] = useState<QueueItem[]>(userQueue);
|
||||||
const [isPaused, setIsPaused] = useState(season.draftPaused || false);
|
const [isPaused, setIsPaused] = useState(season.draftPaused || false);
|
||||||
const [isDraftComplete, setIsDraftComplete] = useState(
|
const [isDraftComplete, setIsDraftComplete] = useState(
|
||||||
|
|
@ -1275,45 +1275,44 @@ export default function DraftRoom() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter participants based on search, sport, drafted status, and eligibility
|
// Filter participants based on search, sport, drafted status, and eligibility
|
||||||
const filteredParticipants = useMemo(
|
const filteredParticipants = useMemo(() => {
|
||||||
() =>
|
const sportFilterSet = new Set(sportFilters);
|
||||||
availableParticipants.filter((participant: any) => {
|
return availableParticipants.filter((participant: any) => {
|
||||||
// Drafted filter - hide drafted participants by default
|
// Drafted filter - hide drafted participants by default
|
||||||
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
|
if (hideDrafted && draftedParticipantIds.has(participant.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Eligibility filter - hide ineligible participants by default (if user has a team)
|
||||||
|
if (hideIneligible && eligibility) {
|
||||||
|
const isEligible = eligibility.eligibleSportIds.has(participant.sport.id);
|
||||||
|
if (!isEligible) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Eligibility filter - hide ineligible participants by default (if user has a team)
|
}
|
||||||
if (hideIneligible && eligibility) {
|
// Hide completed sports filter - hide participants from sports already drafted by user's team
|
||||||
const isEligible = eligibility.eligibleSportIds.has(participant.sport.id);
|
if (hideCompletedSports && userDraftedSportIds.has(participant.sport.id)) {
|
||||||
if (!isEligible) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
// Search filter
|
||||||
}
|
if (
|
||||||
// Hide completed sports filter - hide participants from sports already drafted by user's team
|
searchQuery &&
|
||||||
if (hideCompletedSports && userDraftedSportIds.has(participant.sport.id)) {
|
!participant.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
return false;
|
) {
|
||||||
}
|
return false;
|
||||||
// Search filter
|
}
|
||||||
if (
|
// Sport filter
|
||||||
searchQuery &&
|
if (sportFilterSet.size > 0 && !sportFilterSet.has(participant.sport.name)) {
|
||||||
!participant.name.toLowerCase().includes(searchQuery.toLowerCase())
|
return false;
|
||||||
) {
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
});
|
||||||
// Sport filter
|
}, [availableParticipants, hideDrafted, hideIneligible, hideCompletedSports, eligibility, draftedParticipantIds, userDraftedSportIds, searchQuery, sportFilters]);
|
||||||
if (sportFilter !== "all" && participant.sport.name !== sportFilter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
[availableParticipants, hideDrafted, hideIneligible, hideCompletedSports, eligibility, draftedParticipantIds, userDraftedSportIds, searchQuery, sportFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Shared component props — defined once to avoid duplication between desktop and mobile layouts
|
// Shared component props — defined once to avoid duplication between desktop and mobile layouts
|
||||||
const availableParticipantsSectionProps = {
|
const availableParticipantsSectionProps = {
|
||||||
participants: filteredParticipants,
|
participants: filteredParticipants,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
sportFilter,
|
sportFilters,
|
||||||
hideDrafted,
|
hideDrafted,
|
||||||
hideIneligible,
|
hideIneligible,
|
||||||
uniqueSports,
|
uniqueSports,
|
||||||
|
|
@ -1323,7 +1322,7 @@ export default function DraftRoom() {
|
||||||
canPick,
|
canPick,
|
||||||
hasTeam: !!userTeam,
|
hasTeam: !!userTeam,
|
||||||
onSearchChange: setSearchQuery,
|
onSearchChange: setSearchQuery,
|
||||||
onSportFilterChange: setSportFilter,
|
onSportFiltersChange: setSportFilters,
|
||||||
onHideDraftedChange: setHideDrafted,
|
onHideDraftedChange: setHideDrafted,
|
||||||
onHideIneligibleChange: setHideIneligible,
|
onHideIneligibleChange: setHideIneligible,
|
||||||
hideCompletedSports,
|
hideCompletedSports,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue