Use sport icons in league views (#394)
This commit is contained in:
parent
20685bae6b
commit
777315541b
5 changed files with 132 additions and 25 deletions
|
|
@ -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<string, { icon: typeof ChevronRight; title: string }> = {
|
||||
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 (
|
||||
<div
|
||||
className="h-9 w-9 shrink-0 flex items-center justify-center bg-black"
|
||||
title={entry?.title}
|
||||
>
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 */}
|
||||
<SportIcon pattern={item.scoringPattern} />
|
||||
{/* Left: sport icon */}
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center">
|
||||
<SportSeasonIcon
|
||||
sportName={item.sportName}
|
||||
iconUrl={item.sportIconUrl}
|
||||
size="md"
|
||||
decorative
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right: name + participants + next event */}
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
<input
|
||||
id={`sport-${ss.id}`}
|
||||
type="checkbox"
|
||||
name="sportsSeasons"
|
||||
value={ss.id}
|
||||
checked={selected}
|
||||
onCheckedChange={() => onSportToggle(ss.id)}
|
||||
onChange={() => onSportToggle(ss.id)}
|
||||
disabled={!canEditSports}
|
||||
className="sr-only"
|
||||
/>
|
||||
<SportIcon sport={ss.sport} />
|
||||
<span className="min-w-0 flex-1 overflow-hidden">
|
||||
|
|
|
|||
114
app/components/league/settings/__tests__/SportsSection.test.tsx
Normal file
114
app/components/league/settings/__tests__/SportsSection.test.tsx
Normal file
|
|
@ -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<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
onSubmit?.(formData.getAll("sportsSeasons").map(String));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SportsSection
|
||||
active
|
||||
canEditSports={canEditSports}
|
||||
selectedSports={selectedSports}
|
||||
displaySportsSeasons={displaySportsSeasons}
|
||||
onSportToggle={handleSportToggle}
|
||||
onClearAll={handleClearAll}
|
||||
/>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
describe("SportsSection", () => {
|
||||
it("keeps native checkbox semantics while using the row as the visible control", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SportsSectionHarness />);
|
||||
|
||||
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(<SportsSectionHarness onSubmit={handleSubmit} />);
|
||||
|
||||
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(<SportsSectionHarness onSubmit={handleSubmit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /clear all/i }));
|
||||
await user.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -25,6 +25,7 @@ export interface SeasonWithSportsSeasons extends Season {
|
|||
name: string;
|
||||
type: string;
|
||||
slug: string;
|
||||
iconUrl: string | null;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue