288 lines
11 KiB
TypeScript
288 lines
11 KiB
TypeScript
|
|
import { Form, Link } from "react-router";
|
||
|
|
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId";
|
||
|
|
import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.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 { ArrowLeft, Trophy, CheckCircle2, Pencil, Trash2 } from "lucide-react";
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableHead,
|
||
|
|
TableHeader,
|
||
|
|
TableRow,
|
||
|
|
} from "~/components/ui/table";
|
||
|
|
|
||
|
|
export { loader, action };
|
||
|
|
|
||
|
|
export default function EventResults({
|
||
|
|
loaderData,
|
||
|
|
actionData,
|
||
|
|
}: Route.ComponentProps) {
|
||
|
|
const { sportsSeason, event, participants, results } = loaderData;
|
||
|
|
|
||
|
|
// Create a map of participants with results for easy lookup
|
||
|
|
const participantResultsMap = new Map(
|
||
|
|
results.map((r: { participant: { id: string } }) => [r.participant.id, r])
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get participants without results
|
||
|
|
const participantsWithoutResults = participants.filter(
|
||
|
|
(p: { id: string }) => !participantResultsMap.has(p.id)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Sort results by placement
|
||
|
|
const sortedResults = [...results].sort((a, b) => (a.placement || 999) - (b.placement || 999));
|
||
|
|
|
||
|
|
const getEventTypeLabel = (type: string) => {
|
||
|
|
switch (type) {
|
||
|
|
case "playoff_game":
|
||
|
|
return "Playoff Game";
|
||
|
|
case "major_tournament":
|
||
|
|
return "Major Tournament";
|
||
|
|
case "race":
|
||
|
|
return "Race";
|
||
|
|
case "final_standings":
|
||
|
|
return "Final Standings";
|
||
|
|
default:
|
||
|
|
return type;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
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}/events`}>
|
||
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
|
|
Back to Events
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold">{event.name}</h1>
|
||
|
|
<p className="text-muted-foreground mt-1">
|
||
|
|
{sportsSeason.sport.name} - {sportsSeason.name} •{" "}
|
||
|
|
{getEventTypeLabel(event.eventType)}
|
||
|
|
{event.playoffRound && ` • ${event.playoffRound}`}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
{event.isComplete ? (
|
||
|
|
<Badge variant="default" className="bg-green-500">
|
||
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||
|
|
Completed
|
||
|
|
</Badge>
|
||
|
|
) : (
|
||
|
|
<Badge variant="secondary">In Progress</Badge>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-6">
|
||
|
|
{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">
|
||
|
|
{actionData.success}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Add Result Form */}
|
||
|
|
{!event.isComplete && (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Add Result</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Enter the placement for a participant in this event
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<Form method="post" className="space-y-4">
|
||
|
|
<input type="hidden" name="intent" value="add-result" />
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="participantId">Participant</Label>
|
||
|
|
<Select name="participantId" required>
|
||
|
|
<SelectTrigger id="participantId">
|
||
|
|
<SelectValue placeholder="Select participant" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{participantsWithoutResults.length === 0 ? (
|
||
|
|
<div className="p-2 text-sm text-muted-foreground">
|
||
|
|
All participants have results
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
participantsWithoutResults.map((participant: { id: string; name: string }) => (
|
||
|
|
<SelectItem
|
||
|
|
key={participant.id}
|
||
|
|
value={participant.id}
|
||
|
|
>
|
||
|
|
{participant.name}
|
||
|
|
</SelectItem>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="placement">Placement</Label>
|
||
|
|
<Input
|
||
|
|
id="placement"
|
||
|
|
name="placement"
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="100"
|
||
|
|
placeholder="e.g., 1"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
type="submit"
|
||
|
|
className="w-full"
|
||
|
|
disabled={participantsWithoutResults.length === 0}
|
||
|
|
>
|
||
|
|
<Trophy className="mr-2 h-4 w-4" />
|
||
|
|
Add Result
|
||
|
|
</Button>
|
||
|
|
</Form>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Current Results */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<CardTitle>Results</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
{results.length} of {participants.length} participants have
|
||
|
|
results
|
||
|
|
</CardDescription>
|
||
|
|
</div>
|
||
|
|
{!event.isComplete && results.length > 0 && (
|
||
|
|
<Form method="post">
|
||
|
|
<input type="hidden" name="intent" value="complete" />
|
||
|
|
<Button type="submit" variant="outline" size="sm">
|
||
|
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||
|
|
Mark Event Complete
|
||
|
|
</Button>
|
||
|
|
</Form>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{sortedResults.length === 0 ? (
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
No results added yet. Add results using the form above.
|
||
|
|
</p>
|
||
|
|
) : (
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead className="w-24">Placement</TableHead>
|
||
|
|
<TableHead>Participant</TableHead>
|
||
|
|
{!event.isComplete && <TableHead className="w-32">Actions</TableHead>}
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{sortedResults.map((result) => (
|
||
|
|
<TableRow key={result.id}>
|
||
|
|
<TableCell>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
{result.placement === 1 && (
|
||
|
|
<span className="text-xl">🥇</span>
|
||
|
|
)}
|
||
|
|
{result.placement === 2 && (
|
||
|
|
<span className="text-xl">🥈</span>
|
||
|
|
)}
|
||
|
|
{result.placement === 3 && (
|
||
|
|
<span className="text-xl">🥉</span>
|
||
|
|
)}
|
||
|
|
<span className="font-semibold">
|
||
|
|
{result.placement}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>{result.participant.name}</TableCell>
|
||
|
|
{!event.isComplete && (
|
||
|
|
<TableCell>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Form method="post" className="inline">
|
||
|
|
<input type="hidden" name="intent" value="update-result" />
|
||
|
|
<input type="hidden" name="resultId" value={result.id} />
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
name="placement"
|
||
|
|
defaultValue={result.placement || ""}
|
||
|
|
min="1"
|
||
|
|
max="100"
|
||
|
|
className="w-16 h-8 text-sm"
|
||
|
|
onKeyDown={(e) => {
|
||
|
|
if (e.key === 'Enter') {
|
||
|
|
e.currentTarget.form?.requestSubmit();
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Button type="submit" size="sm" variant="ghost" className="h-8 w-8 p-0">
|
||
|
|
<Pencil className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
</Form>
|
||
|
|
<Form method="post" className="inline">
|
||
|
|
<input type="hidden" name="intent" value="delete-result" />
|
||
|
|
<input type="hidden" name="resultId" value={result.id} />
|
||
|
|
<Button
|
||
|
|
type="submit"
|
||
|
|
size="sm"
|
||
|
|
variant="ghost"
|
||
|
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||
|
|
onClick={(e) => {
|
||
|
|
if (!confirm(`Delete result for ${result.participant.name}?`)) {
|
||
|
|
e.preventDefault();
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Trash2 className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
</Form>
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
)}
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|