feat: Implement event management for sports seasons with CRUD operations and UI
This commit is contained in:
parent
7dddf2c9a7
commit
60ba3d4a6e
7 changed files with 742 additions and 5 deletions
11
.mcp.json
Normal file
11
.mcp.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"shadcn@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,14 @@ export default [
|
|||
"sports-seasons/:id/participants",
|
||||
"routes/admin.sports-seasons.$id.participants.tsx"
|
||||
),
|
||||
route(
|
||||
"sports-seasons/:id/events",
|
||||
"routes/admin.sports-seasons.$id.events.tsx"
|
||||
),
|
||||
route(
|
||||
"sports-seasons/:id/events/:eventId",
|
||||
"routes/admin.sports-seasons.$id.events.$eventId.tsx"
|
||||
),
|
||||
route("participants", "routes/admin.participants.tsx"),
|
||||
route("templates", "routes/admin.templates.tsx"),
|
||||
route("templates/new", "routes/admin.templates.new.tsx"),
|
||||
|
|
|
|||
136
app/routes/admin.sports-seasons.$id.events.$eventId.server.ts
Normal file
136
app/routes/admin.sports-seasons.$id.events.$eventId.server.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId";
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import {
|
||||
getScoringEventById,
|
||||
completeScoringEvent,
|
||||
} from "~/models/scoring-event";
|
||||
import {
|
||||
getEventResults,
|
||||
createEventResult,
|
||||
updateEventResult,
|
||||
deleteEventResult,
|
||||
type CreateEventResultData,
|
||||
type UpdateEventResultData,
|
||||
} from "~/models/event-result";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const sportsSeason = await findSportsSeasonById(params.id);
|
||||
|
||||
if (!sportsSeason) {
|
||||
throw new Response("Sports season not found", { status: 404 });
|
||||
}
|
||||
|
||||
const event = await getScoringEventById(params.eventId);
|
||||
|
||||
if (!event) {
|
||||
throw new Response("Event not found", { status: 404 });
|
||||
}
|
||||
|
||||
const participants = await findParticipantsBySportsSeasonId(params.id);
|
||||
const results = await getEventResults(params.eventId);
|
||||
|
||||
return {
|
||||
sportsSeason: sportsSeason as typeof sportsSeason & {
|
||||
sport: { id: string; name: string; type: string; slug: string };
|
||||
},
|
||||
event,
|
||||
participants,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
export async function action({ request, params }: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "complete") {
|
||||
try {
|
||||
await completeScoringEvent(params.eventId);
|
||||
return { success: "Event marked as completed" };
|
||||
} catch (error) {
|
||||
console.error("Error completing event:", error);
|
||||
return { error: "Failed to complete event" };
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "add-result") {
|
||||
const participantId = formData.get("participantId");
|
||||
const placement = formData.get("placement");
|
||||
|
||||
if (typeof participantId !== "string" || !participantId) {
|
||||
return { error: "Participant is required" };
|
||||
}
|
||||
|
||||
if (typeof placement !== "string" || !placement) {
|
||||
return { error: "Placement is required" };
|
||||
}
|
||||
|
||||
const placementNum = parseInt(placement, 10);
|
||||
if (isNaN(placementNum) || placementNum < 1 || placementNum > 100) {
|
||||
return { error: "Placement must be between 1 and 100" };
|
||||
}
|
||||
|
||||
const resultData: CreateEventResultData = {
|
||||
scoringEventId: params.eventId,
|
||||
participantId,
|
||||
placement: placementNum,
|
||||
};
|
||||
|
||||
try {
|
||||
await createEventResult(resultData);
|
||||
return { success: "Result added successfully" };
|
||||
} catch (error) {
|
||||
console.error("Error adding result:", error);
|
||||
return { error: "Failed to add result" };
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "update-result") {
|
||||
const resultId = formData.get("resultId");
|
||||
const placement = formData.get("placement");
|
||||
|
||||
if (typeof resultId !== "string" || !resultId) {
|
||||
return { error: "Result ID is required" };
|
||||
}
|
||||
|
||||
if (typeof placement !== "string" || !placement) {
|
||||
return { error: "Placement is required" };
|
||||
}
|
||||
|
||||
const placementNum = parseInt(placement, 10);
|
||||
if (isNaN(placementNum) || placementNum < 1 || placementNum > 100) {
|
||||
return { error: "Placement must be between 1 and 100" };
|
||||
}
|
||||
|
||||
const updateData: UpdateEventResultData = {
|
||||
placement: placementNum,
|
||||
};
|
||||
|
||||
try {
|
||||
await updateEventResult(resultId, updateData);
|
||||
return { success: "Result updated successfully" };
|
||||
} catch (error) {
|
||||
console.error("Error updating result:", error);
|
||||
return { error: "Failed to update result" };
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "delete-result") {
|
||||
const resultId = formData.get("resultId");
|
||||
|
||||
if (typeof resultId !== "string" || !resultId) {
|
||||
return { error: "Result ID is required" };
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteEventResult(resultId);
|
||||
return { success: "Result deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Error deleting result:", error);
|
||||
return { error: "Failed to delete result" };
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "Invalid action" };
|
||||
}
|
||||
287
app/routes/admin.sports-seasons.$id.events.$eventId.tsx
Normal file
287
app/routes/admin.sports-seasons.$id.events.$eventId.tsx
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
82
app/routes/admin.sports-seasons.$id.events.server.ts
Normal file
82
app/routes/admin.sports-seasons.$id.events.server.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id.events";
|
||||
import { redirect } from "react-router";
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import {
|
||||
getScoringEventsForSportsSeason,
|
||||
createScoringEvent,
|
||||
deleteScoringEvent,
|
||||
type CreateScoringEventData,
|
||||
} from "~/models/scoring-event";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const sportsSeason = await findSportsSeasonById(params.id);
|
||||
|
||||
if (!sportsSeason) {
|
||||
throw new Response("Sports season not found", { status: 404 });
|
||||
}
|
||||
|
||||
const events = await getScoringEventsForSportsSeason(params.id);
|
||||
|
||||
return {
|
||||
sportsSeason: sportsSeason as typeof sportsSeason & {
|
||||
sport: { id: string; name: string; type: string; slug: string };
|
||||
},
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
export async function action({ request, params }: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "delete-event") {
|
||||
const eventId = formData.get("eventId");
|
||||
|
||||
if (typeof eventId !== "string" || !eventId) {
|
||||
return { error: "Event ID is required" };
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteScoringEvent(eventId);
|
||||
return { success: "Event deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Error deleting event:", error);
|
||||
return { error: "Failed to delete event" };
|
||||
}
|
||||
}
|
||||
|
||||
const name = formData.get("name");
|
||||
const eventType = formData.get("eventType");
|
||||
const eventDate = formData.get("eventDate");
|
||||
const playoffRound = formData.get("playoffRound");
|
||||
|
||||
// Validation
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
return { error: "Event name is required" };
|
||||
}
|
||||
|
||||
if (
|
||||
eventType !== "playoff_game" &&
|
||||
eventType !== "major_tournament" &&
|
||||
eventType !== "race" &&
|
||||
eventType !== "final_standings"
|
||||
) {
|
||||
return { error: "Invalid event type" };
|
||||
}
|
||||
|
||||
const eventData: CreateScoringEventData = {
|
||||
sportsSeasonId: params.id,
|
||||
name: name.trim(),
|
||||
eventType: eventType as "playoff_game" | "major_tournament" | "race" | "final_standings",
|
||||
eventDate: typeof eventDate === "string" && eventDate ? new Date(eventDate) : undefined,
|
||||
playoffRound: typeof playoffRound === "string" && playoffRound ? playoffRound : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
const event = await createScoringEvent(eventData);
|
||||
return redirect(`/admin/sports-seasons/${params.id}/events/${event.id}`);
|
||||
} catch (error) {
|
||||
console.error("Error creating scoring event:", error);
|
||||
return { error: "Failed to create scoring event. Please try again." };
|
||||
}
|
||||
}
|
||||
211
app/routes/admin.sports-seasons.$id.events.tsx
Normal file
211
app/routes/admin.sports-seasons.$id.events.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
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 { Calendar, Trophy, ArrowLeft, Trash2 } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export { loader, action };
|
||||
|
||||
export default function SportsSeasonEvents({
|
||||
loaderData,
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
const { sportsSeason, events } = loaderData;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (isComplete: boolean) => {
|
||||
return isComplete ? (
|
||||
<Badge variant="default" className="bg-green-500">
|
||||
Completed
|
||||
</Badge>
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Create New Event Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create New Event</CardTitle>
|
||||
<CardDescription>
|
||||
Add a new scoring event (game, tournament, race, 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="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="eventType">Event Type</Label>
|
||||
<Select name="eventType" defaultValue="playoff_game" required>
|
||||
<SelectTrigger id="eventType">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="playoff_game">Playoff Game</SelectItem>
|
||||
<SelectItem value="major_tournament">Major Tournament</SelectItem>
|
||||
<SelectItem value="race">Race</SelectItem>
|
||||
<SelectItem value="final_standings">Final Standings</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="playoffRound">Playoff Round (Optional)</Label>
|
||||
<Input
|
||||
id="playoffRound"
|
||||
name="playoffRound"
|
||||
type="text"
|
||||
placeholder="e.g., Quarterfinals, Round 1"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{/* 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; playoffRound?: string | null; 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)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>
|
||||
{getEventTypeLabel(event.eventType)}
|
||||
</span>
|
||||
{event.playoffRound && <span>{event.playoffRound}</span>}
|
||||
{event.eventDate && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{format(new Date(event.eventDate), "MMM d, yyyy")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<Form method="post" className="flex-shrink-0">
|
||||
<input type="hidden" name="intent" value="delete-event" />
|
||||
<input type="hidden" name="eventId" value={event.id} />
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
if (!confirm(`Delete event "${event.name}"? This will also delete all results.`)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -959,11 +959,13 @@ All clarification questions (Q1-Q21) have been answered and confirmed:
|
|||
- [x] Disable editing after draft starts (settings page only)
|
||||
- [x] Show helpful tips and preview
|
||||
|
||||
- [ ] **1.4** Basic admin result entry UI
|
||||
- [ ] Create admin route structure for sports seasons
|
||||
- [ ] List scoring events for a sports season
|
||||
- [ ] Create new event form
|
||||
- [ ] Basic result entry form (placement only)
|
||||
- [x] **1.4** Basic admin result entry UI ✅ *Completed*
|
||||
- [x] Create admin route structure for sports seasons
|
||||
- [x] List scoring events for a sports season
|
||||
- [x] Create new event form
|
||||
- [x] Basic result entry form (placement only)
|
||||
- [x] Update existing results (inline editing)
|
||||
- [x] Delete results with confirmation
|
||||
|
||||
### Phase 2: Playoff Scoring (Single Elimination)
|
||||
**Goal**: Implement playoff bracket tracking and scoring for NFL/NBA/MLB
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue