**Discord webhook improvements** - Show team owner username in parentheses in standings (e.g. "1. Alpha FC (christhrowsrocks) — 150 pts") - Show username in parentheses in match results for drafted participants (e.g. "Sporting (christhrowsrocks) def. Bodø/Glimt (apatel)"), omitting the parenthesis for the undrafted side - Username lookup only runs after the webhook URL guard to avoid wasted DB queries - 4 new tests covering all username display combinations **Fix calculateTeamScore partial-score counting bug** - isPartialScore participants were incorrectly counted in participantsCompleted and placementCounts, causing standings to show wrong remaining count and phantom placement badges (e.g. "5th×1") for still-alive bracket participants - Floor points still flow into totalPoints (guaranteed value for ranking) - 3 new unit tests covering partial vs finalized behavior in calculateTeamScore **Admin Force Re-score action** - New "Fantasy Standings" card on every sports season admin page with a Force Re-score button that triggers recalculateStandings on all linked fantasy seasons — useful after data corrections without waiting for a new scoring event - Auth guard added to the action (was missing — layout loader only protects GET) - Returns a meaningful message when no linked seasons exist - Intent discriminant on action returns so success banners use actionData.intent instead of fragile message.includes() checks Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
628 lines
25 KiB
TypeScript
628 lines
25 KiB
TypeScript
import { Form, Link, redirect, useNavigate } from "react-router";
|
||
import { getAuth } from "@clerk/react-router/server";
|
||
import { isUserAdminByClerkId } from "~/models/user";
|
||
import type { Route } from "./+types/admin.sports-seasons.$id";
|
||
|
||
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason } from "~/models/sports-season";
|
||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||
import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator";
|
||
import { database } from "~/database/context";
|
||
import { participantEvSnapshots, seasonSports } 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(args: Route.ActionArgs) {
|
||
const { request, params } = args;
|
||
const { userId } = await getAuth(args);
|
||
const isAdmin = userId ? await isUserAdminByClerkId(userId) : false;
|
||
if (!isAdmin) {
|
||
throw new Response("Forbidden", { status: 403 });
|
||
}
|
||
|
||
const formData = await request.formData();
|
||
const intent = formData.get("intent");
|
||
|
||
if (intent === "delete") {
|
||
await deleteSportsSeason(params.id);
|
||
return redirect("/admin/sports-seasons");
|
||
}
|
||
|
||
if (intent === "rescore") {
|
||
try {
|
||
const db = database();
|
||
const links = await db.query.seasonSports.findMany({
|
||
where: eq(seasonSports.sportsSeasonId, params.id),
|
||
});
|
||
if (links.length === 0) {
|
||
return { success: true, intent: "rescore", message: "No linked fantasy seasons found — nothing to rescore." };
|
||
}
|
||
await Promise.all(links.map((link) => recalculateStandings(link.seasonId, db)));
|
||
return { success: true, intent: "rescore", message: `Rescored ${links.length} linked season(s).` };
|
||
} catch (error) {
|
||
console.error("Error rescoring:", error);
|
||
return { error: "Failed to rescore. Please try again." };
|
||
}
|
||
}
|
||
|
||
if (intent === "finalize-standings") {
|
||
try {
|
||
await processSeasonStandings(params.id);
|
||
await updateSportsSeason(params.id, { status: "completed" });
|
||
return { success: true, intent: "finalize-standings", 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 (1st–8th) 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.intent === "finalize-standings" && (
|
||
<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>
|
||
<CardHeader>
|
||
<CardTitle>Fantasy Standings</CardTitle>
|
||
<CardDescription>
|
||
Force a recalculation of all fantasy standings linked to this sports season. Use this after fixing scoring bugs or data corrections.
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{actionData?.success && actionData.intent === "rescore" && (
|
||
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm mb-4">
|
||
{actionData.message}
|
||
</div>
|
||
)}
|
||
<Form method="post">
|
||
<input type="hidden" name="intent" value="rescore" />
|
||
<Button type="submit" variant="outline" className="border-blue-500/50 text-blue-600 hover:bg-blue-500/10 dark:text-blue-400">
|
||
Force Re-score
|
||
</Button>
|
||
</Form>
|
||
</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>
|
||
);
|
||
}
|