From 02186dcdfab34307155f2aeb45bd6cbe8928a7e0 Mon Sep 17 00:00:00 2001 From: Zeeshaun Date: Wed, 6 May 2026 16:10:07 +0000 Subject: [PATCH] Updating --- baseball/package-lock.json | 56 +++ baseball/package.json | 3 + baseball/src/app/api/lineups/[id]/route.ts | 23 ++ baseball/src/app/api/lineups/route.ts | 16 +- baseball/src/app/pitching/page.tsx | 5 + baseball/src/components/AppShell.tsx | 21 +- baseball/src/components/LineupClient.tsx | 319 ++++++++++++++---- .../src/components/PitchTrackingClient.tsx | 180 ++++++++++ baseball/src/components/PlayerForm.tsx | 2 +- baseball/src/components/RosterClient.tsx | 102 +----- baseball/src/components/SectionCard.tsx | 4 +- baseball/src/components/ui/card.tsx | 4 +- baseball/src/lib/types.ts | 3 + baseball/src/models/Lineup.ts | 6 +- baseball/tsconfig.tsbuildinfo | 2 +- 15 files changed, 557 insertions(+), 189 deletions(-) create mode 100644 baseball/src/app/api/lineups/[id]/route.ts create mode 100644 baseball/src/app/pitching/page.tsx create mode 100644 baseball/src/components/PitchTrackingClient.tsx diff --git a/baseball/package-lock.json b/baseball/package-lock.json index 59156a3..3fa1e43 100644 --- a/baseball/package-lock.json +++ b/baseball/package-lock.json @@ -8,6 +8,9 @@ "name": "little-league-roster-manager", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-dialog": "^1.1.15", "autoprefixer": "^10.5.0", "class-variance-authority": "^0.7.1", @@ -42,6 +45,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", diff --git a/baseball/package.json b/baseball/package.json index cf226d9..c35d3c6 100644 --- a/baseball/package.json +++ b/baseball/package.json @@ -9,6 +9,9 @@ "lint": "next lint" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-dialog": "^1.1.15", "autoprefixer": "^10.5.0", "class-variance-authority": "^0.7.1", diff --git a/baseball/src/app/api/lineups/[id]/route.ts b/baseball/src/app/api/lineups/[id]/route.ts new file mode 100644 index 0000000..b5f8672 --- /dev/null +++ b/baseball/src/app/api/lineups/[id]/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { handleApiError } from "@/app/api/_utils"; +import { connectToDatabase } from "@/lib/db"; +import { LineupModel } from "@/models/Lineup"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { + try { + await connectToDatabase(); + const { id } = await params; + const lineup = await LineupModel.findByIdAndDelete(id).lean(); + + if (!lineup) { + return NextResponse.json({ error: "Lineup not found." }, { status: 404 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/baseball/src/app/api/lineups/route.ts b/baseball/src/app/api/lineups/route.ts index cead9cf..0ef5c62 100644 --- a/baseball/src/app/api/lineups/route.ts +++ b/baseball/src/app/api/lineups/route.ts @@ -15,7 +15,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url); const gameDate = searchParams.get("gameDate"); const filter = gameDate ? { gameDate: new Date(gameDate) } : {}; - const lineups = await LineupModel.find(filter).sort({ gameDate: -1 }).lean(); + const lineups = await LineupModel.find(filter).sort({ gameDate: -1, lineupNumber: -1 }).lean(); return NextResponse.json(serialize(lineups)); } catch (error) { return handleApiError(error); @@ -26,8 +26,12 @@ export async function POST(request: Request) { try { await connectToDatabase(); const payload = saveLineupSchema.parse(await request.json()); + const gameDate = new Date(payload.gameDate); + const latestForDate = await LineupModel.findOne({ gameDate }).sort({ lineupNumber: -1 }).select("lineupNumber").lean(); + const lineupNumber = (latestForDate?.lineupNumber ?? 0) + 1; const lineupPayload = { - gameDate: new Date(payload.gameDate), + gameDate, + lineupNumber, playerIds: payload.playerIds, startingPitcherId: payload.startingPitcherId ?? null, battingOrder: payload.battingOrder, @@ -36,11 +40,7 @@ export async function POST(request: Request) { notes: payload.notes }; - const lineup = await LineupModel.findOneAndUpdate({ gameDate: lineupPayload.gameDate }, lineupPayload, { - upsert: true, - new: true, - runValidators: true - }).lean(); + const lineup = await LineupModel.create(lineupPayload); await GameModel.findOneAndUpdate( { gameDate: lineupPayload.gameDate }, @@ -52,7 +52,7 @@ export async function POST(request: Request) { { upsert: true, new: true } ); - return NextResponse.json(serialize(lineup), { status: 201 }); + return NextResponse.json(serialize(lineup.toObject()), { status: 201 }); } catch (error) { return handleApiError(error); } diff --git a/baseball/src/app/pitching/page.tsx b/baseball/src/app/pitching/page.tsx new file mode 100644 index 0000000..dd680fc --- /dev/null +++ b/baseball/src/app/pitching/page.tsx @@ -0,0 +1,5 @@ +import { PitchTrackingClient } from "@/components/PitchTrackingClient"; + +export default function PitchTrackingPage() { + return ; +} diff --git a/baseball/src/components/AppShell.tsx b/baseball/src/components/AppShell.tsx index 74c2797..fe92970 100644 --- a/baseball/src/components/AppShell.tsx +++ b/baseball/src/components/AppShell.tsx @@ -1,6 +1,6 @@ "use client"; -import { BarChart3, ClipboardList, ShieldCheck, Trophy, Users } from "lucide-react"; +import { BarChart3, ClipboardList, ShieldCheck, TimerReset, Trophy, Users } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; @@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"; const navItems = [ { href: "/dashboard", label: "Dashboard", icon: BarChart3 }, { href: "/roster", label: "Roster", icon: Users }, + { href: "/pitching", label: "Pitch Tracking", icon: TimerReset }, { href: "/lineup", label: "Generate Lineup", icon: ClipboardList }, { href: "/settings", label: "Pitching Rules", icon: ShieldCheck } ]; @@ -26,24 +27,24 @@ export function AppShell({ const pathname = usePathname(); return ( -
-
+
+
-
+
-
+
Little League Coach Console
-

{title}

-

{subtitle}

+

{title}

+

{subtitle}

{actions ?
{actions}
: null}
-
); } diff --git a/baseball/src/components/LineupClient.tsx b/baseball/src/components/LineupClient.tsx index fb6f034..8d294b0 100644 --- a/baseball/src/components/LineupClient.tsx +++ b/baseball/src/components/LineupClient.tsx @@ -1,7 +1,27 @@ "use client"; +import { + closestCenter, + DndContext, + KeyboardSensor, + PointerSensor, + TouchSensor, + useSensor, + useSensors, + type DragEndEvent +} from "@dnd-kit/core"; +import { + SortableContext, + arrayMove, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { GripVertical, Trash2 } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/AppShell"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; import { EmptyState } from "@/components/EmptyState"; import { SectionCard } from "@/components/SectionCard"; import { StatusBadge } from "@/components/StatusBadge"; @@ -12,7 +32,68 @@ import { Input } from "@/components/ui/input"; import { fetchJson } from "@/lib/client"; import { FIELD_POSITIONS } from "@/lib/constants"; import { formatPlayerName } from "@/lib/utils"; -import type { GeneratedLineup, LineupRecord, PlayerRecord } from "@/lib/types"; +import type { BattingSlot, GeneratedLineup, LineupRecord, PlayerRecord } from "@/lib/types"; + +function SortableBattingOrderItem({ + slot, + index, + selectedPlayers, + onChange +}: { + slot: BattingSlot; + index: number; + selectedPlayers: PlayerRecord[]; + onChange: (playerId: string) => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ + id: slot.playerId + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition + }; + + return ( +
+ +
+

+ Drag +

+ +

#{index + 1}

+
+ +
+ ); +} export function LineupClient() { const [gameDate, setGameDate] = useState(new Date().toISOString().slice(0, 10)); @@ -21,9 +102,19 @@ export function LineupClient() { const [startingPitcherId, setStartingPitcherId] = useState(""); const [showAllPitchers, setShowAllPitchers] = useState(false); const [generated, setGenerated] = useState(null); + const [savedLineups, setSavedLineups] = useState([]); + const [lineupPendingDelete, setLineupPendingDelete] = useState(null); const [message, setMessage] = useState(null); const [error, setError] = useState(null); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(TouchSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ); + async function loadPlayers(targetDate: string) { try { const response = await fetchJson(`/api/players?gameDate=${targetDate}`); @@ -37,8 +128,18 @@ export function LineupClient() { } } + async function loadSavedLineups(targetDate: string) { + try { + const response = await fetchJson(`/api/lineups?gameDate=${targetDate}`); + setSavedLineups(response); + } catch (loadError) { + setError(loadError instanceof Error ? loadError.message : "Failed to load saved lineups."); + } + } + useEffect(() => { void loadPlayers(gameDate); + void loadSavedLineups(gameDate); }, [gameDate]); const selectablePitchers = useMemo( @@ -60,6 +161,34 @@ export function LineupClient() { setGenerated(next); } + function updateBattingOrder(order: BattingSlot[]) { + if (!generated) { + return; + } + + updateGenerated({ + ...generated, + battingOrder: order.map((entry, index) => ({ ...entry, slot: index + 1 })) + }); + } + + function handleBattingDragEnd(event: DragEndEvent) { + if (!generated || !event.over || event.active.id === event.over.id) { + return; + } + + const overId = event.over.id; + + const oldIndex = generated.battingOrder.findIndex((slot) => slot.playerId === event.active.id); + const newIndex = generated.battingOrder.findIndex((slot) => slot.playerId === overId); + + if (oldIndex < 0 || newIndex < 0) { + return; + } + + updateBattingOrder(arrayMove(generated.battingOrder, oldIndex, newIndex)); + } + async function generate() { try { const response = await fetchJson("/api/lineups/generate", { @@ -81,29 +210,60 @@ export function LineupClient() { if (!generated) return; try { - await fetchJson("/api/lineups", { + const saved = await fetchJson("/api/lineups", { method: "POST", body: JSON.stringify({ ...generated, playerIds: selectedPlayerIds }) }); - setMessage("Lineup saved."); + setMessage(`Saved as lineup #${saved.lineupNumber} for ${saved.gameDate}.`); + await loadSavedLineups(gameDate); } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "Unable to save lineup."); } } + async function deleteSavedLineup(lineup: LineupRecord) { + if (!lineup._id) { + return; + } + + try { + await fetchJson(`/api/lineups/${lineup._id}`, { + method: "DELETE" + }); + setMessage(`Deleted lineup #${lineup.lineupNumber}.`); + setLineupPendingDelete(null); + await loadSavedLineups(gameDate); + } catch (deleteError) { + setError(deleteError instanceof Error ? deleteError.message : "Unable to delete lineup."); + } + } + return ( + setLineupPendingDelete(null)} + onConfirm={() => (lineupPendingDelete ? void deleteSavedLineup(lineupPendingDelete) : undefined)} + /> + {message ? {message} : null} {error ? {error} : null} - -
+ +
-
- -
-
+
{players.map((player) => { const selected = selectedPlayerIds.includes(player._id); return ( @@ -149,8 +309,8 @@ export function LineupClient() { key={player._id} className={selected ? "border-primary/50 bg-primary/5 ring-1 ring-primary/20" : ""} > - -