* Fix mobile league settings: prevent sports scroll and remove completion checkmarks - Add overflow-hidden to sport label text span so flex truncation is properly contained - Add min-w-0 to SettingsSection header inner flex item to prevent title/description overflow - Remove completion check marks from mobile grid nav buttons (league settings has no completion concept) - Remove "X of Y set" counter from mobile nav header - Remove isComplete field from SettingsGridSection type and all data https://claude.ai/code/session_01T7iTb9YZLuWJFtKV753tdB * Fix race window in user creation and restore FlagSvg safety fallback auth.server.ts: switch generateId to application-generated UUIDs so the ID is known before the insert, then move flagConfig assignment from create.after (a separate UPDATE) to create.before so it lands in the initial INSERT with no race window. FlagSvg.tsx: validate the config with parseFlagConfig before rendering; corrupt or missing data falls back to a neutral gray triband flag rather than silently rendering blank SVG shapes. https://claude.ai/code/session_01T7iTb9YZLuWJFtKV753tdB --------- Co-authored-by: Claude <noreply@anthropic.com>
502 lines
22 KiB
TypeScript
502 lines
22 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Form, useBeforeUnload, useBlocker, useNavigation } 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 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 [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 initTimerMode = season?.draftTimerMode ?? "chess_clock";
|
|
const [draftSpeed, setDraftSpeed] = useState(() => {
|
|
if (initTimerMode === "standard") return season?.draftIncrementTime?.toString() ?? "90";
|
|
if (season?.draftInitialTime === 60 && season?.draftIncrementTime === 10) return "fast";
|
|
if (season?.draftInitialTime === 120 && season?.draftIncrementTime === 15) return "standard";
|
|
if (season?.draftInitialTime === 28800 && season?.draftIncrementTime === 3600) return "slow";
|
|
if (season?.draftInitialTime === 43200 && season?.draftIncrementTime === 3600) return "very-slow";
|
|
return "standard";
|
|
});
|
|
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") : ""
|
|
);
|
|
|
|
// Draft order state (separate form, separate dirty flag)
|
|
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
|
|
draftSlots.length > 0 ? draftSlots.map((slot) => slot.teamId) : teams.map((team) => team.id)
|
|
);
|
|
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
// Sync draft order from loader after randomization or other server updates
|
|
useEffect(() => {
|
|
const savedOrder = draftSlots.length > 0
|
|
? draftSlots.map((slot) => slot.teamId)
|
|
: teams.map((team) => team.id);
|
|
setDraftOrderTeams(savedOrder);
|
|
setHasDraftOrderChanges(false);
|
|
}, [draftSlots, teams]);
|
|
|
|
// Clear settings dirty flag after a successful (non-error) submission
|
|
useEffect(() => {
|
|
if (!hasUnsavedSettingsChanges || navigation.state !== "idle") return;
|
|
if (actionData && !("error" in actionData)) {
|
|
setHasUnsavedSettingsChanges(false);
|
|
}
|
|
}, [actionData, hasUnsavedSettingsChanges, 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(shouldWarnUnsaved);
|
|
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");
|
|
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") : "");
|
|
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">
|
|
<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(); }}
|
|
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>
|
|
);
|
|
}
|