Extract all settings sections from the monolithic 1009-line route file into individual components under app/components/league/settings/. Route file drops to ~300 lines. Separates draft-order dirty state from general settings dirty state, deduplicates section-change handling, and fixes several bugs found during review (typo in pointsFor5th, wrong mock in tests, lint violations). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
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";
|
|
|
|
type SportsSeason = {
|
|
id: string;
|
|
name: string;
|
|
year: number;
|
|
scoringType: string;
|
|
sport: { id: string; name: string; slug: string; type: string; iconUrl: string | null };
|
|
};
|
|
|
|
const BRACKT_SLUG = "brackt";
|
|
|
|
export function SportsSection({
|
|
active,
|
|
canEditSports,
|
|
selectedSports,
|
|
displaySportsSeasons,
|
|
onSportToggle,
|
|
onClearAll,
|
|
}: {
|
|
active: boolean;
|
|
canEditSports: boolean;
|
|
selectedSports: Set<string>;
|
|
displaySportsSeasons: SportsSeason[];
|
|
onSportToggle: (id: string) => void;
|
|
onClearAll: () => void;
|
|
}) {
|
|
return (
|
|
<SettingsSection
|
|
id="sports"
|
|
icon={ClipboardList}
|
|
title="Sports"
|
|
description="Manage draftable sports seasons for this league. Sports lock when the draft starts."
|
|
status={<SettingsStatusPill tone={canEditSports ? "success" : "locked"}>{canEditSports ? "Editable" : "Locked"}</SettingsStatusPill>}
|
|
className={active ? undefined : "hidden"}
|
|
>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<Label>{selectedSports.size} selected</Label>
|
|
{canEditSports && selectedSports.size > 0 && (
|
|
<Button type="button" variant="ghost" size="sm" onClick={onClearAll}>
|
|
Clear all
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
{displaySportsSeasons.length > 0 ? (
|
|
displaySportsSeasons.map((ss) => {
|
|
const selected = selectedSports.has(ss.id);
|
|
return (
|
|
<label
|
|
key={ss.id}
|
|
htmlFor={`sport-${ss.id}`}
|
|
className={cn(
|
|
"mb-2 flex cursor-pointer items-center gap-3 rounded-md border p-3 text-sm transition-colors last:mb-0",
|
|
selected ? "border-primary bg-primary/10 text-primary" : "border-border bg-background hover:border-primary/50",
|
|
!canEditSports && "cursor-not-allowed opacity-65"
|
|
)}
|
|
>
|
|
<Checkbox
|
|
id={`sport-${ss.id}`}
|
|
name="sportsSeasons"
|
|
value={ss.id}
|
|
checked={selected}
|
|
onCheckedChange={() => onSportToggle(ss.id)}
|
|
disabled={!canEditSports}
|
|
/>
|
|
<SportIcon sport={ss.sport} />
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate font-medium">
|
|
{ss.sport.slug === BRACKT_SLUG ? "Brackt" : `${ss.sport.name} - ${ss.name} (${ss.year})`}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">{ss.scoringType.replace("_", " ")}</span>
|
|
</span>
|
|
</label>
|
|
);
|
|
})
|
|
) : (
|
|
<p className="p-3 text-sm text-muted-foreground">No sports seasons available.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</SettingsSection>
|
|
);
|
|
}
|