brackt/app/routes/admin.sports-seasons.$id.events.tsx

324 lines
13 KiB
TypeScript
Raw Normal View History

import { Form, Link } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id.events";
import { loader, action } from "./admin.sports-seasons.$id.events.server";
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 { Badge } from "~/components/ui/badge";
import { Textarea } from "~/components/ui/textarea";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { Calendar, Trophy, ArrowLeft, Trash2, ListPlus } from "lucide-react";
import { format, parseISO } from "date-fns";
import { QualifyingPointsStandings } from "~/components/scoring/QualifyingPointsStandings";
import { getEventTypeLabel } from "~/models/scoring-event";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `Events — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
}
export { loader, action };
export default function SportsSeasonEvents({
loaderData,
actionData,
}: Route.ComponentProps) {
const { sportsSeason, events, qpStandings, scoringRules } = loaderData;
const defaultEventType =
sportsSeason.scoringPattern === "qualifying_points"
? "major_tournament"
: sportsSeason.scoringPattern === "season_standings"
? "schedule_event"
: "playoff_game";
const getStatusBadge = (isComplete: boolean, eventType: string, eventDate?: string | null) => {
if (isComplete) {
return (
<Badge variant="default" className="bg-emerald-500">
Completed
</Badge>
);
}
if (eventType === "schedule_event") {
const today = new Date().toISOString().split("T")[0];
const isPast = eventDate && eventDate < today;
return isPast ? (
<Badge variant="outline" className="text-muted-foreground">Results Pending</Badge>
) : (
<Badge variant="outline">Upcoming</Badge>
);
}
return <Badge variant="secondary">In Progress</Badge>;
};
return (
<div className="p-8">
<div className="max-w-4xl">
<div className="mb-6">
<Button variant="ghost" size="sm" asChild className="mb-2">
<Link to={`/admin/sports-seasons/${sportsSeason.id}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Sports Season
</Link>
</Button>
<h1 className="text-3xl font-bold">Scoring Events</h1>
<p className="text-muted-foreground mt-1">
{sportsSeason.sport.name} - {sportsSeason.name}
</p>
{sportsSeason.scoringPattern === "qualifying_points" && (
<div className="mt-2">
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300 dark:bg-amber-950 dark:text-amber-300">
Qualifying Points Mode
</Badge>
</div>
)}
</div>
<div className="space-y-6">
{/* Success/Error Messages */}
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
{actionData?.success && (
Redesign to dark-mode-only with navy palette and accent colors (#13) 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>
2026-02-20 19:26:11 -08:00
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
{actionData.success}
</div>
)}
{/* QP Standings for qualifying points sports */}
{sportsSeason.scoringPattern === "qualifying_points" && qpStandings && (
<QualifyingPointsStandings
standings={qpStandings}
scoringRules={scoringRules}
isFinalized={sportsSeason.qualifyingPointsFinalized || false}
totalMajors={sportsSeason.totalMajors}
majorsCompleted={sportsSeason.majorsCompleted}
canFinalize={
sportsSeason.totalMajors !== null &&
sportsSeason.majorsCompleted >= sportsSeason.totalMajors &&
!sportsSeason.qualifyingPointsFinalized
}
/>
)}
{/* Create New Event Card */}
<Card>
<CardHeader>
<CardTitle>Create New Event</CardTitle>
<CardDescription>
Add a new scoring event (bracket, tournament, standings, etc.)
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Event Name</Label>
<Input
id="name"
name="name"
type="text"
placeholder="e.g., Super Bowl, Round 1, Game 3"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="eventType">Event Type</Label>
<Select
name="eventType"
defaultValue={defaultEventType}
required
>
<SelectTrigger id="eventType">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="schedule_event">Non-Scoring</SelectItem>
<SelectItem value="major_tournament">Major Tournament</SelectItem>
<SelectItem value="playoff_game">Bracket</SelectItem>
</SelectContent>
</Select>
{sportsSeason.scoringPattern === "qualifying_points" && (
<p className="text-sm text-muted-foreground">
Major Tournament events will automatically award qualifying points when completed.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="eventDate">Event Date (Optional)</Label>
<Input id="eventDate" name="eventDate" type="date" />
</div>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
<Button type="submit" className="w-full">
<Trophy className="mr-2 h-4 w-4" />
Create Event
</Button>
</Form>
</CardContent>
</Card>
{/* Bulk Create Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ListPlus className="h-5 w-5" />
Bulk Import Events
</CardTitle>
<CardDescription>
Paste a schedule one event per line. Format:{" "}
<code className="text-xs bg-muted px-1 rounded">
Event Name, YYYY-MM-DD
</code>{" "}
(date is optional).
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="bulk-create" />
<div className="space-y-2">
<Label htmlFor="bulkEventType">Event Type</Label>
<Select name="bulkEventType" defaultValue={defaultEventType}>
<SelectTrigger id="bulkEventType">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="schedule_event">Non-Scoring</SelectItem>
<SelectItem value="major_tournament">Major Tournament</SelectItem>
<SelectItem value="playoff_game">Bracket</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="bulkEvents">Events</Label>
<Textarea
id="bulkEvents"
name="bulkEvents"
rows={6}
placeholder={"Bahrain Grand Prix, 2025-03-02\nSaudi Arabian Grand Prix, 2025-03-09\nAustralian Grand Prix, 2025-03-23"}
/>
</div>
<Button type="submit" variant="outline" className="w-full">
<ListPlus className="mr-2 h-4 w-4" />
Import Events
</Button>
</Form>
</CardContent>
</Card>
{/* Events List */}
<Card>
<CardHeader>
<CardTitle>Events</CardTitle>
<CardDescription>
{events.length} {events.length === 1 ? "event" : "events"}
</CardDescription>
</CardHeader>
<CardContent>
{events.length === 0 ? (
<p className="text-sm text-muted-foreground">
No events created yet. Create your first event above.
</p>
) : (
<div className="space-y-3">
{events.map((event: { id: string; name: string; eventType: string; eventDate?: string | null; isComplete: boolean }) => (
<Card key={event.id} className="hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex items-start justify-between gap-4">
<Link
to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}`}
className="flex-1 cursor-pointer"
>
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold">{event.name}</h3>
{getStatusBadge(event.isComplete, event.eventType, event.eventDate)}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>
{getEventTypeLabel(event.eventType)}
</span>
{event.eventDate && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{format(parseISO(event.eventDate), "MMM d, yyyy")}
</span>
)}
</div>
</Link>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="ghost"
className="text-destructive hover:text-destructive shrink-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete "{event.name}"?</AlertDialogTitle>
<AlertDialogDescription>
This will also delete all results for this event. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Form method="post">
<input type="hidden" name="intent" value="delete-event" />
<input type="hidden" name="eventId" value={event.id} />
<AlertDialogAction type="submit" className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</Form>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
}