brackt/app/components/league/settings/DraftOrderSection.tsx
Chris Parsons b1a1a472f5 Refactor league settings into per-section components (#347)
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>
2026-05-05 14:00:35 -07:00

169 lines
6.3 KiB
TypeScript

import type { Dispatch, SetStateAction } from "react";
import {
closestCenter,
DndContext,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { Form } from "react-router";
import { ListOrdered } from "lucide-react";
import { Button } from "~/components/ui/button";
import { SettingsSection, SettingsStatusPill } from "./SettingsSection";
import { DraftOrderMessage, type SettingsActionData } from "./SettingsMessages";
import { SortableDraftOrderRow } from "./SortableDraftOrderRow";
type DraftOrderTeam = {
id: string;
name: string;
ownerId: string | null;
};
export function DraftOrderSection({
active,
draftOrderSet,
canEditDraftOrder,
hasUnsavedChanges,
draftOrderTeams,
setDraftOrderTeams,
onDirtyChange,
onResetChanges,
teams,
ownerMap,
navigationState,
updateIntent,
actionData,
}: {
active: boolean;
draftOrderSet: boolean;
canEditDraftOrder: boolean;
hasUnsavedChanges: boolean;
draftOrderTeams: string[];
setDraftOrderTeams: Dispatch<SetStateAction<string[]>>;
onDirtyChange: (dirty: boolean) => void;
onResetChanges: () => void;
teams: DraftOrderTeam[];
ownerMap: Record<string, string | null>;
navigationState: "idle" | "loading" | "submitting";
updateIntent: FormDataEntryValue | null | undefined;
actionData: SettingsActionData;
}) {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 6 },
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
return (
<SettingsSection
id="draft-order"
icon={ListOrdered}
title="Draft Order"
description="Drag positions to reorder teams, or randomize the entire draft order. Saved independently from the rest of the settings form."
status={draftOrderSet ? <SettingsStatusPill tone="success">Set</SettingsStatusPill> : undefined}
className={active ? undefined : "hidden"}
>
{!draftOrderSet && (
<div className="space-y-3">
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4">
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
The draft cannot begin until a draft order is set.
</p>
<p className="mt-1 text-sm text-amber-600 dark:text-amber-400">
Randomize the order now, or drag and drop teams into position manually below.
</p>
</div>
{canEditDraftOrder && (
<Form method="post">
<input type="hidden" name="intent" value="randomize-draft-order" />
<Button type="submit" className="w-full" disabled={navigationState === "submitting" && updateIntent === "randomize-draft-order"}>
{navigationState === "submitting" && updateIntent === "randomize-draft-order" ? "Randomizing..." : "Randomize Draft Order"}
</Button>
</Form>
)}
</div>
)}
<div>
<h3 className="mb-3 text-sm font-semibold text-muted-foreground">Manually set draft order</h3>
<Form method="post" className="space-y-3">
<input type="hidden" name="intent" value="set-draft-order" />
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={({ active: activeItem, over }) => {
if (!over || activeItem.id === over.id) return;
setDraftOrderTeams((current) => {
const oldIndex = current.indexOf(String(activeItem.id));
const newIndex = current.indexOf(String(over.id));
if (oldIndex === -1 || newIndex === -1) return current;
onDirtyChange(true);
return arrayMove(current, oldIndex, newIndex);
});
}}
>
<SortableContext items={draftOrderTeams} strategy={verticalListSortingStrategy}>
<div className="space-y-3">
{draftOrderTeams.map((teamId, index) => {
const team = teams.find((t) => t.id === teamId);
if (!team) return null;
return (
<SortableDraftOrderRow
key={teamId}
teamId={teamId}
index={index}
teamName={team.name}
ownerName={team.ownerId ? (ownerMap[team.ownerId] ?? "Unknown") : null}
disabled={!canEditDraftOrder}
/>
);
})}
</div>
</SortableContext>
</DndContext>
{canEditDraftOrder ? (
<div className="rounded-xl border bg-card p-3">
{hasUnsavedChanges && (
<p className="mb-3 text-sm font-medium text-amber-700 dark:text-amber-300">
You have unsaved draft order changes.
</p>
)}
<div className="flex flex-col gap-2 sm:flex-row">
{hasUnsavedChanges && (
<Button type="button" variant="outline" className="sm:w-48" onClick={onResetChanges}>
Reset Changes
</Button>
)}
<Button type="submit" className="flex-1" disabled={navigationState === "submitting" && updateIntent === "set-draft-order"}>
{navigationState === "submitting" && updateIntent === "set-draft-order" ? "Saving..." : "Save Draft Order"}
</Button>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">Draft order cannot be changed after the draft has started.</p>
)}
</Form>
</div>
{draftOrderSet && canEditDraftOrder && (
<Form method="post">
<input type="hidden" name="intent" value="randomize-draft-order" />
<Button type="submit" variant="outline" className="w-full" disabled={navigationState === "submitting" && updateIntent === "randomize-draft-order"}>
{navigationState === "submitting" && updateIntent === "randomize-draft-order" ? "Randomizing..." : "Randomize Draft Order"}
</Button>
</Form>
)}
<DraftOrderMessage actionData={actionData} />
</SettingsSection>
);
}