Updating
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user