brackt/app/routes/admin.sports-seasons.new.tsx
Chris Parsons 8aa1726b12
feat: highlight owned participants in standings and fix schedule event badges (#83)
- SeasonStandings: highlight rows for the logged-in user's drafted participants
  with electric accent (in-points) or muted (outside points) left border + star icon
- Loader: derive userParticipantIds from current user's teams and draft picks
- EventSchedule: hide type badge for schedule_event; show "Results Pending" only
  when event is past and not yet marked complete (matches admin page logic)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 21:59:29 -08:00

278 lines
9.9 KiB
TypeScript

import { Form, Link, redirect } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.new";
import { createSportsSeason } from "~/models/sports-season";
import { findAllSports } from "~/models/sport";
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 { useState } from "react";
export async function loader() {
const sports = await findAllSports();
return { sports };
}
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const sportId = formData.get("sportId");
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 sportId !== "string" || !sportId) {
return { error: "Sport is required" };
}
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 seasonData: any = {
sportId,
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") {
seasonData.scoringPattern = scoringPattern;
}
if (totalMajors && typeof totalMajors === "string") {
const totalMajorsNum = parseInt(totalMajors, 10);
if (!isNaN(totalMajorsNum) && totalMajorsNum > 0) {
seasonData.totalMajors = totalMajorsNum;
}
}
await createSportsSeason(seasonData);
return redirect("/admin/sports-seasons");
} catch (error) {
console.error("Error creating sports season:", error);
return { error: "Failed to create sports season. Please try again." };
}
}
export default function NewSportsSeason({ loaderData, actionData }: Route.ComponentProps) {
const { sports } = loaderData;
const currentYear = new Date().getFullYear();
const [scoringPattern, setScoringPattern] = useState<string>("");
return (
<div className="p-8">
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-3xl font-bold">Create New Sports Season</h1>
<p className="text-muted-foreground mt-1">
Add a new season for a sport
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Sports Season Details</CardTitle>
<CardDescription>
Enter the information for the new sports season
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-6">
<div className="space-y-2">
<Label htmlFor="sportId">Sport</Label>
<Select name="sportId" required>
<SelectTrigger id="sportId">
<SelectValue placeholder="Select a sport" />
</SelectTrigger>
<SelectContent>
{sports.map((sport) => (
<SelectItem key={sport.id} value={sport.id}>
{sport.name}
</SelectItem>
))}
</SelectContent>
</Select>
{sports.length === 0 && (
<p className="text-sm text-muted-foreground">
No sports available. <Link to="/admin/sports/new" className="underline">Create a sport first</Link>.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="name">Season Name</Label>
<Input
id="name"
name="name"
type="text"
placeholder="e.g., 2025 NFL Playoffs, 2025 PGA Tour"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="year">Year</Label>
<Input
id="year"
name="year"
type="number"
min="2000"
max="2100"
defaultValue={currentYear}
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"
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate">End Date (Optional)</Label>
<Input
id="endDate"
name="endDate"
type="date"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select name="status" defaultValue="upcoming" required>
<SelectTrigger id="status">
<SelectValue placeholder="Select status" />
</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" required>
<SelectTrigger id="scoringType">
<SelectValue placeholder="Select scoring type" />
</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" 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="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>
)}
<div className="flex gap-4">
<Button type="submit" className="flex-1">
Create Sports Season
</Button>
<Button type="button" variant="outline" asChild>
<Link to="/admin/sports-seasons">Cancel</Link>
</Button>
</div>
</Form>
</CardContent>
</Card>
</div>
</div>
);
}