This commit is contained in:
Zeeshaun
2026-05-06 16:27:48 +00:00
parent 02186dcdfa
commit 1d587599fc
17 changed files with 834 additions and 136 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { BarChart3, ClipboardList, ShieldCheck, TimerReset, Trophy, Users } from "lucide-react";
import { BarChart3, ClipboardList, FileText, ShieldCheck, TimerReset, Trophy, Users } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
@@ -9,6 +9,7 @@ const navItems = [
{ href: "/dashboard", label: "Dashboard", icon: BarChart3 },
{ href: "/roster", label: "Roster", icon: Users },
{ href: "/pitching", label: "Pitch Tracking", icon: TimerReset },
{ href: "/games", label: "Score Game", icon: FileText },
{ href: "/lineup", label: "Generate Lineup", icon: ClipboardList },
{ href: "/settings", label: "Pitching Rules", icon: ShieldCheck }
];
@@ -35,7 +36,7 @@ export function AppShell({
<div className="max-w-3xl">
<div className="mb-3 inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-medium text-white/80">
<Trophy className="h-4 w-4" />
Little League Coach Console
Coach Connor's Lineup Lab
</div>
<h1 className="font-display text-3xl font-semibold leading-none tracking-tight md:text-5xl">{title}</h1>
<p className="mt-3 max-w-2xl text-sm text-white/70 md:text-base">{subtitle}</p>
+33 -24
View File
@@ -15,6 +15,8 @@ export function DashboardClient() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const visiblePitching = data?.recentPitching.filter((entry) => entry.status !== "unknown") ?? [];
async function loadDashboard() {
setLoading(true);
setError(null);
@@ -66,31 +68,38 @@ export function DashboardClient() {
title="Pitcher Availability"
description="Availability is based on the latest appearance and your configured rest rules."
>
<div className="divide-y divide-border">
{data.recentPitching.map((entry) => (
<div
key={entry.playerId}
className="flex flex-col gap-3 py-4 md:flex-row md:items-start md:justify-between"
>
<div>
<h3 className="text-lg font-semibold">{entry.playerName}</h3>
<p className="text-sm text-muted-foreground">
{entry.lastPitchedDate
? `${entry.pitchCount} pitches on ${entry.lastPitchedDate}`
: "No pitching history recorded yet."}
</p>
{visiblePitching.length > 0 ? (
<div className="divide-y divide-border">
{visiblePitching.map((entry) => (
<div
key={entry.playerId}
className="flex flex-col gap-3 py-4 md:flex-row md:items-start md:justify-between"
>
<div>
<h3 className="text-lg font-semibold">{entry.playerName}</h3>
<p className="text-sm text-muted-foreground">
{entry.lastPitchedDate
? `${entry.pitchCount} pitches on ${entry.lastPitchedDate}`
: "No pitching history recorded yet."}
</p>
</div>
<div className="flex flex-col items-start gap-2 md:items-end">
<StatusBadge status={entry.status} />
<p className="text-sm text-muted-foreground">
{entry.daysSinceLastAppearance === null
? "No history"
: `${entry.daysSinceLastAppearance} day(s) since last outing`}
</p>
</div>
</div>
<div className="flex flex-col items-start gap-2 md:items-end">
<StatusBadge status={entry.status} />
<p className="text-sm text-muted-foreground">
{entry.daysSinceLastAppearance === null
? "No history"
: `${entry.daysSinceLastAppearance} day(s) since last outing`}
</p>
</div>
</div>
))}
</div>
))}
</div>
) : (
<EmptyState
title="No eligible pitchers to show"
description="Pitchers with unknown availability are hidden from this dashboard view."
/>
)}
</SectionCard>
</>
) : (
@@ -0,0 +1,321 @@
"use client";
import { FileText, 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 { Alert } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { fetchJson } from "@/lib/client";
import type { GamePlayerStatsInput, GameRecord, LineupRecord } from "@/lib/types";
const statFields = [
["atBats", "AB"],
["runs", "R"],
["hits", "H"],
["doubles", "2B"],
["triples", "3B"],
["homeRuns", "HR"],
["runsBattedIn", "RBI"],
["walks", "BB"],
["strikeouts", "SO"],
["stolenBases", "SB"]
] as const;
function pickDefaultLineupId(gameDate: string, lineups: LineupRecord[]) {
const exactDate = lineups.find((lineup) => lineup.gameDate === gameDate);
return exactDate?._id ?? lineups[0]?._id ?? "";
}
export function GameScoringClient() {
const [gameDate, setGameDate] = useState(new Date().toISOString().slice(0, 10));
const [lineups, setLineups] = useState<LineupRecord[]>([]);
const [selectedLineupId, setSelectedLineupId] = useState("");
const [savedGames, setSavedGames] = useState<GameRecord[]>([]);
const [gamePendingDelete, setGamePendingDelete] = useState<GameRecord | null>(null);
const [opponent, setOpponent] = useState("");
const [teamScore, setTeamScore] = useState<string>("");
const [opponentScore, setOpponentScore] = useState<string>("");
const [notes, setNotes] = useState("");
const [playerStats, setPlayerStats] = useState<Record<string, GamePlayerStatsInput>>({});
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function loadLineups() {
try {
const response = await fetchJson<LineupRecord[]>("/api/lineups");
setLineups(response);
setSelectedLineupId((current) => (current && response.some((lineup) => lineup._id === current) ? current : pickDefaultLineupId(gameDate, response)));
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load lineups.");
}
}
async function loadSavedGames(targetDate: string) {
try {
const response = await fetchJson<GameRecord[]>(`/api/games?gameDate=${targetDate}`);
setSavedGames(response);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load games.");
}
}
useEffect(() => {
void loadLineups();
}, []);
useEffect(() => {
void loadSavedGames(gameDate);
setSelectedLineupId((current) => {
if (current && lineups.some((lineup) => lineup._id === current)) {
return current;
}
return pickDefaultLineupId(gameDate, lineups);
});
}, [gameDate, lineups]);
const selectedLineup = useMemo(
() => lineups.find((lineup) => lineup._id === selectedLineupId) ?? null,
[lineups, selectedLineupId]
);
useEffect(() => {
if (!selectedLineup) {
setPlayerStats({});
return;
}
setPlayerStats((current) =>
Object.fromEntries(
selectedLineup.battingOrder.map((slot) => [
slot.playerId,
current[slot.playerId] ?? {
playerId: slot.playerId,
playerName: slot.playerName
}
])
)
);
}, [selectedLineup]);
function updatePlayerStat(playerId: string, playerName: string, field: keyof Omit<GamePlayerStatsInput, "playerId" | "playerName">, value: string) {
setPlayerStats((current) => ({
...current,
[playerId]: {
...(current[playerId] ?? { playerId, playerName }),
playerId,
playerName,
[field]: value === "" ? null : Number(value)
}
}));
}
async function saveGame() {
if (!selectedLineup || !selectedLineup._id) {
setError("Select a saved lineup before scoring the game.");
return;
}
try {
const payload = {
gameDate,
lineupId: selectedLineup._id,
opponent,
teamScore: teamScore === "" ? null : Number(teamScore),
opponentScore: opponentScore === "" ? null : Number(opponentScore),
notes,
playerStats: selectedLineup.battingOrder.map((slot) => playerStats[slot.playerId] ?? { playerId: slot.playerId, playerName: slot.playerName })
};
const saved = await fetchJson<GameRecord>("/api/games", {
method: "POST",
body: JSON.stringify(payload)
});
setMessage(`Saved game #${saved.gameNumber} for ${saved.gameDate}.`);
setOpponent("");
setTeamScore("");
setOpponentScore("");
setNotes("");
setPlayerStats({});
await loadSavedGames(gameDate);
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "Unable to save game.");
}
}
async function deleteGame(game: GameRecord) {
if (!game._id) return;
try {
await fetchJson(`/api/games/${game._id}`, { method: "DELETE" });
setMessage(`Deleted game #${game.gameNumber}.`);
setGamePendingDelete(null);
await loadSavedGames(gameDate);
} catch (deleteError) {
setError(deleteError instanceof Error ? deleteError.message : "Unable to delete game.");
}
}
return (
<AppShell
title="Score Game"
subtitle="Pick the lineup used, enter the results, and roll the stats into each player automatically."
>
<ConfirmDialog
open={gamePendingDelete !== null}
title="Delete Saved Game?"
description={
gamePendingDelete
? `Delete game #${gamePendingDelete.gameNumber} for ${gamePendingDelete.gameDate}? Player stats from that game will be reversed.`
: ""
}
confirmLabel="Delete Game"
onCancel={() => setGamePendingDelete(null)}
onConfirm={() => (gamePendingDelete ? void deleteGame(gamePendingDelete) : undefined)}
/>
{message ? <Alert tone="success">{message}</Alert> : null}
{error ? <Alert tone="error">{error}</Alert> : null}
<SectionCard
title="Game Setup"
description="Choose the lineup used in the game. The latest saved lineup is selected by default."
>
{lineups.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2">
<label>
<span className="field-label">Game Date</span>
<Input type="date" value={gameDate} onChange={(event) => setGameDate(event.target.value)} />
</label>
<label>
<span className="field-label">Lineup Used</span>
<select className="field-select" value={selectedLineupId} onChange={(event) => setSelectedLineupId(event.target.value)}>
{lineups.map((lineup) => (
<option key={lineup._id} value={lineup._id}>
{lineup.gameDate} · Lineup #{lineup.lineupNumber}
</option>
))}
</select>
</label>
<label>
<span className="field-label">Opponent</span>
<Input value={opponent} onChange={(event) => setOpponent(event.target.value)} placeholder="Optional" />
</label>
<div className="grid grid-cols-2 gap-3">
<label>
<span className="field-label">Our Score</span>
<Input type="number" min={0} value={teamScore} onChange={(event) => setTeamScore(event.target.value)} />
</label>
<label>
<span className="field-label">Their Score</span>
<Input type="number" min={0} value={opponentScore} onChange={(event) => setOpponentScore(event.target.value)} />
</label>
</div>
<label className="md:col-span-2">
<span className="field-label">Game Notes</span>
<Textarea rows={3} value={notes} onChange={(event) => setNotes(event.target.value)} />
</label>
</div>
) : (
<EmptyState
title="No lineup available"
description="Save a lineup on the lineup tab first, then return here to score the game."
/>
)}
</SectionCard>
{selectedLineup ? (
<SectionCard
title="Player Results"
description="Stat entry is optional. Leave any field blank if you do not want to track it for that player."
>
<div className="grid gap-3">
{selectedLineup.battingOrder.map((slot, index) => {
const statLine = playerStats[slot.playerId] ?? { playerId: slot.playerId, playerName: slot.playerName };
return (
<Card key={slot.playerId}>
<CardContent className="grid gap-3 pt-4">
<div className="flex items-center gap-3">
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-muted text-xs font-semibold text-muted-foreground">
{index + 1}
</div>
<div>
<h3 className="text-sm font-semibold">{slot.playerName}</h3>
<p className="text-xs text-muted-foreground">Batting order slot {index + 1}</p>
</div>
</div>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5">
{statFields.map(([field, label]) => (
<label key={field}>
<span className="field-label">{label}</span>
<Input
type="number"
min={0}
value={statLine[field] ?? ""}
onChange={(event) => updatePlayerStat(slot.playerId, slot.playerName, field, event.target.value)}
/>
</label>
))}
</div>
</CardContent>
</Card>
);
})}
<div>
<Button onClick={() => void saveGame()}>Save Game</Button>
</div>
</div>
</SectionCard>
) : null}
<SectionCard
title="Saved Games"
description="Previously saved games for the selected date can be deleted."
>
{savedGames.length > 0 ? (
<div className="grid gap-3">
{savedGames.map((game) => (
<Card key={game._id}>
<CardContent className="flex items-start justify-between gap-3 pt-4">
<div className="flex items-start gap-3">
<div className="mt-0.5 rounded-full bg-muted p-2 text-muted-foreground">
<FileText className="h-4 w-4" />
</div>
<div>
<h3 className="text-sm font-semibold">
{game.gameDate} · Game #{game.gameNumber}
</h3>
<p className="mt-1 text-xs text-muted-foreground">
Lineup #{game.lineupNumber ?? "?"}
{game.opponent ? ` · vs ${game.opponent}` : ""}
{game.teamScore !== null && game.teamScore !== undefined && game.opponentScore !== null && game.opponentScore !== undefined
? ` · ${game.teamScore}-${game.opponentScore}`
: ""}
</p>
</div>
</div>
<Button size="sm" variant="destructive" className="gap-1" onClick={() => setGamePendingDelete(game)}>
<Trash2 className="h-3.5 w-3.5" />
Delete
</Button>
</CardContent>
</Card>
))}
</div>
) : (
<EmptyState
title="No saved games"
description="Score and save a game to keep a dated history here."
/>
)}
</SectionCard>
</AppShell>
);
}
@@ -25,6 +25,8 @@ export function PitchTrackingClient() {
notes: ""
});
const visiblePitchers = players.filter((player) => player.pitcherAvailability.status !== "unknown");
async function loadPlayers() {
setLoading(true);
setError(null);
@@ -142,9 +144,9 @@ export function PitchTrackingClient() {
title="Pitcher Snapshot"
description="Quick read of each player’s latest pitching status."
>
{players.length > 0 ? (
{visiblePitchers.length > 0 ? (
<div className="grid gap-3">
{players.map((player) => (
{visiblePitchers.map((player) => (
<Card key={player._id}>
<CardContent className="flex items-start justify-between gap-3 pt-4">
<div>
@@ -170,8 +172,8 @@ export function PitchTrackingClient() {
</div>
) : (
<EmptyState
title="No pitching data yet"
description="Log the first appearance above once your roster is in place."
title="No eligible pitchers to show"
description="Pitchers with unknown availability are hidden from this snapshot."
/>
)}
</SectionCard>
+47 -2
View File
@@ -15,7 +15,8 @@ const defaultForm: PlayerInput = {
secondaryPosition: "C",
tertiaryPosition: "1B",
isActive: true,
notes: ""
notes: "",
stats: {}
};
export function emptyPlayerForm() {
@@ -32,10 +33,25 @@ export function toPlayerForm(player: PlayerRecord): PlayerInput {
secondaryPosition: player.secondaryPosition ?? null,
tertiaryPosition: player.tertiaryPosition ?? null,
isActive: player.isActive,
notes: player.notes ?? ""
notes: player.notes ?? "",
stats: player.stats ?? {}
};
}
const statFields = [
["gamesPlayed", "Games"],
["atBats", "AB"],
["runs", "Runs"],
["hits", "Hits"],
["doubles", "2B"],
["triples", "3B"],
["homeRuns", "HR"],
["runsBattedIn", "RBI"],
["walks", "BB"],
["strikeouts", "SO"],
["stolenBases", "SB"]
] as const;
export function PlayerForm({
value,
onChange,
@@ -119,6 +135,35 @@ export function PlayerForm({
<span className="field-label">Notes</span>
<Textarea rows={4} value={value.notes ?? ""} onChange={(event) => onChange({ ...value, notes: event.target.value })} />
</label>
<div className="md:col-span-2 space-y-3 rounded-[1.5rem] border border-border bg-muted/40 p-4">
<div>
<p className="text-sm font-semibold">Optional Player Stats</p>
<p className="mt-1 text-xs text-muted-foreground">
Leave any field blank if you do not want to track it.
</p>
</div>
<div className="grid gap-3 grid-cols-2 md:grid-cols-4">
{statFields.map(([key, label]) => (
<label key={key}>
<span className="field-label">{label}</span>
<Input
type="number"
min={0}
value={value.stats?.[key] ?? ""}
onChange={(event) =>
onChange({
...value,
stats: {
...value.stats,
[key]: event.target.value === "" ? null : Number(event.target.value)
}
})
}
/>
</label>
))}
</div>
</div>
<div className="md:col-span-2">
<Button onClick={onSubmit}>
{submitLabel}
+111 -92
View File
@@ -20,6 +20,7 @@ export function RosterClient() {
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [editingPlayerId, setEditingPlayerId] = useState<string | null>(null);
const [showPlayerForm, setShowPlayerForm] = useState(false);
const [playerPendingDelete, setPlayerPendingDelete] = useState<PlayerRecord | null>(null);
const [playerForm, setPlayerForm] = useState<PlayerInput>(emptyPlayerForm());
const playerFormRef = useRef<HTMLDivElement | null>(null);
@@ -56,21 +57,40 @@ export function RosterClient() {
useEffect(() => {
if (editingPlayerId) {
setShowPlayerForm(true);
playerFormRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, [editingPlayerId]);
function startEditingPlayer(playerId: string) {
setEditingPlayerId(playerId);
setShowPlayerForm(true);
setMessage(null);
setError(null);
}
function stopEditingPlayer() {
setEditingPlayerId(null);
setShowPlayerForm(false);
setPlayerForm(emptyPlayerForm());
}
function startAddingPlayer() {
setEditingPlayerId(null);
setPlayerForm(emptyPlayerForm());
setShowPlayerForm((current) => {
const next = !current;
if (!current) {
requestAnimationFrame(() => {
playerFormRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
});
}
return next;
});
setMessage(null);
setError(null);
}
async function submitPlayer() {
try {
const url = editingPlayerId ? `/api/players/${editingPlayerId}` : "/api/players";
@@ -99,7 +119,7 @@ export function RosterClient() {
return (
<AppShell
title="Roster Management"
subtitle="Add players, update availability, and keep pitching history current from one screen."
subtitle="Review the active roster first, then open a compact add or edit form when you need it."
>
<ConfirmDialog
open={playerPendingDelete !== null}
@@ -117,104 +137,79 @@ export function RosterClient() {
{message ? <Alert tone="success">{message}</Alert> : null}
{error ? <Alert tone="error">{error}</Alert> : null}
<div ref={playerFormRef}>
<SectionCard
title={editingPlayerId ? "Edit Player" : "Add Player"}
description="Keep player details simple so a coach can update them quickly before game time."
>
{editingPlayer ? (
<Alert tone="info" className="flex items-center justify-between gap-4">
<span>Editing {formatPlayerName(editingPlayer.firstName, editingPlayer.lastName)}.</span>
<Button variant="ghost" size="sm" onClick={stopEditingPlayer}>
Cancel
</Button>
</Alert>
) : null}
<PlayerForm
value={playerForm}
onChange={setPlayerForm}
onSubmit={() => void submitPlayer()}
submitLabel={editingPlayerId ? "Save Changes" : "Add Player"}
/>
</SectionCard>
</div>
<SectionCard
title="Current Roster"
description="Tap edit for fast changes and check pitcher status at a glance."
>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={startAddingPlayer}>
{showPlayerForm && !editingPlayerId ? "Hide Add Player" : "Add Player"}
</Button>
</div>
{loading ? (
<p className="text-sm text-muted-foreground">Loading roster...</p>
) : players.length > 0 ? (
<div className="grid gap-3 xl:grid-cols-2">
{players.map((player) => (
<Card
key={player._id}
className={
editingPlayerId === player._id
? "border-secondary/60 bg-secondary/5 ring-1 ring-secondary/30"
: ""
}
>
<CardContent className="grid gap-4 pt-4 md:pt-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 className="text-lg font-semibold md:text-xl">
#{player.jerseyNumber} {formatPlayerName(player.firstName, player.lastName)}
</h3>
<p className="mt-1 text-sm text-muted-foreground">
{player.primaryPosition}
{player.secondaryPosition ? ` / ${player.secondaryPosition}` : ""}
{player.tertiaryPosition ? ` / ${player.tertiaryPosition}` : ""}
</p>
</div>
<StatusBadge status={player.pitcherAvailability.status} />
</div>
<div className="grid gap-3 rounded-[1.5rem] bg-muted/60 p-3 sm:grid-cols-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Bats/Throws
</p>
<p className="mt-1 text-sm font-medium">{player.batsThrows}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Status
</p>
<p className="mt-1 text-sm font-medium">
{player.isActive ? "Active" : "Inactive"}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Pitching
</p>
<p className="mt-1 text-sm font-medium">
{player.pitcherAvailability.lastPitchedDate
? `${player.pitcherAvailability.lastPitchCount} pitches on ${player.pitcherAvailability.lastPitchedDate}`
: "No history"}
</p>
</div>
</div>
{player.notes ? (
<div className="rounded-[1.5rem] border border-border/70 bg-white/70 px-4 py-3 text-sm text-muted-foreground">
{player.notes}
</div>
) : null}
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="secondary" onClick={() => startEditingPlayer(player._id)}>
{editingPlayerId === player._id ? "Editing" : "Edit"}
</Button>
<Button size="sm" variant="destructive" onClick={() => setPlayerPendingDelete(player)}>
Delete
</Button>
</div>
</CardContent>
</Card>
))}
<div className="overflow-x-auto rounded-[1.5rem] border border-border bg-white/70">
<table className="min-w-full text-left text-sm">
<thead className="border-b border-border bg-muted/60 text-xs uppercase tracking-[0.18em] text-muted-foreground">
<tr>
<th className="px-3 py-3 font-semibold">#</th>
<th className="px-3 py-3 font-semibold">Player</th>
<th className="px-3 py-3 font-semibold">Pos</th>
<th className="px-3 py-3 font-semibold">B/T</th>
<th className="px-3 py-3 font-semibold">Pitching</th>
<th className="px-3 py-3 font-semibold">Status</th>
<th className="px-3 py-3 font-semibold">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{players.map((player) => (
<tr
key={player._id}
className={
editingPlayerId === player._id ? "bg-secondary/5" : ""
}
>
<td className="px-3 py-3 align-top font-medium">{player.jerseyNumber}</td>
<td className="px-3 py-3 align-top">
<div className="font-medium">{formatPlayerName(player.firstName, player.lastName)}</div>
{player.notes ? (
<div className="mt-1 max-w-[18rem] text-xs text-muted-foreground">{player.notes}</div>
) : null}
</td>
<td className="px-3 py-3 align-top text-xs text-muted-foreground">
<div>{player.primaryPosition}</div>
{player.secondaryPosition ? <div>{player.secondaryPosition}</div> : null}
{player.tertiaryPosition ? <div>{player.tertiaryPosition}</div> : null}
</td>
<td className="px-3 py-3 align-top">{player.batsThrows}</td>
<td className="px-3 py-3 align-top text-xs text-muted-foreground">
{player.pitcherAvailability.lastPitchedDate
? `${player.pitcherAvailability.lastPitchCount} on ${player.pitcherAvailability.lastPitchedDate}`
: "No history"}
</td>
<td className="px-3 py-3 align-top">
<div className="flex flex-col gap-2">
<StatusBadge status={player.pitcherAvailability.status} />
<span className="text-xs text-muted-foreground">
{player.isActive ? "Active" : "Inactive"}
</span>
</div>
</td>
<td className="px-3 py-3 align-top">
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="secondary" onClick={() => startEditingPlayer(player._id)}>
{editingPlayerId === player._id ? "Editing" : "Edit"}
</Button>
<Button size="sm" variant="destructive" onClick={() => setPlayerPendingDelete(player)}>
Delete
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<EmptyState
@@ -223,6 +218,30 @@ export function RosterClient() {
/>
)}
</SectionCard>
{showPlayerForm || editingPlayerId ? (
<div ref={playerFormRef}>
<SectionCard
title={editingPlayerId ? "Edit Player" : "Add Player"}
description="Use the compact form below to update the roster."
>
{editingPlayer ? (
<Alert tone="info" className="flex items-center justify-between gap-4">
<span>Editing {formatPlayerName(editingPlayer.firstName, editingPlayer.lastName)}.</span>
<Button variant="ghost" size="sm" onClick={stopEditingPlayer}>
Cancel
</Button>
</Alert>
) : null}
<PlayerForm
value={playerForm}
onChange={setPlayerForm}
onSubmit={() => void submitPlayer()}
submitLabel={editingPlayerId ? "Save Changes" : "Add Player"}
/>
</SectionCard>
</div>
) : null}
</AppShell>
);
}
@@ -110,9 +110,6 @@ export function SettingsClient() {
</div>
<div className="flex flex-wrap gap-3">
<Button variant="secondary" onClick={() => setRules(DEFAULT_PITCHING_RULES)}>
Reset to Example Rules
</Button>
<Button onClick={() => void saveRules()}>Save Rules</Button>
</div>
</SectionCard>