* 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>
156 lines
6.3 KiB
TypeScript
156 lines
6.3 KiB
TypeScript
import { Crown, Users } from "lucide-react";
|
|
import { Form } from "react-router";
|
|
import { Button } from "~/components/ui/button";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "~/components/ui/select";
|
|
import { SettingsSection, SettingsStatusPill } from "./SettingsSection";
|
|
|
|
type Team = { id: string; name: string; ownerId: string | null };
|
|
type Commissioner = { id: string; userId: string; userName: string };
|
|
type LeagueMember = { id: string; name: string | null };
|
|
|
|
export function PeopleSection({
|
|
active,
|
|
teams,
|
|
ownerMap,
|
|
commissioners,
|
|
currentUserId,
|
|
teamCountValue,
|
|
teamsWithOwners,
|
|
leagueMembers,
|
|
isAdmin,
|
|
navigationState,
|
|
}: {
|
|
active: boolean;
|
|
teams: Team[];
|
|
ownerMap: Record<string, string | null>;
|
|
commissioners: Commissioner[];
|
|
currentUserId: string;
|
|
teamCountValue: number;
|
|
teamsWithOwners: number;
|
|
leagueMembers: LeagueMember[];
|
|
isAdmin: boolean;
|
|
navigationState: "idle" | "loading" | "submitting";
|
|
}) {
|
|
return (
|
|
<SettingsSection
|
|
id="people"
|
|
icon={Users}
|
|
title="People"
|
|
description="Manage team owners and commissioners. Team capacity is saved from League Basics."
|
|
status={<SettingsStatusPill tone="default">{teamsWithOwners}/{teamCountValue} filled</SettingsStatusPill>}
|
|
className={active ? undefined : "hidden"}
|
|
>
|
|
<div className="space-y-3">
|
|
{teams.map((team) => {
|
|
const ownerName = team.ownerId ? ownerMap[team.ownerId] : null;
|
|
return (
|
|
<div key={team.id} className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="min-w-0">
|
|
<p className="truncate font-medium">{team.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{team.ownerId ? `Owner: ${ownerName || "Unknown"}` : "No owner"}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
{team.ownerId && (
|
|
<Form method="post">
|
|
<input type="hidden" name="intent" value="remove-team-owner" />
|
|
<input type="hidden" name="teamId" value={team.id} />
|
|
<Button type="submit" variant="outline" size="sm" disabled={navigationState === "submitting"}>
|
|
Remove Owner
|
|
</Button>
|
|
</Form>
|
|
)}
|
|
{isAdmin && (
|
|
<Form method="post" className="flex flex-col gap-2 sm:flex-row">
|
|
<input type="hidden" name="intent" value="assign-team-owner" />
|
|
<input type="hidden" name="teamId" value={team.id} />
|
|
<Select name="userId" required>
|
|
<SelectTrigger className="h-9 sm:w-[190px]">
|
|
<SelectValue placeholder="Select user" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{leagueMembers.map((member) => {
|
|
const userOwnsTeam = teams.some((t) => t.ownerId === member.id);
|
|
return (
|
|
<SelectItem key={member.id} value={member.id} disabled={userOwnsTeam}>
|
|
{member.name || "Unknown"}
|
|
{userOwnsTeam && " (already has team)"}
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button type="submit" variant="outline" size="sm" disabled={navigationState === "submitting"}>
|
|
Assign
|
|
</Button>
|
|
</Form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="border-t pt-5">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<Crown className="h-4 w-4 text-muted-foreground" />
|
|
<h3 className="font-semibold">Commissioners</h3>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{commissioners.map((commissioner) => (
|
|
<div key={commissioner.id} className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<p className="font-medium">
|
|
{commissioner.userName}
|
|
{commissioner.userId === currentUserId && <span className="ml-2 text-xs text-muted-foreground">(you)</span>}
|
|
</p>
|
|
{!teams.some((t) => t.ownerId === commissioner.userId) && (
|
|
<p className="text-xs text-muted-foreground">No team in current season</p>
|
|
)}
|
|
</div>
|
|
{commissioners.length > 1 && commissioner.userId !== currentUserId && (
|
|
<Form method="post">
|
|
<input type="hidden" name="intent" value="remove-commissioner" />
|
|
<input type="hidden" name="commissionerUserId" value={commissioner.userId} />
|
|
<Button type="submit" variant="outline" size="sm" disabled={navigationState === "submitting"}>
|
|
Remove
|
|
</Button>
|
|
</Form>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Form method="post" className="mt-4 flex flex-col gap-2 sm:flex-row">
|
|
<input type="hidden" name="intent" value="add-commissioner" />
|
|
<Select name="userId" required>
|
|
<SelectTrigger className="flex-1">
|
|
<SelectValue placeholder="Add commissioner from league members" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{leagueMembers.map((member) => {
|
|
const alreadyCommissioner = commissioners.some((c) => c.userId === member.id);
|
|
return (
|
|
<SelectItem key={member.id} value={member.id} disabled={alreadyCommissioner}>
|
|
{member.name || "Unknown"}
|
|
{alreadyCommissioner && " (already commissioner)"}
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button type="submit" variant="outline" disabled={navigationState === "submitting"}>
|
|
Add
|
|
</Button>
|
|
</Form>
|
|
</div>
|
|
</SettingsSection>
|
|
);
|
|
}
|