This commit is contained in:
Zeeshaun
2026-05-06 18:31:27 +00:00
parent 2263dc3aa5
commit c48023ace4
11 changed files with 404 additions and 97 deletions
+72 -27
View File
@@ -10,11 +10,11 @@ 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 { getBattingSummary, getPitchingSummary, hasBattingStats, hasPitchingStats } from "@/lib/baseball-stats";
import { fetchJson } from "@/lib/client";
import type { GameRecord } from "@/lib/types";
const statFields = [
["pitchCount", "Pitches"],
const battingFields = [
["atBats", "AB"],
["runs", "R"],
["hits", "H"],
@@ -27,6 +27,15 @@ const statFields = [
["stolenBases", "SB"]
] as const;
const pitchingFields = [
["pitchCount", "Pitches"],
["inningsPitchedOuts", "IP Outs"],
["earnedRunsAllowed", "ER"],
["hitsAllowed", "H Allowed"],
["walksAllowed", "BB Allowed"],
["strikeoutsPitched", "Pitching K"]
] as const;
export function GameHistoryClient() {
const [gameDate, setGameDate] = useState("");
const [games, setGames] = useState<GameRecord[]>([]);
@@ -165,31 +174,67 @@ export function GameHistoryClient() {
</div>
) : null}
<div className="overflow-x-auto rounded-[1rem] border border-border bg-white/70">
<table className="min-w-full text-left text-xs">
<thead className="border-b border-border bg-muted/60 uppercase tracking-[0.18em] text-muted-foreground">
<tr>
<th className="px-3 py-2 font-semibold">Player</th>
{statFields.map(([, label]) => (
<th key={label} className="px-3 py-2 font-semibold">
{label}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border">
{game.playerStats.map((playerStat) => (
<tr key={playerStat.playerId}>
<td className="px-3 py-2 font-medium">{playerStat.playerName}</td>
{statFields.map(([field, label]) => (
<td key={label} className="px-3 py-2">
{playerStat[field] ?? "-"}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="grid gap-3">
{game.playerStats.map((playerStat) => (
<div key={playerStat.playerId} className="rounded-[1rem] border border-border bg-white/70 p-3">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-semibold">{playerStat.playerName}</p>
<p className="text-xs text-muted-foreground">Saved stat line</p>
</div>
<div className="mt-3 grid gap-3 md:grid-cols-2">
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Hitting Summary
</p>
{hasBattingStats(playerStat) ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
{getBattingSummary(playerStat).map(([label, value]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{value ?? "-"}</span>
</div>
))}
</div>
) : (
<p className="text-xs text-muted-foreground">No batting stats recorded.</p>
)}
<div className="mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs">
{battingFields.map(([field, label]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{playerStat[field] ?? "-"}</span>
</div>
))}
</div>
</div>
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Pitching Summary
</p>
{hasPitchingStats(playerStat) ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
{getPitchingSummary(playerStat).map(([label, value]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{value ?? "-"}</span>
</div>
))}
</div>
) : (
<p className="text-xs text-muted-foreground">No pitching stats recorded.</p>
)}
<div className="mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs">
{pitchingFields.map(([field, label]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{playerStat[field] ?? "-"}</span>
</div>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</details>
+72 -27
View File
@@ -12,8 +12,7 @@ import { Textarea } from "@/components/ui/textarea";
import { fetchJson } from "@/lib/client";
import type { GamePlayerStatsInput, GameRecord, LineupRecord } from "@/lib/types";
const statFields = [
["pitchCount", "Pitches"],
const battingFields = [
["atBats", "AB"],
["runs", "R"],
["hits", "H"],
@@ -26,6 +25,15 @@ const statFields = [
["stolenBases", "SB"]
] as const;
const pitchingFields = [
["pitchCount", "Pitches"],
["inningsPitchedOuts", "IP Outs"],
["earnedRunsAllowed", "ER"],
["hitsAllowed", "H Allowed"],
["walksAllowed", "BB Allowed"],
["strikeoutsPitched", "Pitching K"]
] as const;
function pickDefaultLineupId(gameDate: string, lineups: LineupRecord[]) {
const exactDate = lineups.find((lineup) => lineup.gameDate === gameDate);
return exactDate?._id ?? lineups[0]?._id ?? "";
@@ -196,16 +204,6 @@ export function GameScoringClient() {
<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)} />
@@ -230,9 +228,29 @@ export function GameScoringClient() {
{selectedLineup && isScoring ? (
<SectionCard
title="Player Results"
description="Stat entry is optional. Leave any field blank if you do not want to track it for that player."
description="Enter player results as the game unfolds. Final score lives here because you know it once the game is actually being scored."
>
<div className="grid gap-3">
<div className="grid grid-cols-2 gap-3 rounded-[1.5rem] border border-border bg-muted/30 p-4 md:max-w-sm">
<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>
{selectedLineup.battingOrder.map((slot, index) => {
const statLine = playerStats[slot.playerId] ?? {
playerId: slot.playerId,
@@ -251,20 +269,47 @@ export function GameScoringClient() {
<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 lg:grid-cols-6">
{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 className="grid gap-3">
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Batting Line
</p>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5 lg:grid-cols-6">
{battingFields.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>
</div>
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Pitching Line
</p>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6">
{pitchingFields.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>
</div>
</div>
</CardContent>
</Card>
+57 -20
View File
@@ -52,6 +52,14 @@ const statFields = [
["stolenBases", "SB"]
] as const;
const pitchingStatFields = [
["inningsPitchedOuts", "IP Outs"],
["earnedRunsAllowed", "ER"],
["hitsAllowed", "H Allowed"],
["walksAllowed", "BB Allowed"],
["strikeoutsPitched", "Pitching K"]
] as const;
export function PlayerForm({
value,
onChange,
@@ -135,33 +143,62 @@ 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 className="md:col-span-2 space-y-4 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)
<div className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Batting Totals</p>
<div className="mt-2 grid grid-cols-2 gap-3 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>
))}
/>
</label>
))}
</div>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Pitching Totals</p>
<div className="mt-2 grid grid-cols-2 gap-3 md:grid-cols-4">
{pitchingStatFields.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>
</div>
<div className="md:col-span-2">
+28 -16
View File
@@ -12,6 +12,7 @@ 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 { getBattingSummary, getPitchingSummary, hasBattingStats, hasPitchingStats } from "@/lib/baseball-stats";
import { fetchJson } from "@/lib/client";
import { formatPlayerName } from "@/lib/utils";
import type { PlayerInput, PlayerPitchingInput, PlayerRecord } from "@/lib/types";
@@ -33,15 +34,6 @@ export function RosterClient() {
});
const playerFormRef = useRef<HTMLDivElement | null>(null);
const statColumns = [
["G", "gamesPlayed"],
["AB", "atBats"],
["R", "runs"],
["H", "hits"],
["RBI", "runsBattedIn"],
["SB", "stolenBases"]
] as const;
const editingPlayer = useMemo(
() => players.find((player) => player._id === editingPlayerId) ?? null,
[editingPlayerId, players]
@@ -253,14 +245,34 @@ export function RosterClient() {
</td>
<td className="px-3 py-3 align-top">{player.batsThrows}</td>
<td className="px-3 py-3 align-top">
{player.stats && Object.values(player.stats).some((value) => value !== null && value !== undefined) ? (
<div className="grid grid-cols-3 gap-x-3 gap-y-1 text-xs">
{statColumns.map(([label, key]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{player.stats?.[key] ?? 0}</span>
{hasBattingStats(player.stats) || hasPitchingStats(player.stats) ? (
<div className="space-y-2 text-xs">
{hasBattingStats(player.stats) ? (
<div>
<div className="mb-1 font-semibold text-muted-foreground">Hitting</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1">
{getBattingSummary(player.stats).map(([label, value]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{value ?? "-"}</span>
</div>
))}
</div>
</div>
))}
) : null}
{hasPitchingStats(player.stats) ? (
<div>
<div className="mb-1 font-semibold text-muted-foreground">Pitching</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1">
{getPitchingSummary(player.stats).map(([label, value]) => (
<div key={label}>
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
<span>{value ?? "-"}</span>
</div>
))}
</div>
</div>
) : null}
</div>
) : (
<span className="text-xs text-muted-foreground">No stats</span>