- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats. - Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets. - Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly. - Enhanced database schema to accommodate new scoring rules and bracket templates. - Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly. - Documented implementation phases for gradual rollout of new features. - Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
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 { 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 { Trash2, Users, Trophy } from "lucide-react";
|
|
|
|
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") {
|
|
await deleteSportsSeason(params.id);
|
|
return redirect("/admin/sports-seasons");
|
|
}
|
|
|
|
// 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");
|
|
|
|
// 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" };
|
|
}
|
|
|
|
try {
|
|
await updateSportsSeason(params.id, {
|
|
name: name.trim(),
|
|
year: yearNum,
|
|
startDate: typeof startDate === "string" && startDate ? startDate : null,
|
|
endDate: typeof endDate === "string" && endDate ? endDate : null,
|
|
status,
|
|
scoringType,
|
|
});
|
|
|
|
return { success: true };
|
|
} 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 } = loaderData;
|
|
const navigate = useNavigate();
|
|
|
|
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>
|
|
</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-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
|
|
Sports season updated successfully!
|
|
</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>Scoring Events</CardTitle>
|
|
<CardDescription>
|
|
Manage games, tournaments, and results
|
|
</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 games, tournaments, races, or standings events to track participant results and calculate fantasy points.
|
|
</p>
|
|
</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>
|
|
);
|
|
}
|