- 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.
278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
import { Form, Link } from "react-router";
|
|
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
|
|
import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.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 { ArrowLeft, Plus, Trophy } from "lucide-react";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "~/components/ui/table";
|
|
|
|
export { loader, action };
|
|
|
|
export default function EventBracket({
|
|
loaderData,
|
|
actionData,
|
|
}: Route.ComponentProps) {
|
|
const { sportsSeason, event, participants, matches } = loaderData;
|
|
|
|
// Group matches by round
|
|
const matchesByRound: Record<string, typeof matches> = {};
|
|
for (const match of matches) {
|
|
if (!matchesByRound[match.round]) {
|
|
matchesByRound[match.round] = [];
|
|
}
|
|
matchesByRound[match.round].push(match);
|
|
}
|
|
|
|
const rounds = ["Quarterfinals", "Semifinals", "Finals"];
|
|
const availableRounds = rounds.filter(r => matchesByRound[r]);
|
|
|
|
// Get participants not yet in any match
|
|
const participantsInMatches = new Set(
|
|
matches.flatMap(m => [m.participant1Id, m.participant2Id].filter(Boolean))
|
|
);
|
|
const availableParticipants = participants.filter(
|
|
(p: { id: string }) => !participantsInMatches.has(p.id)
|
|
);
|
|
|
|
return (
|
|
<div className="p-8">
|
|
<div className="max-w-6xl mx-auto">
|
|
<div className="mb-6">
|
|
<Button variant="ghost" size="sm" asChild className="mb-2">
|
|
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}`}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Back to Event
|
|
</Link>
|
|
</Button>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Playoff Bracket</h1>
|
|
<p className="text-muted-foreground mt-1">
|
|
{event.name} - {sportsSeason.sport.name} {sportsSeason.name}
|
|
</p>
|
|
</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>
|
|
)}
|
|
|
|
{/* Generate Bracket Form */}
|
|
{matches.length === 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Generate Bracket</CardTitle>
|
|
<CardDescription>
|
|
Create the bracket structure for this playoff event
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Form method="post" className="space-y-4">
|
|
<input type="hidden" name="intent" value="generate-bracket" />
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bracketSize">Bracket Size</Label>
|
|
<Select name="bracketSize" defaultValue="8" required>
|
|
<SelectTrigger id="bracketSize">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="4">4 teams (Semifinals + Finals)</SelectItem>
|
|
<SelectItem value="8">8 teams (Quarterfinals + Semifinals + Finals)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Select Participants (in order)</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Select {8} participants for the bracket
|
|
</p>
|
|
{[...Array(8)].map((_, i) => (
|
|
<Select key={i} name={`participant${i}`} required>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={`Seed ${i + 1}`} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{participants.map((participant: { id: string; name: string }) => (
|
|
<SelectItem key={participant.id} value={participant.id}>
|
|
{participant.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
))}
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Generate Bracket
|
|
</Button>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Bracket Display */}
|
|
{availableRounds.map(round => (
|
|
<Card key={round}>
|
|
<CardHeader>
|
|
<CardTitle>{round}</CardTitle>
|
|
<CardDescription>
|
|
{matchesByRound[round].length} {matchesByRound[round].length === 1 ? "match" : "matches"}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-16">Match</TableHead>
|
|
<TableHead>Participant 1</TableHead>
|
|
<TableHead>Score</TableHead>
|
|
<TableHead className="w-24 text-center">vs</TableHead>
|
|
<TableHead>Score</TableHead>
|
|
<TableHead>Participant 2</TableHead>
|
|
<TableHead>Winner</TableHead>
|
|
<TableHead className="w-32">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{matchesByRound[round].map((match) => (
|
|
<TableRow key={match.id}>
|
|
<TableCell className="font-semibold">
|
|
{match.matchNumber}
|
|
</TableCell>
|
|
<TableCell>
|
|
{match.participant1?.name || "TBD"}
|
|
</TableCell>
|
|
<TableCell>
|
|
{match.participant1Score ? parseFloat(match.participant1Score) : "-"}
|
|
</TableCell>
|
|
<TableCell className="text-center text-muted-foreground">
|
|
vs
|
|
</TableCell>
|
|
<TableCell>
|
|
{match.participant2Score ? parseFloat(match.participant2Score) : "-"}
|
|
</TableCell>
|
|
<TableCell>
|
|
{match.participant2?.name || "TBD"}
|
|
</TableCell>
|
|
<TableCell>
|
|
{match.winner?.name ? (
|
|
<div className="flex items-center gap-1">
|
|
<Trophy className="h-4 w-4 text-yellow-500" />
|
|
{match.winner.name}
|
|
</div>
|
|
) : (
|
|
"-"
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{!match.isComplete && match.participant1Id && match.participant2Id ? (
|
|
<Form method="post" className="inline">
|
|
<input type="hidden" name="intent" value="set-winner" />
|
|
<input type="hidden" name="matchId" value={match.id} />
|
|
<Select name="winnerId" required>
|
|
<SelectTrigger className="h-8 w-full">
|
|
<SelectValue placeholder="Set winner" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={match.participant1Id}>
|
|
{match.participant1?.name}
|
|
</SelectItem>
|
|
<SelectItem value={match.participant2Id}>
|
|
{match.participant2?.name}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button type="submit" size="sm" className="ml-2 h-8">
|
|
Set
|
|
</Button>
|
|
</Form>
|
|
) : match.isComplete ? (
|
|
<span className="text-sm text-muted-foreground">Complete</span>
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">Waiting</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
|
|
{/* Complete Round Button */}
|
|
{availableRounds.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Complete Round</CardTitle>
|
|
<CardDescription>
|
|
Finalize the current round and calculate placements
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Form method="post">
|
|
<input type="hidden" name="intent" value="complete-round" />
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="round">Round to Complete</Label>
|
|
<Select name="round" required>
|
|
<SelectTrigger id="round">
|
|
<SelectValue placeholder="Select round" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableRounds.map(round => (
|
|
<SelectItem key={round} value={round}>
|
|
{round}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button type="submit">
|
|
Complete Round & Calculate Placements
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|