Removes light mode entirely in favour of a permanent dark theme with a navy-tinted background and three signature accents (electric blue, amber/gold, coral) exposed as CSS custom properties and Tailwind utilities (bg-electric, text-amber-accent, text-coral-accent). - Set class="dark" on <html> and apply Clerk dark base theme - Rewrite app.css: single :root palette (oklch navy values), custom --electric / --amber-accent / --coral-accent variables, remove duplicate .dark block and light-mode bg-white/bg-gray-950 rule - Install @clerk/themes for Clerk dark modal support - Replace hardcoded Tailwind colors across 30+ files: - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10 - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400 - Info cards: blue-50 dark:bg-blue-950 → electric/10 - Warning cards: yellow-500 → amber-accent variants - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent - Movement indicators: green-600/red-600 → emerald-400/coral-accent - Connection dots: green-500/red-500 → emerald-500/coral-accent - Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark) - Update DraftGrid test assertions to match new class names Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
343 lines
12 KiB
TypeScript
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-emerald-500/15 text-emerald-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>
|
|
);
|
|
}
|