- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats. - Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets. - Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly. - Enhanced database schema to accommodate new scoring rules and bracket templates. - Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly. - Documented implementation phases for gradual rollout of new features. - Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
295 lines
11 KiB
TypeScript
295 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, Brackets } 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 "Bracket";
|
|
case "major_tournament":
|
|
return "Major Tournament";
|
|
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>
|
|
<div className="flex items-center gap-2">
|
|
{event.eventType === "playoff_game" && (
|
|
<Button variant="outline" asChild>
|
|
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}/bracket`}>
|
|
<Brackets className="mr-2 h-4 w-4" />
|
|
Manage Bracket
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
{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>
|
|
|
|
<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>
|
|
);
|
|
}
|