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>
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
import { Trophy } from "lucide-react";
|
|
import { Input } from "~/components/ui/input";
|
|
import { Label } from "~/components/ui/label";
|
|
import { Button } from "~/components/ui/button";
|
|
import { StepperInput } from "~/components/league/StepperInput";
|
|
import { SettingsSection, SettingsStatusPill } from "./SettingsSection";
|
|
|
|
export function LeagueBasicsSection({
|
|
active,
|
|
season,
|
|
leagueName,
|
|
onLeagueNameChange,
|
|
isPublicDraftBoard,
|
|
onIsPublicDraftBoardChange,
|
|
teamCountValue,
|
|
onTeamCountValueChange,
|
|
teamsWithOwners,
|
|
canEditTeamCount,
|
|
minTeamCount,
|
|
inviteUrl,
|
|
copied,
|
|
onCopyInviteLink,
|
|
}: {
|
|
active: boolean;
|
|
season: { inviteCode: string | null } | null;
|
|
leagueName: string;
|
|
onLeagueNameChange: (v: string) => void;
|
|
isPublicDraftBoard: boolean;
|
|
onIsPublicDraftBoardChange: (v: boolean) => void;
|
|
teamCountValue: number;
|
|
onTeamCountValueChange: (v: number) => void;
|
|
teamsWithOwners: number;
|
|
canEditTeamCount: boolean;
|
|
minTeamCount: number;
|
|
inviteUrl: string;
|
|
copied: boolean;
|
|
onCopyInviteLink: () => void;
|
|
}) {
|
|
return (
|
|
<SettingsSection
|
|
id="league-basics"
|
|
icon={Trophy}
|
|
title="League Basics"
|
|
description="High-level league identity and public visibility controls."
|
|
status={<SettingsStatusPill tone="success">Always editable</SettingsStatusPill>}
|
|
className={active ? undefined : "hidden"}
|
|
>
|
|
<input type="hidden" name="teamCount" value={teamCountValue} />
|
|
<div className="space-y-5">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">League Name</Label>
|
|
<Input
|
|
id="name"
|
|
name="name"
|
|
type="text"
|
|
value={leagueName}
|
|
onChange={(e) => onLeagueNameChange(e.target.value)}
|
|
placeholder="Enter league name"
|
|
required
|
|
minLength={3}
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
<label
|
|
htmlFor="isPublicDraftBoard"
|
|
className="flex cursor-pointer items-start gap-3 rounded-lg border p-4"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
id="isPublicDraftBoard"
|
|
name="isPublicDraftBoard"
|
|
checked={isPublicDraftBoard}
|
|
onChange={(e) => onIsPublicDraftBoardChange(e.target.checked)}
|
|
className="mt-1 h-4 w-4 rounded border-border"
|
|
/>
|
|
<span>
|
|
<span className="block font-medium">Public draft board</span>
|
|
<span className="text-sm text-muted-foreground">Allow anyone with the link to view the board without logging in.</span>
|
|
</span>
|
|
</label>
|
|
<div className="rounded-lg border p-4">
|
|
<div className="mb-3">
|
|
<Label>Number of Teams</Label>
|
|
<p className="text-sm text-muted-foreground">Minimum {minTeamCount} based on teams with owners.</p>
|
|
</div>
|
|
<StepperInput
|
|
value={teamCountValue}
|
|
min={canEditTeamCount ? minTeamCount : teamCountValue}
|
|
max={canEditTeamCount ? 16 : teamCountValue}
|
|
onChange={onTeamCountValueChange}
|
|
decrementLabel="Decrease team count"
|
|
incrementLabel="Increase team count"
|
|
/>
|
|
{!canEditTeamCount && (
|
|
<p className="mt-3 text-sm text-muted-foreground">Locked after the draft starts.</p>
|
|
)}
|
|
</div>
|
|
{season?.inviteCode && (
|
|
<div className="rounded-lg border p-4">
|
|
<div className="mb-3">
|
|
<h3 className="font-medium">Invite Link</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{/* eslint-disable-next-line no-nested-ternary */}
|
|
{(() => {
|
|
const open = teamCountValue - teamsWithOwners;
|
|
return open > 0
|
|
? `${open} open spot${open === 1 ? "" : "s"} available.`
|
|
: "No open team spots right now.";
|
|
})()}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
<Input readOnly value={inviteUrl} onClick={(e) => e.currentTarget.select()} className="text-sm" />
|
|
<Button type="button" variant={copied ? "default" : "outline"} onClick={onCopyInviteLink}>
|
|
{copied ? "Copied" : "Copy"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SettingsSection>
|
|
);
|
|
}
|