brackt/app/routes/admin.templates.$id.tsx

343 lines
12 KiB
TypeScript

import { Form, Link, redirect } from "react-router";
import { useState } from "react";
import type { Route } from "./+types/admin.templates.$id";
import {
findSeasonTemplateWithSportsSeasons,
updateSeasonTemplate,
deleteSeasonTemplate,
setSeasonTemplateActive
} from "~/models/season-template";
import {
addSportToTemplate,
removeSportFromTemplate
} from "~/models/season-template-sport";
import { findAllSportsSeasons } from "~/models/sports-season";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
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, Plus, X } from "lucide-react";
import { Badge } from "~/components/ui/badge";
export async function loader({ params }: Route.LoaderArgs) {
const template = await findSeasonTemplateWithSportsSeasons(params.id);
if (!template) {
throw new Response("Template not found", { status: 404 });
}
const allSportsSeasons = await findAllSportsSeasons();
// Type assertion since we know the sport relation is included
return {
template,
allSportsSeasons: allSportsSeasons as Array<typeof allSportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }>
};
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
await deleteSeasonTemplate(params.id);
return redirect("/admin/templates");
}
if (intent === "toggle-active") {
const isActive = formData.get("isActive") === "true";
await setSeasonTemplateActive(params.id, isActive);
return { success: true };
}
if (intent === "add-sport") {
const sportsSeasonId = formData.get("sportsSeasonId");
if (typeof sportsSeasonId === "string") {
await addSportToTemplate(params.id, sportsSeasonId);
}
return { success: true };
}
if (intent === "remove-sport") {
const sportsSeasonId = formData.get("sportsSeasonId");
if (typeof sportsSeasonId === "string") {
await removeSportFromTemplate(params.id, sportsSeasonId);
}
return { success: true };
}
// Update template
const name = formData.get("name");
const year = formData.get("year");
const description = formData.get("description");
if (typeof name !== "string" || !name.trim()) {
return { error: "Template 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" };
}
try {
await updateSeasonTemplate(params.id, {
name: name.trim(),
year: yearNum,
description: typeof description === "string" && description.trim() ? description.trim() : null,
});
return { success: true };
} catch (error) {
console.error("Error updating template:", error);
return { error: "Failed to update template. Please try again." };
}
}
export default function EditTemplate({ loaderData, actionData }: Route.ComponentProps) {
const { template, allSportsSeasons } = loaderData;
const [selectedSportId, setSelectedSportId] = useState("");
const includedSeasonIds = new Set(
template.seasonTemplateSports.map((s) => s.sportsSeasonId)
);
const availableSportsSeasons = allSportsSeasons.filter(
(s) => !includedSeasonIds.has(s.id)
);
return (
<div className="p-8">
<div className="max-w-4xl">
<div className="mb-6">
<h1 className="text-3xl font-bold">Edit Template</h1>
<p className="text-muted-foreground mt-1">{template.name}</p>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Template Details</CardTitle>
<CardDescription>Update the template information</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">Template Name</Label>
<Input
id="name"
name="name"
type="text"
defaultValue={template.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={template.year}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description (Optional)</Label>
<Textarea
id="description"
name="description"
defaultValue={template.description || ""}
rows={3}
/>
</div>
<div className="flex items-center space-x-2">
<Form method="post">
<input type="hidden" name="intent" value="toggle-active" />
<input type="hidden" name="isActive" value={(!template.isActive).toString()} />
<Button
type="submit"
variant={template.isActive ? "outline" : "default"}
size="sm"
>
{template.isActive ? "Set Inactive" : "Set Active"}
</Button>
</Form>
<Badge variant={template.isActive ? "default" : "secondary"}>
{template.isActive ? "Active" : "Inactive"}
</Badge>
</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">
Template 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/templates">Cancel</Link>
</Button>
</div>
</Form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Sports Seasons</CardTitle>
<CardDescription>
Add sports seasons to this template
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{template.seasonTemplateSports.length > 0 && (
<div className="space-y-2">
{template.seasonTemplateSports.map((templateSport) => (
<div
key={templateSport.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<p className="font-medium">
{templateSport.sportsSeason.sport.name} - {templateSport.sportsSeason.name}
</p>
<p className="text-sm text-muted-foreground">
{templateSport.sportsSeason.year} {templateSport.sportsSeason.scoringType.replace("_", " ")}
</p>
</div>
<Form method="post">
<input type="hidden" name="intent" value="remove-sport" />
<input type="hidden" name="sportsSeasonId" value={templateSport.sportsSeasonId} />
<Button
type="submit"
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
>
<X className="h-4 w-4" />
</Button>
</Form>
</div>
))}
</div>
)}
{availableSportsSeasons.length > 0 ? (
<Form method="post" className="flex gap-2">
<input type="hidden" name="intent" value="add-sport" />
<Select
name="sportsSeasonId"
value={selectedSportId}
onValueChange={setSelectedSportId}
required
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Select a sports season" />
</SelectTrigger>
<SelectContent>
{availableSportsSeasons.map((season) => (
<SelectItem key={season.id} value={season.id}>
{season.sport.name} - {season.name} ({season.year})
</SelectItem>
))}
</SelectContent>
</Select>
<Button type="submit">
<Plus className="h-4 w-4" />
</Button>
</Form>
) : (
<p className="text-sm text-muted-foreground text-center py-4">
All available sports seasons have been added
</p>
)}
</CardContent>
</Card>
<Card className="border-destructive">
<CardHeader>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
<CardDescription>
Permanently delete this template
</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete Template
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the template "{template.name}".
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>
);
}