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 final 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>
|
|
);
|
|
}
|