brackt/app/routes/admin.sports-seasons.$id.tsx
Chris Parsons d784571f29
feat: add meta title to all route pages (#109)
Every route now exports a meta function so the browser title bar reflects
the current page. Static pages use fixed titles; dynamic pages pull names
from loader data with a sensible fallback (e.g. league name, sport season
name, team name).

Titles follow the pattern "Page Name - Brackt" for user-facing routes and
"Page Name - Brackt Admin" for admin routes.

Fixes #74

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 12:10:52 -07:00

580 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Form, Link, redirect, useNavigate } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id";
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason } from "~/models/sports-season";
import { findParticipantsBySportsSeasonId } from "~/models/participant";
import { processSeasonStandings } from "~/models/scoring-calculator";
import { database } from "~/database/context";
import { participantEvSnapshots } from "~/database/schema";
import { eq, desc } from "drizzle-orm";
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { Badge } from "~/components/ui/badge";
import { Trash2, Users, Trophy, Calculator, CheckCircle2, Zap, AlertTriangle, Loader2 } from "lucide-react";
import { useState } from "react";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `${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);
// Get the most recent snapshot date (if any)
const db = database();
const lastSnapshot = await db
.select({ snapshotDate: participantEvSnapshots.snapshotDate })
.from(participantEvSnapshots)
.where(eq(participantEvSnapshots.sportsSeasonId, params.id))
.orderBy(desc(participantEvSnapshots.snapshotDate))
.limit(1);
const lastSimulatedDate = lastSnapshot[0]?.snapshotDate ?? null;
const simulatorInfo = sportsSeason.sport?.simulatorType
? getSimulatorInfo(sportsSeason.sport.simulatorType as SimulatorType)
: null;
return {
sportsSeason,
participants,
lastSimulatedDate,
simulatorInfo,
};
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
await deleteSportsSeason(params.id);
return redirect("/admin/sports-seasons");
}
if (intent === "finalize-standings") {
try {
await processSeasonStandings(params.id);
await updateSportsSeason(params.id, { status: "completed" });
return { success: true, message: "Standings finalized and fantasy placements assigned!" };
} catch (error) {
console.error("Error finalizing standings:", error);
return { error: "Failed to finalize standings. Please try again." };
}
}
// Update
const name = formData.get("name");
const year = formData.get("year");
const startDate = formData.get("startDate");
const endDate = formData.get("endDate");
const status = formData.get("status");
const scoringType = formData.get("scoringType");
const scoringPattern = formData.get("scoringPattern");
const totalMajors = formData.get("totalMajors");
// Validation
if (typeof name !== "string" || !name.trim()) {
return { error: "Season name is required" };
}
if (typeof year !== "string") {
return { error: "Year is required" };
}
const yearNum = parseInt(year, 10);
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
return { error: "Year must be between 2000 and 2100" };
}
if (status !== "upcoming" && status !== "active" && status !== "completed") {
return { error: "Invalid status" };
}
if (scoringType !== "playoffs" && scoringType !== "regular_season" && scoringType !== "majors") {
return { error: "Invalid scoring type" };
}
const validScoringPatterns = ["playoff_bracket", "season_standings", "qualifying_points"];
if (scoringPattern && typeof scoringPattern === "string" && !validScoringPatterns.includes(scoringPattern)) {
return { error: "Invalid scoring pattern" };
}
try {
const updateData: any = {
name: name.trim(),
year: yearNum,
startDate: typeof startDate === "string" && startDate ? startDate : null,
endDate: typeof endDate === "string" && endDate ? endDate : null,
status,
scoringType,
};
if (scoringPattern && typeof scoringPattern === "string") {
updateData.scoringPattern = scoringPattern;
}
if (totalMajors && typeof totalMajors === "string") {
const totalMajorsNum = parseInt(totalMajors, 10);
if (!isNaN(totalMajorsNum) && totalMajorsNum > 0) {
updateData.totalMajors = totalMajorsNum;
}
}
await updateSportsSeason(params.id, updateData);
return { success: true, message: "Sports season updated successfully!" };
} catch (error) {
console.error("Error updating sports season:", error);
return { error: "Failed to update sports season. Please try again." };
}
}
export default function EditSportsSeason({ loaderData, actionData }: Route.ComponentProps) {
const { sportsSeason, participants, lastSimulatedDate, simulatorInfo } = loaderData;
const navigate = useNavigate();
const [scoringPattern, setScoringPattern] = useState<string>(sportsSeason.scoringPattern || "");
return (
<div className="p-8">
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-3xl font-bold">Edit Sports Season</h1>
<p className="text-muted-foreground mt-1">
{sportsSeason.sport.name} - {sportsSeason.name}
</p>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Sports Season Details</CardTitle>
<CardDescription>
Update the information for this sports season
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">Season Name</Label>
<Input
id="name"
name="name"
type="text"
defaultValue={sportsSeason.name}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="year">Year</Label>
<Input
id="year"
name="year"
type="number"
min="2000"
max="2100"
defaultValue={sportsSeason.year}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startDate">Start Date (Optional)</Label>
<Input
id="startDate"
name="startDate"
type="date"
defaultValue={sportsSeason.startDate || ""}
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate">End Date (Optional)</Label>
<Input
id="endDate"
name="endDate"
type="date"
defaultValue={sportsSeason.endDate || ""}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select name="status" defaultValue={sportsSeason.status} required>
<SelectTrigger id="status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="upcoming">Upcoming</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="scoringType">Scoring Type</Label>
<Select name="scoringType" defaultValue={sportsSeason.scoringType} required>
<SelectTrigger id="scoringType">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="playoffs">Playoffs</SelectItem>
<SelectItem value="regular_season">Regular Season</SelectItem>
<SelectItem value="majors">Majors</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Playoffs: Team sports playoffs. Regular Season: Full season standings. Majors: Individual sport majors.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="scoringPattern">Scoring Pattern (Optional)</Label>
<Select name="scoringPattern" value={scoringPattern} onValueChange={setScoringPattern}>
<SelectTrigger id="scoringPattern">
<SelectValue placeholder="Select scoring pattern (optional)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="playoff_bracket">Playoff Bracket</SelectItem>
<SelectItem value="season_standings">Season Standings</SelectItem>
<SelectItem value="qualifying_points">Qualifying Points (Golf/Tennis)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Qualifying Points: For sports like Golf/Tennis where participants earn points across majors, then top 8 get fantasy points.
</p>
</div>
{scoringPattern === "qualifying_points" && (
<div className="space-y-2">
<Label htmlFor="totalMajors">Total Majors</Label>
<Input
id="totalMajors"
name="totalMajors"
type="number"
min="1"
max="10"
defaultValue={sportsSeason.totalMajors || 4}
placeholder="e.g., 4 (for Golf)"
/>
<p className="text-sm text-muted-foreground">
How many major tournaments will be tracked? (e.g., Golf has 4 majors, Tennis has 4 Grand Slams)
</p>
</div>
)}
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
{actionData?.success && (
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
{actionData.message}
</div>
)}
<div className="flex gap-4">
<Button type="submit" className="flex-1">
Save Changes
</Button>
<Button type="button" variant="outline" asChild>
<Link to="/admin/sports-seasons">Cancel</Link>
</Button>
</div>
</Form>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Participants</CardTitle>
<CardDescription>
{participants.length} {participants.length === 1 ? "participant" : "participants"}
</CardDescription>
</div>
<Button
size="sm"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/participants`)}
>
<Users className="mr-2 h-4 w-4" />
Manage Participants
</Button>
</div>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
<p className="text-sm text-muted-foreground">
No participants added yet. Add teams or players to this season.
</p>
) : (
<div className="space-y-2">
{participants.slice(0, 5).map((participant) => (
<div key={participant.id} className="text-sm">
{participant.name}
</div>
))}
{participants.length > 5 && (
<p className="text-sm text-muted-foreground">
And {participants.length - 5} more...
</p>
)}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Expected Values</CardTitle>
<CardDescription>
{simulatorInfo
? simulatorInfo.name
: "Manage probability distributions and projected points"}
</CardDescription>
</div>
<div className="flex items-center gap-2">
{sportsSeason.simulationStatus === "failed" && (
<Badge variant="outline" className="bg-destructive/15 text-destructive border-destructive/30">
<AlertTriangle className="mr-1 h-3 w-3" />
Last run failed
</Badge>
)}
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/futures-odds`)}
>
<Calculator className="mr-2 h-4 w-4" />
Futures Odds
</Button>
{simulatorInfo && (
<Form method="post" action={`/admin/sports-seasons/${sportsSeason.id}/simulate`}>
<Button
type="submit"
size="sm"
disabled={sportsSeason.simulationStatus === "running"}
>
{sportsSeason.simulationStatus === "running" ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Running...
</>
) : (
<>
<Zap className="mr-2 h-4 w-4" />
Run Simulation
</>
)}
</Button>
</Form>
)}
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{simulatorInfo
? <>
{simulatorInfo.description}.{" "}
{lastSimulatedDate ? `Last simulated: ${lastSimulatedDate}.` : "No simulation has been run yet."}
{" "}Import futures odds first, then run the simulation to update EVs and save a snapshot.
</>
: "Import futures odds to set probability distributions."}
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Scoring Events</CardTitle>
<CardDescription>
Manage games, tournaments, and schedule entries
</CardDescription>
</div>
<Button
size="sm"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/events`)}
>
<Trophy className="mr-2 h-4 w-4" />
Manage Events
</Button>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Create playoff brackets, major tournaments, or import a race/game schedule.
</p>
</CardContent>
</Card>
{sportsSeason.scoringPattern === "season_standings" && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Championship Standings</CardTitle>
<CardDescription>
Update participant positions and points as the season progresses
</CardDescription>
</div>
<Button
size="sm"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/standings`)}
>
Update Standings
</Button>
</div>
</CardHeader>
</Card>
)}
{sportsSeason.scoringPattern === "season_standings" && (
<Card className={sportsSeason.status === "completed" ? "border-emerald-500/30" : "border-amber-500/30"}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
Finalize Standings
{sportsSeason.status === "completed" && (
<Badge variant="outline" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">
<CheckCircle2 className="mr-1 h-3 w-3" />
Finalized
</Badge>
)}
</CardTitle>
<CardDescription>
{sportsSeason.status === "completed"
? "Season standings have been finalized and fantasy placements assigned."
: "When the championship is over, finalize standings to assign fantasy placements (1st8th) to participants based on their final positions."}
</CardDescription>
</div>
</div>
</CardHeader>
{sportsSeason.status !== "completed" && (
<CardContent>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm mb-4">
{actionData.error}
</div>
)}
{actionData?.success && actionData.message?.includes("finalized") && (
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm mb-4">
{actionData.message}
</div>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="border-amber-500/50 text-amber-600 hover:bg-amber-500/10 dark:text-amber-400">
<CheckCircle2 className="mr-2 h-4 w-4" />
Finalize Standings
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Finalize championship standings?</AlertDialogTitle>
<AlertDialogDescription>
This will read the current participant standings and assign fantasy placements (1st through 8th place). The season will be marked as completed. This action can be re-run to correct results if needed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Form method="post">
<input type="hidden" name="intent" value="finalize-standings" />
<AlertDialogAction type="submit">
Finalize Standings
</AlertDialogAction>
</Form>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
)}
</Card>
)}
<Card className="border-destructive">
<CardHeader>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
<CardDescription>
Permanently delete this sports season
</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete Sports Season
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the sports season "{sportsSeason.name}" and all
associated participants and results. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Form method="post">
<input type="hidden" name="intent" value="delete" />
<AlertDialogAction type="submit" className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</Form>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
</div>
</div>
</div>
);
}