brackt/app/routes/leagues/$leagueId.settings.tsx
Chris Parsons 0ba2b391f8
Fix draft order initialization for teams added mid-season (#449)
* Fix draft order showing only new team when league size increased before order was set

When a commissioner increased the team count on a league that had teams
but no draft order set yet, the server created draft slots only for the
newly-added teams. This left the DB with N+K teams but only K slots,
causing the drag-and-drop list to display only the K new teams.

Two fixes:
1. Server: only append new draft slots when an order was already set
   (existingSlots.length > 0). If no order exists yet, skip slot creation
   so the page correctly treats the order as unset for all teams.
2. Frontend: buildDraftOrderTeams() appends any unslotted teams after the
   slotted ones, so a partially-corrupt DB state still shows all teams.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Address code review feedback on draft order bug fix

- Move buildDraftOrderTeams to app/lib/draft-order.ts so it is importable
  and testable; remove the local copy from the settings component
- Add unit tests for buildDraftOrderTeams covering the empty, full, and
  partial-slot cases
- Tighten the draft slot guard from existingSlots.length > 0 to
  existingSlots.length === currentTeamCount so partial legacy states are
  treated the same as "order not set"
- Condense the 3-line server comment to a single line per project style
- Rename getNumTeamsInSeason → getNumDraftSlotsBySeasonId to reflect what
  the function actually counts, and update its one call site
- Add tests for the server-side slot-creation guard logic

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Fix lint: move shouldAppendDraftSlots to outer scope

oxlint (consistent-function-scoping) requires functions that don't close
over any variables to be defined at the outer scope rather than inside a
describe block.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 18:22:47 -07:00

513 lines
23 KiB
TypeScript

import { useState, useEffect, useCallback } from "react";
import { Form, useBeforeUnload, useBlocker, useLocation, useNavigation, type BlockerFunction } from "react-router";
import { format } from "date-fns";
import {
AlertTriangle,
Bell,
ListOrdered,
Shield,
Swords,
TrendingUp,
Trophy,
Users,
} from "lucide-react";
import { type ScoringRules, DEFAULT_SCORING_RULES } from "~/lib/scoring-types";
import { getInitialDraftSpeed } from "~/lib/draft-timer";
import { buildDraftOrderTeams } from "~/lib/draft-order";
import type { Route } from "./+types/$leagueId.settings";
export { action, loader } from "./$leagueId.settings.server";
import { OMNIFANTASY_SCORING } from "~/components/league/ScoringPresetPicker";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
import { SettingsMessage } from "~/components/league/settings/SettingsMessages";
import { LeagueSettingsHeader } from "~/components/league/settings/LeagueSettingsHeader";
import {
SettingsDesktopNav,
SettingsMobileGridNav,
SettingsMobileSectionPill,
type SettingsGridSection,
} from "~/components/league/settings/SettingsNavigation";
import { SettingsSaveBar } from "~/components/league/settings/SettingsSaveBar";
import { LeaveSettingsDialog, SwitchSettingsSectionDialog } from "~/components/league/settings/UnsavedChangesDialogs";
import { DraftOrderSection } from "~/components/league/settings/DraftOrderSection";
import { LeagueBasicsSection } from "~/components/league/settings/LeagueBasicsSection";
import { DraftSetupSection } from "~/components/league/settings/DraftSetupSection";
import { SportsSection } from "~/components/league/settings/SportsSection";
import { ScoringSection } from "~/components/league/settings/ScoringSection";
import { NotificationsSection } from "~/components/league/settings/NotificationsSection";
import { PeopleSection } from "~/components/league/settings/PeopleSection";
import { HistoryDangerSection } from "~/components/league/settings/HistoryDangerSection";
import { toast } from "sonner";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `Settings — ${data?.league?.name ?? "League"} - Brackt` }];
}
const BRACKT_SLUG = "brackt";
type LoaderSeason = Route.ComponentProps["loaderData"]["season"];
type LoaderSportsSeason = Route.ComponentProps["loaderData"]["allSportsSeasons"][number] & {
fantasySeasonId?: string | null;
};
function getInitialScoringPreset(season: LoaderSeason) {
const rules: ScoringRules = {
pointsFor1st: season?.pointsFor1st ?? DEFAULT_SCORING_RULES.pointsFor1st,
pointsFor2nd: season?.pointsFor2nd ?? DEFAULT_SCORING_RULES.pointsFor2nd,
pointsFor3rd: season?.pointsFor3rd ?? DEFAULT_SCORING_RULES.pointsFor3rd,
pointsFor4th: season?.pointsFor4th ?? DEFAULT_SCORING_RULES.pointsFor4th,
pointsFor5th: season?.pointsFor5th ?? DEFAULT_SCORING_RULES.pointsFor5th,
pointsFor6th: season?.pointsFor6th ?? DEFAULT_SCORING_RULES.pointsFor6th,
pointsFor7th: season?.pointsFor7th ?? DEFAULT_SCORING_RULES.pointsFor7th,
pointsFor8th: season?.pointsFor8th ?? DEFAULT_SCORING_RULES.pointsFor8th,
};
if (Object.entries(DEFAULT_SCORING_RULES).every(([k, v]) => rules[k as keyof ScoringRules] === v)) return "brackt" as const;
if (Object.entries(OMNIFANTASY_SCORING).every(([k, v]) => rules[k as keyof ScoringRules] === v)) return "omnifantasy" as const;
return "custom" as const;
}
function hasFantasySeasonId(season: object): boolean {
return "fantasySeasonId" in season && typeof season.fantasySeasonId === "string" && season.fantasySeasonId.length > 0;
}
function findTemplateBrackt(seasons: LoaderSportsSeason[]): LoaderSportsSeason | undefined {
return seasons.find((ss) => ss.sport.slug === BRACKT_SLUG && !hasFantasySeasonId(ss));
}
function getInitialSelectedSports({
season,
allSportsSeasons,
}: {
season: LoaderSeason;
allSportsSeasons: LoaderSportsSeason[];
}) {
const linkedSeasons = season?.seasonSports?.map((s) => s.sportsSeason) ?? [];
const linkedIds = linkedSeasons.map((ss) => ss.id);
const templateBrackt = findTemplateBrackt(allSportsSeasons);
const privateBrackt = linkedSeasons.find((ss) => ss.sport.slug === BRACKT_SLUG && hasFantasySeasonId(ss));
if (templateBrackt && privateBrackt) {
return new Set(linkedIds.map((id) => (id === privateBrackt.id ? templateBrackt.id : id)));
}
return new Set(linkedIds);
}
const SETTINGS_SECTIONS = [
{ id: "league-basics", label: "League Basics" },
{ id: "draft-setup", label: "Draft Settings" },
{ id: "draft-order", label: "Draft Order" },
{ id: "sports", label: "Sports" },
{ id: "scoring", label: "Scoring" },
{ id: "notifications", label: "Notifications" },
{ id: "people", label: "People" },
{ id: "history-danger", label: "History & Danger" },
] as const;
type SettingsSectionId = (typeof SETTINGS_SECTIONS)[number]["id"];
const UPDATE_SECTIONS: SettingsSectionId[] = [
"league-basics",
"draft-setup",
"sports",
"scoring",
"notifications",
];
export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) {
const {
league, season, teams, teamCount, teamsWithOwners, allSportsSeasons,
draftSlots, isAdmin, leagueMembers, ownerMap, commissioners, currentUserId, commishTimezone,
} = loaderData;
const navigation = useNavigation();
const location = useLocation();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [mobileView, setMobileView] = useState<"grid" | "section">("grid");
const [activeSection, setActiveSection] = useState<SettingsSectionId>("league-basics");
const [pendingSection, setPendingSection] = useState<SettingsSectionId | null>(null);
// Two separate dirty flags: one for the multi-section settings form, one for draft order
const [hasUnsavedSettingsChanges, setHasUnsavedSettingsChanges] = useState(false);
const [hasDraftOrderChanges, setHasDraftOrderChanges] = useState(false);
// Settings form state
const [leagueName, setLeagueName] = useState(league.name);
const [isPublicDraftBoard, setIsPublicDraftBoard] = useState(league.isPublicDraftBoard);
const [discordWebhookUrl, setDiscordWebhookUrl] = useState(league.discordWebhookUrl ?? "");
const [teamCountValue, setTeamCountValue] = useState<number>(teamCount);
const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock");
const [overnightMode, setOvernightMode] = useState<"none" | "league" | "per_user">(season?.overnightPauseMode ?? "none");
const [overnightStart, setOvernightStart] = useState(season?.overnightPauseStart ?? "23:00");
const [overnightEnd, setOvernightEnd] = useState(season?.overnightPauseEnd ?? "07:00");
const [overnightTimezone, setOvernightTimezone] = useState(season?.overnightPauseTimezone ?? commishTimezone ?? "");
const [scoringPreset, setScoringPreset] = useState<"brackt" | "omnifantasy" | "custom">(() => getInitialScoringPreset(season));
const [scoringRules, setScoringRules] = useState<ScoringRules>({
pointsFor1st: season?.pointsFor1st ?? DEFAULT_SCORING_RULES.pointsFor1st,
pointsFor2nd: season?.pointsFor2nd ?? DEFAULT_SCORING_RULES.pointsFor2nd,
pointsFor3rd: season?.pointsFor3rd ?? DEFAULT_SCORING_RULES.pointsFor3rd,
pointsFor4th: season?.pointsFor4th ?? DEFAULT_SCORING_RULES.pointsFor4th,
pointsFor5th: season?.pointsFor5th ?? DEFAULT_SCORING_RULES.pointsFor5th,
pointsFor6th: season?.pointsFor6th ?? DEFAULT_SCORING_RULES.pointsFor6th,
pointsFor7th: season?.pointsFor7th ?? DEFAULT_SCORING_RULES.pointsFor7th,
pointsFor8th: season?.pointsFor8th ?? DEFAULT_SCORING_RULES.pointsFor8th,
});
const [draftSpeed, setDraftSpeed] = useState(() => getInitialDraftSpeed(season));
const [selectedSports, setSelectedSports] = useState<Set<string>>(
() => getInitialSelectedSports({ season, allSportsSeasons })
);
const [draftRounds, setDraftRounds] = useState<number>(season?.draftRounds || 20);
const [draftDate, setDraftDate] = useState<Date | undefined>(
season?.draftDateTime ? new Date(season.draftDateTime) : undefined
);
const [draftTime, setDraftTime] = useState<string>(
season?.draftDateTime ? format(new Date(season.draftDateTime), "HH:mm") : ""
);
const [autoStartDraft, setAutoStartDraft] = useState<boolean>(season?.autoStartDraft ?? false);
// Draft order state (separate form, separate dirty flag)
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
() => buildDraftOrderTeams(draftSlots, teams)
);
const [copied, setCopied] = useState(false);
// Sync draft order from loader after randomization or other server updates
useEffect(() => {
setDraftOrderTeams(buildDraftOrderTeams(draftSlots, teams));
setHasDraftOrderChanges(false);
}, [draftSlots, teams]);
// Re-mark dirty if the settings save returned an error. Guard against non-settings errors
// (e.g. draft-order) which carry a `section` field.
useEffect(() => {
if (navigation.state !== "idle") return;
if (actionData && "error" in actionData && !("section" in actionData)) {
setHasUnsavedSettingsChanges(true);
}
}, [actionData, navigation.state]);
useEffect(() => {
if (navigation.state !== "idle") return;
if (actionData && "intent" in actionData && actionData.intent === "update" && actionData.success) {
toast.success(actionData.message ?? "Settings saved!");
setHasUnsavedSettingsChanges(false);
}
}, [actionData, navigation.state]);
const canEditSports = season?.status === "pre_draft";
const canEditTeamCount = season?.status === "pre_draft";
const canEditDraftOrder = season?.status === "pre_draft";
const canEditDraftRounds = season?.status === "pre_draft";
const minTeamCount = Math.max(6, teamsWithOwners);
const sportsCount = selectedSports.size;
const minRounds = sportsCount;
const recommendedRounds = Math.ceil(sportsCount * 1.25);
const flexSpots = Math.max(0, draftRounds - sportsCount);
const updateIntent = navigation.formData?.get("intent");
const draftOrderSet = draftSlots.length === teams.length && teams.length > 0;
const showUpdateForm = UPDATE_SECTIONS.includes(activeSection);
const isSubmittingSettings = navigation.state === "submitting" && updateIntent === "update";
const isSubmittingDraftOrder = navigation.state === "submitting" && updateIntent === "set-draft-order";
const shouldWarnUnsaved = (hasUnsavedSettingsChanges || hasDraftOrderChanges) && !isSubmittingSettings && !isSubmittingDraftOrder;
const blocker = useBlocker(
useCallback<BlockerFunction>(
({ nextLocation }) => shouldWarnUnsaved && nextLocation.pathname !== location.pathname,
[shouldWarnUnsaved, location.pathname]
)
);
const inviteUrl = season?.inviteCode
? `${typeof window !== "undefined" ? window.location.origin : ""}/i/${season.inviteCode}`
: "";
const displaySportsSeasons = (() => {
const templateBrackt = findTemplateBrackt(allSportsSeasons);
if (!templateBrackt) return allSportsSeasons;
return allSportsSeasons.filter((ss) => !(ss.sport.slug === BRACKT_SLUG && hasFantasySeasonId(ss)));
})();
useBeforeUnload(
(event) => {
if (!shouldWarnUnsaved) return;
event.preventDefault();
event.returnValue = "";
},
{ capture: true }
);
const markDirty = () => setHasUnsavedSettingsChanges(true);
const resetSettingsFormState = () => {
setLeagueName(league.name);
setIsPublicDraftBoard(league.isPublicDraftBoard);
setDiscordWebhookUrl(league.discordWebhookUrl ?? "");
setTeamCountValue(teamCount);
setTimerMode(season?.draftTimerMode ?? "chess_clock");
setDraftSpeed(getInitialDraftSpeed(season));
setOvernightMode(season?.overnightPauseMode ?? "none");
setOvernightStart(season?.overnightPauseStart ?? "23:00");
setOvernightEnd(season?.overnightPauseEnd ?? "07:00");
setOvernightTimezone(season?.overnightPauseTimezone ?? commishTimezone ?? "");
setScoringPreset(getInitialScoringPreset(season));
setScoringRules({
pointsFor1st: season?.pointsFor1st ?? DEFAULT_SCORING_RULES.pointsFor1st,
pointsFor2nd: season?.pointsFor2nd ?? DEFAULT_SCORING_RULES.pointsFor2nd,
pointsFor3rd: season?.pointsFor3rd ?? DEFAULT_SCORING_RULES.pointsFor3rd,
pointsFor4th: season?.pointsFor4th ?? DEFAULT_SCORING_RULES.pointsFor4th,
pointsFor5th: season?.pointsFor5th ?? DEFAULT_SCORING_RULES.pointsFor5th,
pointsFor6th: season?.pointsFor6th ?? DEFAULT_SCORING_RULES.pointsFor6th,
pointsFor7th: season?.pointsFor7th ?? DEFAULT_SCORING_RULES.pointsFor7th,
pointsFor8th: season?.pointsFor8th ?? DEFAULT_SCORING_RULES.pointsFor8th,
});
setSelectedSports(getInitialSelectedSports({ season, allSportsSeasons }));
setDraftRounds(season?.draftRounds || 20);
setDraftDate(season?.draftDateTime ? new Date(season.draftDateTime) : undefined);
setDraftTime(season?.draftDateTime ? format(new Date(season.draftDateTime), "HH:mm") : "");
setAutoStartDraft(season?.autoStartDraft ?? false);
setHasUnsavedSettingsChanges(false);
};
const resetDraftOrder = () => {
setDraftOrderTeams(
draftSlots.length > 0 ? draftSlots.map((s) => s.teamId) : teams.map((t) => t.id)
);
setHasDraftOrderChanges(false);
};
const handleSectionChange = (sectionId: SettingsSectionId, { mobile = false } = {}) => {
if (sectionId === activeSection && !mobile) return;
if (hasUnsavedSettingsChanges) {
setPendingSection(sectionId);
return;
}
setActiveSection(sectionId);
if (mobile) setMobileView("section");
};
const discardChangesAndSwitchSection = () => {
if (!pendingSection) return;
resetSettingsFormState();
setActiveSection(pendingSection);
setPendingSection(null);
setMobileView("section");
};
const handleCopyInviteLink = async () => {
if (!season?.inviteCode) return;
try {
await navigator.clipboard.writeText(inviteUrl);
setCopied(true);
toast.success("Invite link copied to clipboard!");
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error("Failed to copy invite link");
}
};
const handleSportToggle = (sportId: string) => {
setSelectedSports((prev) => {
const next = new Set(prev);
if (next.has(sportId)) {
next.delete(sportId);
} else {
next.add(sportId);
}
return next;
});
};
const sectionGridData: SettingsGridSection[] = [
{ id: "league-basics", label: "League Basics", icon: Shield, subtitle: "Name, teams, join" },
{ id: "draft-setup", label: "Draft Settings", icon: Swords, subtitle: "Format, timing, rounds" },
{ id: "draft-order", label: "Draft Order", icon: ListOrdered, subtitle: "Pick order, randomize" },
{ id: "sports", label: "Sports", icon: Trophy, subtitle: `${selectedSports.size} season${selectedSports.size !== 1 ? "s" : ""} selected` },
{ id: "scoring", label: "Scoring", icon: TrendingUp, subtitle: "Place values, ties" },
{ id: "notifications", label: "Notifications", icon: Bell, subtitle: "Discord alerts" },
{ id: "people", label: "People", icon: Users, subtitle: `${teamsWithOwners} of ${teamCountValue} teams` },
{ id: "history-danger", label: "History & Danger", icon: AlertTriangle, subtitle: "Audit log, dissolve", isDanger: true },
];
return (
<div className="container mx-auto max-w-5xl px-4 py-6 sm:py-8">
<div className={cn(mobileView === "section" && "hidden lg:block")}>
<LeagueSettingsHeader leagueId={league.id} leagueName={league.name} />
</div>
{mobileView === "grid" && (
<div className="lg:hidden">
<SettingsMobileGridNav
sections={sectionGridData}
onSectionChange={(id) => handleSectionChange(id as SettingsSectionId, { mobile: true })}
/>
</div>
)}
{mobileView === "section" && (
<SettingsMobileSectionPill onShowGrid={() => setMobileView("grid")} />
)}
<div className={cn("grid gap-6 lg:grid-cols-[220px_minmax(0,1fr)]", mobileView === "grid" && "hidden lg:grid")}>
<SettingsDesktopNav
sections={sectionGridData}
activeSection={activeSection}
onSectionChange={(id) => handleSectionChange(id as SettingsSectionId)}
/>
<main className="space-y-6">
{showUpdateForm && (
<Form method="post" noValidate className="space-y-6" onSubmit={() => setHasUnsavedSettingsChanges(false)}>
<input type="hidden" name="intent" value="update" />
<LeagueBasicsSection
active={activeSection === "league-basics"}
season={season ?? null}
leagueName={leagueName}
onLeagueNameChange={(v) => { setLeagueName(v); markDirty(); }}
isPublicDraftBoard={isPublicDraftBoard}
onIsPublicDraftBoardChange={(v) => { setIsPublicDraftBoard(v); markDirty(); }}
teamCountValue={teamCountValue}
onTeamCountValueChange={(v) => { setTeamCountValue(v); markDirty(); }}
teamsWithOwners={teamsWithOwners}
canEditTeamCount={canEditTeamCount}
minTeamCount={minTeamCount}
inviteUrl={inviteUrl}
copied={copied}
onCopyInviteLink={handleCopyInviteLink}
/>
<DraftSetupSection
active={activeSection === "draft-setup"}
canEditDraftRounds={canEditDraftRounds}
minRounds={minRounds}
recommendedRounds={recommendedRounds}
flexSpots={flexSpots}
draftRounds={draftRounds}
onDraftRoundsChange={(v) => { setDraftRounds(v); markDirty(); }}
draftDate={draftDate}
onDraftDateChange={(v) => { setDraftDate(v); markDirty(); }}
draftTime={draftTime}
onDraftTimeChange={(v) => { setDraftTime(v); markDirty(); }}
autoStartDraft={autoStartDraft}
onAutoStartDraftChange={(v) => { setAutoStartDraft(v); markDirty(); }}
timerMode={timerMode}
onTimerModeChange={(v) => { setTimerMode(v); markDirty(); }}
draftSpeed={draftSpeed}
onDraftSpeedChange={(v) => { setDraftSpeed(v); markDirty(); }}
overnightMode={overnightMode}
onOvernightModeChange={(v) => { setOvernightMode(v); markDirty(); }}
overnightStart={overnightStart}
onOvernightStartChange={(v) => { setOvernightStart(v); markDirty(); }}
overnightEnd={overnightEnd}
onOvernightEndChange={(v) => { setOvernightEnd(v); markDirty(); }}
overnightTimezone={overnightTimezone}
onOvernightTimezoneChange={(v) => { setOvernightTimezone(v); markDirty(); }}
commishTimezone={commishTimezone}
/>
<SportsSection
active={activeSection === "sports"}
canEditSports={canEditSports}
selectedSports={selectedSports}
displaySportsSeasons={displaySportsSeasons}
onSportToggle={(id) => { handleSportToggle(id); markDirty(); }}
onClearAll={() => { setSelectedSports(new Set()); markDirty(); }}
/>
<ScoringSection
active={activeSection === "scoring"}
canEditSports={canEditSports}
season={season ?? null}
scoringPreset={scoringPreset}
onScoringPresetChange={(v) => { setScoringPreset(v); markDirty(); }}
scoringRules={scoringRules}
onScoringRulesChange={(v) => { setScoringRules(v); markDirty(); }}
/>
<NotificationsSection
active={activeSection === "notifications"}
savedDiscordWebhookUrl={league.discordWebhookUrl}
discordWebhookUrl={discordWebhookUrl}
onDiscordWebhookUrlChange={(v) => { setDiscordWebhookUrl(v); markDirty(); }}
/>
<SettingsMessage actionData={actionData} />
<SettingsSaveBar
hasUnsavedChanges={hasUnsavedSettingsChanges}
isSubmitting={isSubmittingSettings}
onReset={resetSettingsFormState}
/>
</Form>
)}
<LeaveSettingsDialog blocker={blocker} />
<SwitchSettingsSectionDialog
open={pendingSection !== null}
onOpenChange={(open) => { if (!open) setPendingSection(null); }}
onStay={() => setPendingSection(null)}
onDiscard={discardChangesAndSwitchSection}
/>
<DraftOrderSection
active={activeSection === "draft-order"}
draftOrderSet={draftOrderSet}
canEditDraftOrder={canEditDraftOrder}
hasUnsavedChanges={hasDraftOrderChanges}
draftOrderTeams={draftOrderTeams}
setDraftOrderTeams={setDraftOrderTeams}
onDirtyChange={setHasDraftOrderChanges}
onResetChanges={resetDraftOrder}
teams={teams}
ownerMap={ownerMap}
navigationState={navigation.state}
updateIntent={updateIntent}
actionData={actionData}
/>
<PeopleSection
active={activeSection === "people"}
teams={teams}
ownerMap={ownerMap}
commissioners={commissioners}
currentUserId={currentUserId}
teamCountValue={teamCountValue}
teamsWithOwners={teamsWithOwners}
leagueMembers={leagueMembers}
isAdmin={isAdmin}
navigationState={navigation.state}
/>
{league.discordWebhookUrl && activeSection === "notifications" && (
<Form method="post" className="rounded-lg border bg-card p-4">
<input type="hidden" name="intent" value="test-discord-webhook" />
<input type="hidden" name="webhookUrl" value={league.discordWebhookUrl} />
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-medium">Test Discord notification</p>
<p className="text-sm text-muted-foreground">Sends a sample standings update to the saved webhook.</p>
</div>
<Button
type="submit"
variant="outline"
disabled={navigation.state === "submitting" && updateIntent === "test-discord-webhook"}
>
Send Test
</Button>
</div>
</Form>
)}
<HistoryDangerSection
active={activeSection === "history-danger"}
league={{ id: league.id, name: league.name }}
season={season ?? null}
isAdmin={isAdmin}
isDeleteDialogOpen={isDeleteDialogOpen}
onDeleteDialogOpenChange={setIsDeleteDialogOpen}
navigationState={navigation.state}
/>
</main>
</div>
</div>
);
}