* 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> * Fix Stop hook loop: suppress output on typecheck success The Stop hook was producing stdout on every run, causing Claude Code to feed it back as context and rewake the model each turn. Now emits a systemMessage JSON only on failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Typescript fix * Remove type asserting --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|