From 7b491d86d23c7fcd8679c870ecc74627687a8b79 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:13:51 -0800 Subject: [PATCH] refactor: integrate sport filter dropdown with drafted sports toggle (#53) - Move "Show drafted sports" into the sport filter dropdown (sheet/popover) instead of a standalone checkbox in the filter bar - When unchecked, drafted sports are hidden from the dropdown list entirely; when checked, they appear in alphabetical order with muted text - Auto-deselect drafted sport filters when hiding drafted sports - Reset button also restores "Show drafted sports" to checked - Extract SportFilterContent component to eliminate sheet/popover duplication - Make userDraftedSportNames required; add useCallback for handlers; memoize triggerText/triggerAriaLabel; unify all checkboxes to ShadCN Checkbox; replace empty div divider with hr Co-authored-by: Claude Sonnet 4.6 --- .../AvailableParticipantsSection.test.tsx | 11 +- .../draft/AvailableParticipantsSection.tsx | 240 ++++++++++++------ .../leagues/$leagueId.draft.$seasonId.tsx | 18 +- 3 files changed, 182 insertions(+), 87 deletions(-) diff --git a/app/components/__tests__/AvailableParticipantsSection.test.tsx b/app/components/__tests__/AvailableParticipantsSection.test.tsx index a4dd801..3d28666 100644 --- a/app/components/__tests__/AvailableParticipantsSection.test.tsx +++ b/app/components/__tests__/AvailableParticipantsSection.test.tsx @@ -15,6 +15,7 @@ const defaultProps = { hideIneligible: false, hideCompletedSports: false, uniqueSports: ["NBA", "NFL"], + userDraftedSportNames: new Set(), draftedParticipantIds: new Set(), queue: [] as Array<{ id: string; participantId: string }>, eligibility: null, @@ -64,7 +65,7 @@ describe("AvailableParticipantsSection", () => { expect(screen.getByText("Show Drafted")).toBeInTheDocument(); }); - it("renders Show Ineligible and Show drafted sports toggles when hasTeam=true", () => { + it("renders Show Ineligible and Show drafted sports toggles when hasTeam=true", async () => { render( { /> ); expect(screen.getByText("Show Ineligible")).toBeInTheDocument(); - expect(screen.getByText("Show drafted sports")).toBeInTheDocument(); + // Show drafted sports lives inside the sport filter dropdown + await clickFirstSportFilterTrigger("Filter by sport: All Sports"); + const dialog = screen.getByRole("dialog"); + expect(within(dialog).getByText("Show drafted sports")).toBeInTheDocument(); }); it("does not render Show Ineligible when hasTeam=false", () => { @@ -185,7 +189,7 @@ describe("AvailableParticipantsSection", () => { expect(within(dialog).getByText("Reset")).toBeInTheDocument(); }); - it('calls onSportFiltersChange([]) when "Reset" clicked', async () => { + it('calls onSportFiltersChange([]) and onHideCompletedSportsChange(false) when "Reset" clicked', async () => { render( ); @@ -193,6 +197,7 @@ describe("AvailableParticipantsSection", () => { const dialog = screen.getByRole("dialog"); await user.click(within(dialog).getByText("Reset")); expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith([]); + expect(defaultProps.onHideCompletedSportsChange).toHaveBeenCalledWith(false); }); it("Done button closes the sheet", async () => { diff --git a/app/components/draft/AvailableParticipantsSection.tsx b/app/components/draft/AvailableParticipantsSection.tsx index 2fed65b..81061c0 100644 --- a/app/components/draft/AvailableParticipantsSection.tsx +++ b/app/components/draft/AvailableParticipantsSection.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Checkbox } from "~/components/ui/checkbox"; @@ -35,6 +35,92 @@ function getParticipantState( return { isDrafted, isInQueue, isEligible, ineligibleReason }; } +interface SportFilterContentProps { + sportsForDropdown: Array<{ name: string; isDrafted: boolean }>; + sportFilterSet: Set; + hideCompletedSports: boolean; + hasTeam: boolean; + variant: "sheet" | "popover"; + onToggleSport: (sport: string, checked: boolean | "indeterminate") => void; + onHideCompletedSportsChange: (hide: boolean) => void; +} + +function SportFilterContent({ + sportsForDropdown, + sportFilterSet, + hideCompletedSports, + hasTeam, + variant, + onToggleSport, + onHideCompletedSportsChange, +}: SportFilterContentProps) { + const isSheet = variant === "sheet"; + const idPrefix = isSheet ? "sheet-sport-filter-" : "popover-sport-filter-"; + const listClass = isSheet + ? "overflow-y-auto flex-1 px-4" + : "max-h-64 overflow-y-auto p-2 space-y-0.5"; + const itemBaseClass = isSheet + ? "flex items-center gap-4 py-4 border-b last:border-b-0 cursor-pointer text-base" + : "flex items-center gap-2 px-2 py-2 rounded-sm hover:bg-accent cursor-pointer text-sm"; + const toggleLabelClass = isSheet + ? "flex items-center gap-4 py-2 cursor-pointer text-base" + : "flex items-center gap-2 px-4 py-3 cursor-pointer text-sm"; + + return ( + <> +
+ {sportsForDropdown + .filter(({ isDrafted }) => !isDrafted || !hideCompletedSports) + .map(({ name, isDrafted }) => { + const checkboxId = `${idPrefix}${name.replace(/\s+/g, "-").toLowerCase()}`; + return ( + + ); + })} +
+ {hasTeam && ( + isSheet ? ( +
+ +
+ ) : ( + <> +
+ + + ) + )} + + ); +} + interface AvailableParticipantsSectionProps { participants: Array<{ id: string; @@ -49,6 +135,7 @@ interface AvailableParticipantsSectionProps { hideDrafted: boolean; hideIneligible: boolean; hideCompletedSports: boolean; + userDraftedSportNames: Set; uniqueSports: string[]; draftedParticipantIds: Set; queue: Array<{ id: string; participantId: string }>; @@ -75,6 +162,7 @@ export function AvailableParticipantsSection({ hideDrafted, hideIneligible, hideCompletedSports, + userDraftedSportNames, uniqueSports, draftedParticipantIds, queue, @@ -97,27 +185,48 @@ export function AvailableParticipantsSection({ const sportFilterSet = useMemo(() => new Set(sportFilters), [sportFilters]); - const triggerText = - sportFilters.length === 0 - ? "All Sports" - : sportFilters.length === 1 - ? sportFilters[0] - : `${sportFilters.length} sports`; + const sportsForDropdown = useMemo(() => { + return uniqueSports.map((s) => ({ + name: s, + isDrafted: hasTeam && userDraftedSportNames.has(s), + })); + }, [uniqueSports, userDraftedSportNames, hasTeam]); - 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`; + const triggerText = useMemo( + () => + sportFilters.length === 0 + ? "All Sports" + : sportFilters.length === 1 + ? sportFilters[0] + : `${sportFilters.length} sports`, + [sportFilters] + ); - function handleToggleSport(sport: string, checked: boolean | "indeterminate") { - if (checked === true) { - onSportFiltersChange([...sportFilters, sport]); - } else { - onSportFiltersChange(sportFilters.filter((s) => s !== sport)); - } - } + const triggerAriaLabel = useMemo( + () => + sportFilters.length === 0 + ? "Filter by sport: All Sports" + : sportFilters.length === 1 + ? `Filter by sport: ${sportFilters[0]}` + : `Filter by sport: ${sportFilters.length} sports selected`, + [sportFilters] + ); + + const handleToggleSport = useCallback( + (sport: string, checked: boolean | "indeterminate") => { + if (checked === true) { + onSportFiltersChange([...sportFilters, sport]); + } else { + onSportFiltersChange(sportFilters.filter((s) => s !== sport)); + } + }, + [sportFilters, onSportFiltersChange] + ); + + const handleReset = useCallback(() => { + onSportFiltersChange([]); + onHideCompletedSportsChange(false); + }, [onSportFiltersChange, onHideCompletedSportsChange]); const emptyMessage = useMemo(() => { if (participants.length > 0) return null; @@ -130,6 +239,8 @@ export function AvailableParticipantsSection({ return `No participants found. Try showing ${active.join(", ")}.`; }, [participants.length, hideDrafted, hideIneligible, hideCompletedSports, eligibility, sportFilters]); + const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports; + return (
@@ -161,31 +272,21 @@ export function AvailableParticipantsSection({ Filter by sport -
- {uniqueSports.map((sport) => { - const checkboxId = `sheet-sport-filter-${sport.replace(/\s+/g, "-").toLowerCase()}`; - return ( - - ); - })} -
+ - {sportFilters.length > 0 && ( + {hasActiveFilters && ( @@ -212,37 +313,27 @@ export function AvailableParticipantsSection({ - {sportFilters.length > 0 && ( + {hasActiveFilters && (
)} -
- {uniqueSports.map((sport) => { - const checkboxId = `popover-sport-filter-${sport.replace(/\s+/g, "-").toLowerCase()}`; - return ( - - ); - })} -
+
@@ -250,11 +341,9 @@ export function AvailableParticipantsSection({ {/* Show/Hide Drafted Toggle */} @@ -262,28 +351,13 @@ export function AvailableParticipantsSection({ {/* Show/Hide Ineligible Toggle - only show if user has a team */} {hasTeam && eligibility && ( )} - - {/* Show drafted sports - only show if user has a team */} - {hasTeam && ( - - )}
diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 158e07c..23cb384 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -1265,6 +1265,16 @@ export default function DraftRoom() { ); }, [picks, userTeam]); + // Sport names where the current user's team has already made at least one pick + const userDraftedSportNames = useMemo(() => { + if (!userTeam) return new Set(); + return new Set( + picks + .filter((p: any) => p.team.id === userTeam.id) + .map((p: any) => p.sport.name) + ); + }, [picks, userTeam]); + // Get unique sports for filter dropdown const uniqueSports = useMemo( () => @@ -1326,7 +1336,13 @@ export default function DraftRoom() { onHideDraftedChange: setHideDrafted, onHideIneligibleChange: setHideIneligible, hideCompletedSports, - onHideCompletedSportsChange: setHideCompletedSports, + userDraftedSportNames, + onHideCompletedSportsChange: (hide: boolean) => { + setHideCompletedSports(hide); + if (hide) { + setSportFilters((prev) => prev.filter((s) => !userDraftedSportNames.has(s))); + } + }, onMakePick: handleMakePick, onAddToQueue: handleAddToQueue, onRemoveFromQueue: handleRemoveFromQueue,