diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..bd98b4f --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": [ + "shadcn@latest", + "mcp" + ] + } + } +} diff --git a/app/routes.ts b/app/routes.ts index 4a20640..108a74a 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -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"), diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts new file mode 100644 index 0000000..2ba8bb2 --- /dev/null +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -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" }; +} diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx new file mode 100644 index 0000000..88f5f6e --- /dev/null +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx @@ -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 ( +
+ {sportsSeason.sport.name} - {sportsSeason.name} •{" "} + {getEventTypeLabel(event.eventType)} + {event.playoffRound && ` • ${event.playoffRound}`} +
++ No results added yet. Add results using the form above. +
+ ) : ( ++ {sportsSeason.sport.name} - {sportsSeason.name} +
++ No events created yet. Create your first event above. +
+ ) : ( +