brackt/app/routes/admin.sports-seasons.$id.participants.tsx

283 lines
9.7 KiB
TypeScript
Raw Normal View History

import { Form, Link } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id.participants";
import { logger } from "~/lib/logger";
import { findSportsSeasonById } from "~/models/sports-season";
import {
findParticipantsBySportsSeasonId,
createParticipant,
deleteParticipant
} from "~/models/participant";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import { Plus, Trash2, ArrowLeft } from "lucide-react";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `Participants — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
}
export async function loader({ params }: Route.LoaderArgs) {
const sportsSeason = await findSportsSeasonById(params.id);
if (!sportsSeason) {
throw new Response("Sports season not found", { status: 404 });
}
const participants = await findParticipantsBySportsSeasonId(params.id);
// Type assertion since we know the sport relation is included
return {
sportsSeason: sportsSeason as typeof sportsSeason & { sport: { id: string; name: string; type: string; slug: string } },
participants
};
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
const participantId = formData.get("participantId");
if (typeof participantId === "string") {
await deleteParticipant(participantId);
}
return { success: true };
}
// Bulk add participants
if (intent === "bulk") {
const bulkNames = formData.get("bulkNames");
if (typeof bulkNames !== "string" || !bulkNames.trim()) {
return { error: "Please enter at least one participant name" };
}
const names = bulkNames
.split("\n")
.map(name => name.trim())
.filter(name => name.length > 0);
if (names.length === 0) {
return { error: "Please enter at least one participant name" };
}
try {
for (const name of names) {
await createParticipant({
sportsSeasonId: params.id,
name,
shortName: null,
externalId: null,
expectedValue: "0",
});
}
return { success: true, count: names.length };
} catch (error) {
logger.error("Error creating participants:", error);
return { error: "Failed to add participants. Please try again." };
}
}
// Add single participant
const name = formData.get("name");
if (typeof name !== "string" || !name.trim()) {
return { error: "Participant name is required" };
}
try {
await createParticipant({
sportsSeasonId: params.id,
name: name.trim(),
shortName: null,
externalId: null,
expectedValue: "0",
});
return { success: true };
} catch (error) {
logger.error("Error creating participant:", error);
return { error: "Failed to add participant. Please try again." };
}
}
export default function ManageParticipants({ loaderData, actionData }: Route.ComponentProps) {
const { sportsSeason, participants } = loaderData;
// Use success state to reset forms by changing the key
const formKey = actionData?.success ? Date.now() : 'static';
return (
<div className="p-8">
<div className="max-w-4xl">
<div className="mb-6">
<Button variant="ghost" size="sm" asChild className="mb-2">
<Link to={`/admin/sports-seasons/${sportsSeason.id}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Sports Season
</Link>
</Button>
<h1 className="text-3xl font-bold">Manage Participants</h1>
<p className="text-muted-foreground mt-1">
{sportsSeason.sport.name} - {sportsSeason.name}
</p>
</div>
<div className="grid gap-6 md:grid-cols-2 mb-6">
<Card>
<CardHeader>
<CardTitle>Add Participant</CardTitle>
<CardDescription>
Add a {sportsSeason.sport.type === "team" ? "team" : "player"} to this season
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4" key={`single-${formKey}`}>
<div className="space-y-2">
<Label htmlFor="name">Participant Name</Label>
<Input
id="name"
name="name"
type="text"
placeholder={sportsSeason.sport.type === "team" ? "e.g., Kansas City Chiefs" : "e.g., Tiger Woods"}
required
/>
</div>
{actionData?.error && !actionData?.count && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
{actionData?.success && !actionData?.count && (
Redesign to dark-mode-only with navy palette and accent colors (#13) Removes light mode entirely in favour of a permanent dark theme with a navy-tinted background and three signature accents (electric blue, amber/gold, coral) exposed as CSS custom properties and Tailwind utilities (bg-electric, text-amber-accent, text-coral-accent). - Set class="dark" on <html> and apply Clerk dark base theme - Rewrite app.css: single :root palette (oklch navy values), custom --electric / --amber-accent / --coral-accent variables, remove duplicate .dark block and light-mode bg-white/bg-gray-950 rule - Install @clerk/themes for Clerk dark modal support - Replace hardcoded Tailwind colors across 30+ files: - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10 - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400 - Info cards: blue-50 dark:bg-blue-950 → electric/10 - Warning cards: yellow-500 → amber-accent variants - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent - Movement indicators: green-600/red-600 → emerald-400/coral-accent - Connection dots: green-500/red-500 → emerald-500/coral-accent - Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark) - Update DraftGrid test assertions to match new class names Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:26:11 -08:00
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
Participant added successfully!
</div>
)}
<Button type="submit" className="w-full">
<Plus className="mr-2 h-4 w-4" />
Add Participant
</Button>
</Form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Bulk Add Participants</CardTitle>
<CardDescription>
Add multiple participants at once (one per line)
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4" key={`bulk-${formKey}`}>
<input type="hidden" name="intent" value="bulk" />
<div className="space-y-2">
<Label htmlFor="bulkNames">Participant Names</Label>
<Textarea
id="bulkNames"
name="bulkNames"
placeholder={sportsSeason.sport.type === "team"
? "Kansas City Chiefs\nBuffalo Bills\nSan Francisco 49ers"
: "Tiger Woods\nRory McIlroy\nJon Rahm"}
rows={8}
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
Enter one participant name per line
</p>
</div>
{actionData?.error && actionData?.count === undefined && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
{actionData?.success && actionData?.count && (
Redesign to dark-mode-only with navy palette and accent colors (#13) Removes light mode entirely in favour of a permanent dark theme with a navy-tinted background and three signature accents (electric blue, amber/gold, coral) exposed as CSS custom properties and Tailwind utilities (bg-electric, text-amber-accent, text-coral-accent). - Set class="dark" on <html> and apply Clerk dark base theme - Rewrite app.css: single :root palette (oklch navy values), custom --electric / --amber-accent / --coral-accent variables, remove duplicate .dark block and light-mode bg-white/bg-gray-950 rule - Install @clerk/themes for Clerk dark modal support - Replace hardcoded Tailwind colors across 30+ files: - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10 - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400 - Info cards: blue-50 dark:bg-blue-950 → electric/10 - Warning cards: yellow-500 → amber-accent variants - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent - Movement indicators: green-600/red-600 → emerald-400/coral-accent - Connection dots: green-500/red-500 → emerald-500/coral-accent - Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark) - Update DraftGrid test assertions to match new class names Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:26:11 -08:00
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
{actionData.count} participant{actionData.count !== 1 ? 's' : ''} added successfully!
</div>
)}
<Button type="submit" className="w-full">
<Plus className="mr-2 h-4 w-4" />
Add All Participants
</Button>
</Form>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>All Participants</CardTitle>
<CardDescription>
{participants.length} {participants.length === 1 ? "participant" : "participants"} total
</CardDescription>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No participants yet. Add your first {sportsSeason.sport.type === "team" ? "team" : "player"}.
</p>
) : (
<div className="max-h-[500px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
<TableHead className="w-[50px]" />
</TableRow>
</TableHeader>
<TableBody>
{participants.map((participant) => (
<TableRow key={participant.id}>
<TableCell className="font-medium">{participant.name}</TableCell>
<TableCell>
<Form method="post">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="participantId" value={participant.id} />
<Button
type="submit"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</Form>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}