* 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>
127 lines
4 KiB
TypeScript
127 lines
4 KiB
TypeScript
import type { ComponentType } from "react";
|
|
import type { LucideProps } from "lucide-react";
|
|
import { ArrowLeft, Check } from "lucide-react";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
type SettingsNavSection = {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
export type SettingsGridSection = SettingsNavSection & {
|
|
icon: ComponentType<LucideProps>;
|
|
subtitle: string;
|
|
isComplete: boolean;
|
|
isDanger?: boolean;
|
|
};
|
|
|
|
export function SettingsMobileGridNav({
|
|
sections,
|
|
completedCount,
|
|
onSectionChange,
|
|
}: {
|
|
sections: readonly SettingsGridSection[];
|
|
completedCount: number;
|
|
onSectionChange: (sectionId: string) => void;
|
|
}) {
|
|
return (
|
|
<div className="mb-5 lg:hidden">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Manage
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{completedCount} of {sections.length} set
|
|
</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{sections.map((section) => (
|
|
<button
|
|
key={section.id}
|
|
type="button"
|
|
onClick={() => onSectionChange(section.id)}
|
|
className={cn(
|
|
"relative flex cursor-pointer flex-col gap-3 rounded-xl border bg-card p-4 text-left transition-colors hover:border-primary/40 active:scale-[0.98]",
|
|
section.isDanger && "border-destructive/40"
|
|
)}
|
|
>
|
|
{section.isComplete && (
|
|
<div className="absolute right-3 top-3 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
<Check className="h-3 w-3" strokeWidth={3} />
|
|
</div>
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"flex h-10 w-10 items-center justify-center rounded-lg",
|
|
section.isDanger
|
|
? "bg-destructive/15 text-destructive"
|
|
: "bg-primary/20 text-primary"
|
|
)}
|
|
>
|
|
<section.icon className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold leading-tight">{section.label}</p>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">{section.subtitle}</p>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SettingsMobileSectionPill({
|
|
onShowGrid,
|
|
}: {
|
|
onShowGrid: () => void;
|
|
}) {
|
|
return (
|
|
<div className="mb-5 lg:hidden">
|
|
<button
|
|
type="button"
|
|
onClick={onShowGrid}
|
|
className="flex cursor-pointer items-center gap-1.5 rounded-full border bg-card px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
|
>
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
Back to all settings
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SettingsDesktopNav({
|
|
sections,
|
|
activeSection,
|
|
onSectionChange,
|
|
}: {
|
|
sections: readonly SettingsGridSection[];
|
|
activeSection: string;
|
|
onSectionChange: (sectionId: string) => void;
|
|
}) {
|
|
return (
|
|
<aside className="hidden lg:block">
|
|
<div className="sticky top-6 rounded-xl border bg-card p-3">
|
|
<p className="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Manage
|
|
</p>
|
|
<nav className="space-y-1">
|
|
{sections.map((section) => (
|
|
<button
|
|
key={section.id}
|
|
type="button"
|
|
onClick={() => onSectionChange(section.id)}
|
|
className={cn(
|
|
"flex w-full cursor-pointer items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
activeSection === section.id && "bg-muted text-foreground"
|
|
)}
|
|
>
|
|
<section.icon className={cn("h-4 w-4 shrink-0", section.isDanger && "text-destructive")} />
|
|
{section.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|