From 777315541b42019a5eeecaca978af75fbc287dfc Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 7 May 2026 23:06:49 -0700 Subject: [PATCH] Use sport icons in league views (#394) --- .../league/SportsSeasonsSummary.tsx | 34 ++---- .../league/settings/SportsSection.tsx | 7 +- .../settings/__tests__/SportsSection.test.tsx | 114 ++++++++++++++++++ app/models/season.ts | 1 + app/routes/leagues/$leagueId.tsx | 1 + 5 files changed, 132 insertions(+), 25 deletions(-) create mode 100644 app/components/league/settings/__tests__/SportsSection.test.tsx diff --git a/app/components/league/SportsSeasonsSummary.tsx b/app/components/league/SportsSeasonsSummary.tsx index 1e3d775..1f24b11 100644 --- a/app/components/league/SportsSeasonsSummary.tsx +++ b/app/components/league/SportsSeasonsSummary.tsx @@ -1,6 +1,7 @@ import { format } from "date-fns"; -import { Calendar, CheckCircle2, ChevronRight, Clock, Layers, ListOrdered, SquareStack, Trophy } from "lucide-react"; +import { Calendar, CheckCircle2, Clock, Layers } from "lucide-react"; import { Link } from "react-router"; +import { SportIcon as SportSeasonIcon } from "~/components/SportIcon"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; @@ -21,6 +22,7 @@ export interface DraftedParticipantSummary { export interface SportSeasonSummaryItem { id: string; sportName: string; + sportIconUrl?: string | null; seasonName: string; status: "upcoming" | "active" | "completed"; scoringPattern?: string | null; @@ -52,25 +54,6 @@ function sortedByStatus(items: SportSeasonSummaryItem[]): SportSeasonSummaryItem }); } -const PATTERN_ICON: Record = { - playoff_bracket: { icon: ChevronRight, title: "Bracket" }, - season_standings: { icon: ListOrdered, title: "Season Standings" }, - qualifying_points: { icon: SquareStack, title: "Qualifying Points" }, -}; - -function SportIcon({ pattern }: { pattern?: string | null }) { - const entry = pattern ? PATTERN_ICON[pattern] : null; - const Icon = entry?.icon ?? Trophy; - return ( -
- -
- ); -} - /** Format the next upcoming event into a short string. */ function formatNextEvent(event: UpcomingParticipantEvent): string { const base = event.matchLabel @@ -175,8 +158,15 @@ function SportRow({ : "hover:bg-white/[0.06]" }`} > - {/* Left: scoring pattern icon in black box */} - + {/* Left: sport icon */} +
+ +
{/* Right: name + participants + next event */}
diff --git a/app/components/league/settings/SportsSection.tsx b/app/components/league/settings/SportsSection.tsx index 8a04fc7..5c8de02 100644 --- a/app/components/league/settings/SportsSection.tsx +++ b/app/components/league/settings/SportsSection.tsx @@ -1,7 +1,6 @@ import { ClipboardList } from "lucide-react"; import { Label } from "~/components/ui/label"; import { Button } from "~/components/ui/button"; -import { Checkbox } from "~/components/ui/checkbox"; import { cn } from "~/lib/utils"; import { SportIcon } from "~/components/league/SportIcon"; import { SettingsSection, SettingsStatusPill } from "./SettingsSection"; @@ -63,13 +62,15 @@ export function SportsSection({ !canEditSports && "cursor-not-allowed opacity-65" )} > - onSportToggle(ss.id)} + onChange={() => onSportToggle(ss.id)} disabled={!canEditSports} + className="sr-only" /> diff --git a/app/components/league/settings/__tests__/SportsSection.test.tsx b/app/components/league/settings/__tests__/SportsSection.test.tsx new file mode 100644 index 0000000..6a8c98b --- /dev/null +++ b/app/components/league/settings/__tests__/SportsSection.test.tsx @@ -0,0 +1,114 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { useState, type FormEvent } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { SportsSection } from "../SportsSection"; + +type SportsSeason = { + id: string; + name: string; + year: number; + scoringType: string; + sport: { id: string; name: string; slug: string; type: string; iconUrl: string | null }; +}; + +const displaySportsSeasons: SportsSeason[] = [ + { + id: "nba-2025", + name: "2025 Season", + year: 2025, + scoringType: "standings", + sport: { id: "sport-1", name: "NBA", slug: "nba", type: "team", iconUrl: "/sports-icons/nba.svg" }, + }, + { + id: "mlb-2025", + name: "2025 Season", + year: 2025, + scoringType: "playoffs", + sport: { id: "sport-2", name: "MLB", slug: "mlb", type: "team", iconUrl: "/sports-icons/mlb.svg" }, + }, +]; + +function SportsSectionHarness({ + initialSelected = ["nba-2025"], + canEditSports = true, + onSubmit, +}: { + initialSelected?: string[]; + canEditSports?: boolean; + onSubmit?: (selectedSports: string[]) => void; +}) { + const [selectedSports, setSelectedSports] = useState(new Set(initialSelected)); + + const handleSportToggle = (id: string) => { + setSelectedSports((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const handleClearAll = () => { + setSelectedSports(new Set()); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + onSubmit?.(formData.getAll("sportsSeasons").map(String)); + }; + + return ( +
+ + + + ); +} + +describe("SportsSection", () => { + it("keeps native checkbox semantics while using the row as the visible control", async () => { + const user = userEvent.setup(); + render(); + + const mlbCheckbox = screen.getByRole("checkbox", { name: /mlb - 2025 season \(2025\)/i }); + expect(mlbCheckbox).not.toBeChecked(); + + await user.click(screen.getByText("MLB - 2025 Season (2025)")); + + expect(mlbCheckbox).toBeChecked(); + }); + + it("submits the currently selected sports seasons after toggling", async () => { + const user = userEvent.setup(); + const handleSubmit = vi.fn(); + render(); + + await user.click(screen.getByText("MLB - 2025 Season (2025)")); + await user.click(screen.getByRole("button", { name: "Save" })); + + expect(handleSubmit).toHaveBeenCalledWith(["nba-2025", "mlb-2025"]); + }); + + it("submits an empty sports selection after clearing all", async () => { + const user = userEvent.setup(); + const handleSubmit = vi.fn(); + render(); + + await user.click(screen.getByRole("button", { name: /clear all/i })); + await user.click(screen.getByRole("button", { name: "Save" })); + + expect(handleSubmit).toHaveBeenCalledWith([]); + }); +}); diff --git a/app/models/season.ts b/app/models/season.ts index 78e592f..d573f48 100644 --- a/app/models/season.ts +++ b/app/models/season.ts @@ -25,6 +25,7 @@ export interface SeasonWithSportsSeasons extends Season { name: string; type: string; slug: string; + iconUrl: string | null; }; }; }>; diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx index 31d36d9..8609672 100644 --- a/app/routes/leagues/$leagueId.tsx +++ b/app/routes/leagues/$leagueId.tsx @@ -232,6 +232,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { sportsSeasons={sortedSportsSeasons.map((ss) => ({ id: ss.id, sportName: ss.sport.name, + sportIconUrl: ss.sport.iconUrl, seasonName: ss.name, status: ss.status, scoringPattern: ss.scoringPattern,