Updating
This commit is contained in:
@@ -18,7 +18,12 @@ const statFields = [
|
|||||||
"runsBattedIn",
|
"runsBattedIn",
|
||||||
"walks",
|
"walks",
|
||||||
"strikeouts",
|
"strikeouts",
|
||||||
"stolenBases"
|
"stolenBases",
|
||||||
|
"inningsPitchedOuts",
|
||||||
|
"earnedRunsAllowed",
|
||||||
|
"hitsAllowed",
|
||||||
|
"walksAllowed",
|
||||||
|
"strikeoutsPitched"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ const statFields = [
|
|||||||
"runsBattedIn",
|
"runsBattedIn",
|
||||||
"walks",
|
"walks",
|
||||||
"strikeouts",
|
"strikeouts",
|
||||||
"stolenBases"
|
"stolenBases",
|
||||||
|
"inningsPitchedOuts",
|
||||||
|
"earnedRunsAllowed",
|
||||||
|
"hitsAllowed",
|
||||||
|
"walksAllowed",
|
||||||
|
"strikeoutsPitched"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { Alert } from "@/components/ui/alert";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { getBattingSummary, getPitchingSummary, hasBattingStats, hasPitchingStats } from "@/lib/baseball-stats";
|
||||||
import { fetchJson } from "@/lib/client";
|
import { fetchJson } from "@/lib/client";
|
||||||
import type { GameRecord } from "@/lib/types";
|
import type { GameRecord } from "@/lib/types";
|
||||||
|
|
||||||
const statFields = [
|
const battingFields = [
|
||||||
["pitchCount", "Pitches"],
|
|
||||||
["atBats", "AB"],
|
["atBats", "AB"],
|
||||||
["runs", "R"],
|
["runs", "R"],
|
||||||
["hits", "H"],
|
["hits", "H"],
|
||||||
@@ -27,6 +27,15 @@ const statFields = [
|
|||||||
["stolenBases", "SB"]
|
["stolenBases", "SB"]
|
||||||
] as const;
|
] 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() {
|
export function GameHistoryClient() {
|
||||||
const [gameDate, setGameDate] = useState("");
|
const [gameDate, setGameDate] = useState("");
|
||||||
const [games, setGames] = useState<GameRecord[]>([]);
|
const [games, setGames] = useState<GameRecord[]>([]);
|
||||||
@@ -165,31 +174,67 @@ export function GameHistoryClient() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-[1rem] border border-border bg-white/70">
|
<div className="grid gap-3">
|
||||||
<table className="min-w-full text-left text-xs">
|
{game.playerStats.map((playerStat) => (
|
||||||
<thead className="border-b border-border bg-muted/60 uppercase tracking-[0.18em] text-muted-foreground">
|
<div key={playerStat.playerId} className="rounded-[1rem] border border-border bg-white/70 p-3">
|
||||||
<tr>
|
<div className="flex items-center justify-between gap-3">
|
||||||
<th className="px-3 py-2 font-semibold">Player</th>
|
<p className="text-sm font-semibold">{playerStat.playerName}</p>
|
||||||
{statFields.map(([, label]) => (
|
<p className="text-xs text-muted-foreground">Saved stat line</p>
|
||||||
<th key={label} className="px-3 py-2 font-semibold">
|
</div>
|
||||||
{label}
|
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
</th>
|
<div>
|
||||||
))}
|
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||||
</tr>
|
Hitting Summary
|
||||||
</thead>
|
</p>
|
||||||
<tbody className="divide-y divide-border">
|
{hasBattingStats(playerStat) ? (
|
||||||
{game.playerStats.map((playerStat) => (
|
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
|
||||||
<tr key={playerStat.playerId}>
|
{getBattingSummary(playerStat).map(([label, value]) => (
|
||||||
<td className="px-3 py-2 font-medium">{playerStat.playerName}</td>
|
<div key={label}>
|
||||||
{statFields.map(([field, label]) => (
|
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
|
||||||
<td key={label} className="px-3 py-2">
|
<span>{value ?? "-"}</span>
|
||||||
{playerStat[field] ?? "-"}
|
</div>
|
||||||
</td>
|
))}
|
||||||
))}
|
</div>
|
||||||
</tr>
|
) : (
|
||||||
))}
|
<p className="text-xs text-muted-foreground">No batting stats recorded.</p>
|
||||||
</tbody>
|
)}
|
||||||
</table>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { fetchJson } from "@/lib/client";
|
import { fetchJson } from "@/lib/client";
|
||||||
import type { GamePlayerStatsInput, GameRecord, LineupRecord } from "@/lib/types";
|
import type { GamePlayerStatsInput, GameRecord, LineupRecord } from "@/lib/types";
|
||||||
|
|
||||||
const statFields = [
|
const battingFields = [
|
||||||
["pitchCount", "Pitches"],
|
|
||||||
["atBats", "AB"],
|
["atBats", "AB"],
|
||||||
["runs", "R"],
|
["runs", "R"],
|
||||||
["hits", "H"],
|
["hits", "H"],
|
||||||
@@ -26,6 +25,15 @@ const statFields = [
|
|||||||
["stolenBases", "SB"]
|
["stolenBases", "SB"]
|
||||||
] as const;
|
] 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[]) {
|
function pickDefaultLineupId(gameDate: string, lineups: LineupRecord[]) {
|
||||||
const exactDate = lineups.find((lineup) => lineup.gameDate === gameDate);
|
const exactDate = lineups.find((lineup) => lineup.gameDate === gameDate);
|
||||||
return exactDate?._id ?? lineups[0]?._id ?? "";
|
return exactDate?._id ?? lineups[0]?._id ?? "";
|
||||||
@@ -196,16 +204,6 @@ export function GameScoringClient() {
|
|||||||
<span className="field-label">Opponent</span>
|
<span className="field-label">Opponent</span>
|
||||||
<Input value={opponent} onChange={(event) => setOpponent(event.target.value)} placeholder="Optional" />
|
<Input value={opponent} onChange={(event) => setOpponent(event.target.value)} placeholder="Optional" />
|
||||||
</label>
|
</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">
|
<label className="md:col-span-2">
|
||||||
<span className="field-label">Game Notes</span>
|
<span className="field-label">Game Notes</span>
|
||||||
<Textarea rows={3} value={notes} onChange={(event) => setNotes(event.target.value)} />
|
<Textarea rows={3} value={notes} onChange={(event) => setNotes(event.target.value)} />
|
||||||
@@ -230,9 +228,29 @@ export function GameScoringClient() {
|
|||||||
{selectedLineup && isScoring ? (
|
{selectedLineup && isScoring ? (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="Player Results"
|
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 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) => {
|
{selectedLineup.battingOrder.map((slot, index) => {
|
||||||
const statLine = playerStats[slot.playerId] ?? {
|
const statLine = playerStats[slot.playerId] ?? {
|
||||||
playerId: 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>
|
<p className="text-xs text-muted-foreground">Batting order slot {index + 1}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5 lg:grid-cols-6">
|
<div className="grid gap-3">
|
||||||
{statFields.map(([field, label]) => (
|
<div>
|
||||||
<label key={field}>
|
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||||
<span className="field-label">{label}</span>
|
Batting Line
|
||||||
<Input
|
</p>
|
||||||
type="number"
|
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5 lg:grid-cols-6">
|
||||||
min={0}
|
{battingFields.map(([field, label]) => (
|
||||||
value={statLine[field] ?? ""}
|
<label key={field}>
|
||||||
onChange={(event) =>
|
<span className="field-label">{label}</span>
|
||||||
updatePlayerStat(slot.playerId, slot.playerName, field, event.target.value)
|
<Input
|
||||||
}
|
type="number"
|
||||||
/>
|
min={0}
|
||||||
</label>
|
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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ const statFields = [
|
|||||||
["stolenBases", "SB"]
|
["stolenBases", "SB"]
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const pitchingStatFields = [
|
||||||
|
["inningsPitchedOuts", "IP Outs"],
|
||||||
|
["earnedRunsAllowed", "ER"],
|
||||||
|
["hitsAllowed", "H Allowed"],
|
||||||
|
["walksAllowed", "BB Allowed"],
|
||||||
|
["strikeoutsPitched", "Pitching K"]
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function PlayerForm({
|
export function PlayerForm({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -135,33 +143,62 @@ export function PlayerForm({
|
|||||||
<span className="field-label">Notes</span>
|
<span className="field-label">Notes</span>
|
||||||
<Textarea rows={4} value={value.notes ?? ""} onChange={(event) => onChange({ ...value, notes: event.target.value })} />
|
<Textarea rows={4} value={value.notes ?? ""} onChange={(event) => onChange({ ...value, notes: event.target.value })} />
|
||||||
</label>
|
</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>
|
<div>
|
||||||
<p className="text-sm font-semibold">Optional Player Stats</p>
|
<p className="text-sm font-semibold">Optional Player Stats</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Leave any field blank if you do not want to track it.
|
Leave any field blank if you do not want to track it.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 grid-cols-2 md:grid-cols-4">
|
<div className="space-y-3">
|
||||||
{statFields.map(([key, label]) => (
|
<div>
|
||||||
<label key={key}>
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Batting Totals</p>
|
||||||
<span className="field-label">{label}</span>
|
<div className="mt-2 grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
<Input
|
{statFields.map(([key, label]) => (
|
||||||
type="number"
|
<label key={key}>
|
||||||
min={0}
|
<span className="field-label">{label}</span>
|
||||||
value={value.stats?.[key] ?? ""}
|
<Input
|
||||||
onChange={(event) =>
|
type="number"
|
||||||
onChange({
|
min={0}
|
||||||
...value,
|
value={value.stats?.[key] ?? ""}
|
||||||
stats: {
|
onChange={(event) =>
|
||||||
...value.stats,
|
onChange({
|
||||||
[key]: event.target.value === "" ? null : Number(event.target.value)
|
...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>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { getBattingSummary, getPitchingSummary, hasBattingStats, hasPitchingStats } from "@/lib/baseball-stats";
|
||||||
import { fetchJson } from "@/lib/client";
|
import { fetchJson } from "@/lib/client";
|
||||||
import { formatPlayerName } from "@/lib/utils";
|
import { formatPlayerName } from "@/lib/utils";
|
||||||
import type { PlayerInput, PlayerPitchingInput, PlayerRecord } from "@/lib/types";
|
import type { PlayerInput, PlayerPitchingInput, PlayerRecord } from "@/lib/types";
|
||||||
@@ -33,15 +34,6 @@ export function RosterClient() {
|
|||||||
});
|
});
|
||||||
const playerFormRef = useRef<HTMLDivElement | null>(null);
|
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(
|
const editingPlayer = useMemo(
|
||||||
() => players.find((player) => player._id === editingPlayerId) ?? null,
|
() => players.find((player) => player._id === editingPlayerId) ?? null,
|
||||||
[editingPlayerId, players]
|
[editingPlayerId, players]
|
||||||
@@ -253,14 +245,34 @@ export function RosterClient() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-3 align-top">{player.batsThrows}</td>
|
<td className="px-3 py-3 align-top">{player.batsThrows}</td>
|
||||||
<td className="px-3 py-3 align-top">
|
<td className="px-3 py-3 align-top">
|
||||||
{player.stats && Object.values(player.stats).some((value) => value !== null && value !== undefined) ? (
|
{hasBattingStats(player.stats) || hasPitchingStats(player.stats) ? (
|
||||||
<div className="grid grid-cols-3 gap-x-3 gap-y-1 text-xs">
|
<div className="space-y-2 text-xs">
|
||||||
{statColumns.map(([label, key]) => (
|
{hasBattingStats(player.stats) ? (
|
||||||
<div key={label}>
|
<div>
|
||||||
<span className="font-semibold text-muted-foreground">{label}</span>{" "}
|
<div className="mb-1 font-semibold text-muted-foreground">Hitting</div>
|
||||||
<span>{player.stats?.[key] ?? 0}</span>
|
<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>
|
</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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-muted-foreground">No stats</span>
|
<span className="text-xs text-muted-foreground">No stats</span>
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import type { PlayerStatsInput } from "@/lib/types";
|
||||||
|
|
||||||
|
type StatValue = number | null | undefined;
|
||||||
|
|
||||||
|
function stat(value: StatValue) {
|
||||||
|
return value ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasValue(value: StatValue) {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRate(value: number | null, digits = 3) {
|
||||||
|
if (value === null || Number.isNaN(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toFixed(digits).replace(/^0(?=\.)/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDecimal(value: number | null, digits = 2) {
|
||||||
|
if (value === null || Number.isNaN(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toFixed(digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatInningsPitched(outs: StatValue) {
|
||||||
|
const totalOuts = stat(outs);
|
||||||
|
if (totalOuts <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wholeInnings = Math.floor(totalOuts / 3);
|
||||||
|
const remainder = totalOuts % 3;
|
||||||
|
return `${wholeInnings}.${remainder}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasBattingStats(stats?: PlayerStatsInput | null) {
|
||||||
|
if (!stats) return false;
|
||||||
|
|
||||||
|
return [
|
||||||
|
stats.atBats,
|
||||||
|
stats.runs,
|
||||||
|
stats.hits,
|
||||||
|
stats.doubles,
|
||||||
|
stats.triples,
|
||||||
|
stats.homeRuns,
|
||||||
|
stats.runsBattedIn,
|
||||||
|
stats.walks,
|
||||||
|
stats.strikeouts,
|
||||||
|
stats.stolenBases
|
||||||
|
].some(hasValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPitchingStats(stats?: PlayerStatsInput | null) {
|
||||||
|
if (!stats) return false;
|
||||||
|
|
||||||
|
return [
|
||||||
|
stats.inningsPitchedOuts,
|
||||||
|
stats.earnedRunsAllowed,
|
||||||
|
stats.hitsAllowed,
|
||||||
|
stats.walksAllowed,
|
||||||
|
stats.strikeoutsPitched
|
||||||
|
].some(hasValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBattingSummary(stats?: PlayerStatsInput | null) {
|
||||||
|
if (!hasBattingStats(stats)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const atBats = stat(stats?.atBats);
|
||||||
|
const hits = stat(stats?.hits);
|
||||||
|
const walks = stat(stats?.walks);
|
||||||
|
const doubles = stat(stats?.doubles);
|
||||||
|
const triples = stat(stats?.triples);
|
||||||
|
const homeRuns = stat(stats?.homeRuns);
|
||||||
|
const singles = Math.max(hits - doubles - triples - homeRuns, 0);
|
||||||
|
const totalBases = singles + doubles * 2 + triples * 3 + homeRuns * 4;
|
||||||
|
|
||||||
|
const average = atBats > 0 ? hits / atBats : null;
|
||||||
|
const onBase = atBats + walks > 0 ? (hits + walks) / (atBats + walks) : null;
|
||||||
|
const slugging = atBats > 0 ? totalBases / atBats : null;
|
||||||
|
const ops = onBase !== null || slugging !== null ? (onBase ?? 0) + (slugging ?? 0) : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
["AVG", formatRate(average)],
|
||||||
|
["OBP", formatRate(onBase)],
|
||||||
|
["SLG", formatRate(slugging)],
|
||||||
|
["OPS", formatRate(ops)]
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPitchingSummary(stats?: PlayerStatsInput | null) {
|
||||||
|
if (!hasPitchingStats(stats)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const outs = stat(stats?.inningsPitchedOuts);
|
||||||
|
const innings = outs / 3;
|
||||||
|
const earnedRuns = stat(stats?.earnedRunsAllowed);
|
||||||
|
const hitsAllowed = stat(stats?.hitsAllowed);
|
||||||
|
const walksAllowed = stat(stats?.walksAllowed);
|
||||||
|
const strikeoutsPitched = stat(stats?.strikeoutsPitched);
|
||||||
|
|
||||||
|
const era = outs > 0 ? (earnedRuns * 27) / outs : null;
|
||||||
|
const whip = innings > 0 ? (hitsAllowed + walksAllowed) / innings : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
["IP", formatInningsPitched(outs)],
|
||||||
|
["ERA", formatDecimal(era)],
|
||||||
|
["WHIP", formatDecimal(whip)],
|
||||||
|
["K", strikeoutsPitched > 0 || hasValue(stats?.strikeoutsPitched) ? String(strikeoutsPitched) : null]
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
@@ -26,6 +26,11 @@ export interface PlayerStatsInput {
|
|||||||
walks?: number | null;
|
walks?: number | null;
|
||||||
strikeouts?: number | null;
|
strikeouts?: number | null;
|
||||||
stolenBases?: number | null;
|
stolenBases?: number | null;
|
||||||
|
inningsPitchedOuts?: number | null;
|
||||||
|
earnedRunsAllowed?: number | null;
|
||||||
|
hitsAllowed?: number | null;
|
||||||
|
walksAllowed?: number | null;
|
||||||
|
strikeoutsPitched?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerPitchingInput {
|
export interface PlayerPitchingInput {
|
||||||
@@ -38,6 +43,7 @@ export interface PlayerPitchingInput {
|
|||||||
export interface GamePlayerStatsInput {
|
export interface GamePlayerStatsInput {
|
||||||
playerId: string;
|
playerId: string;
|
||||||
playerName: string;
|
playerName: string;
|
||||||
|
pitchCount?: number | null;
|
||||||
atBats?: number | null;
|
atBats?: number | null;
|
||||||
runs?: number | null;
|
runs?: number | null;
|
||||||
hits?: number | null;
|
hits?: number | null;
|
||||||
@@ -48,7 +54,11 @@ export interface GamePlayerStatsInput {
|
|||||||
walks?: number | null;
|
walks?: number | null;
|
||||||
strikeouts?: number | null;
|
strikeouts?: number | null;
|
||||||
stolenBases?: number | null;
|
stolenBases?: number | null;
|
||||||
pitchCount?: number | null;
|
inningsPitchedOuts?: number | null;
|
||||||
|
earnedRunsAllowed?: number | null;
|
||||||
|
hitsAllowed?: number | null;
|
||||||
|
walksAllowed?: number | null;
|
||||||
|
strikeoutsPitched?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PitchingRuleInput {
|
export interface PitchingRuleInput {
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ export const playerSchema = z.object({
|
|||||||
runsBattedIn: optionalStat,
|
runsBattedIn: optionalStat,
|
||||||
walks: optionalStat,
|
walks: optionalStat,
|
||||||
strikeouts: optionalStat,
|
strikeouts: optionalStat,
|
||||||
stolenBases: optionalStat
|
stolenBases: optionalStat,
|
||||||
|
inningsPitchedOuts: optionalStat,
|
||||||
|
earnedRunsAllowed: optionalStat,
|
||||||
|
hitsAllowed: optionalStat,
|
||||||
|
walksAllowed: optionalStat,
|
||||||
|
strikeoutsPitched: optionalStat
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
@@ -91,7 +96,12 @@ export const saveGameSchema = z.object({
|
|||||||
walks: optionalStat,
|
walks: optionalStat,
|
||||||
strikeouts: optionalStat,
|
strikeouts: optionalStat,
|
||||||
stolenBases: optionalStat,
|
stolenBases: optionalStat,
|
||||||
pitchCount: optionalStat
|
pitchCount: optionalStat,
|
||||||
|
inningsPitchedOuts: optionalStat,
|
||||||
|
earnedRunsAllowed: optionalStat,
|
||||||
|
hitsAllowed: optionalStat,
|
||||||
|
walksAllowed: optionalStat,
|
||||||
|
strikeoutsPitched: optionalStat
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface GameDocument {
|
|||||||
playerStats: Array<{
|
playerStats: Array<{
|
||||||
playerId: Types.ObjectId;
|
playerId: Types.ObjectId;
|
||||||
playerName: string;
|
playerName: string;
|
||||||
|
pitchCount?: number | null;
|
||||||
atBats?: number | null;
|
atBats?: number | null;
|
||||||
runs?: number | null;
|
runs?: number | null;
|
||||||
hits?: number | null;
|
hits?: number | null;
|
||||||
@@ -24,6 +25,11 @@ export interface GameDocument {
|
|||||||
walks?: number | null;
|
walks?: number | null;
|
||||||
strikeouts?: number | null;
|
strikeouts?: number | null;
|
||||||
stolenBases?: number | null;
|
stolenBases?: number | null;
|
||||||
|
inningsPitchedOuts?: number | null;
|
||||||
|
earnedRunsAllowed?: number | null;
|
||||||
|
hitsAllowed?: number | null;
|
||||||
|
walksAllowed?: number | null;
|
||||||
|
strikeoutsPitched?: number | null;
|
||||||
}>;
|
}>;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
@@ -55,7 +61,12 @@ const gameSchema = new Schema<GameDocument>(
|
|||||||
walks: { type: Number, default: null },
|
walks: { type: Number, default: null },
|
||||||
strikeouts: { type: Number, default: null },
|
strikeouts: { type: Number, default: null },
|
||||||
stolenBases: { type: Number, default: null },
|
stolenBases: { type: Number, default: null },
|
||||||
pitchCount: { type: Number, default: null }
|
pitchCount: { type: Number, default: null },
|
||||||
|
inningsPitchedOuts: { type: Number, default: null },
|
||||||
|
earnedRunsAllowed: { type: Number, default: null },
|
||||||
|
hitsAllowed: { type: Number, default: null },
|
||||||
|
walksAllowed: { type: Number, default: null },
|
||||||
|
strikeoutsPitched: { type: Number, default: null }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export interface PlayerDocument {
|
|||||||
walks?: number | null;
|
walks?: number | null;
|
||||||
strikeouts?: number | null;
|
strikeouts?: number | null;
|
||||||
stolenBases?: number | null;
|
stolenBases?: number | null;
|
||||||
|
inningsPitchedOuts?: number | null;
|
||||||
|
earnedRunsAllowed?: number | null;
|
||||||
|
hitsAllowed?: number | null;
|
||||||
|
walksAllowed?: number | null;
|
||||||
|
strikeoutsPitched?: number | null;
|
||||||
};
|
};
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
@@ -49,7 +54,12 @@ const playerSchema = new Schema<PlayerDocument>(
|
|||||||
runsBattedIn: { type: Number, default: null },
|
runsBattedIn: { type: Number, default: null },
|
||||||
walks: { type: Number, default: null },
|
walks: { type: Number, default: null },
|
||||||
strikeouts: { type: Number, default: null },
|
strikeouts: { type: Number, default: null },
|
||||||
stolenBases: { type: Number, default: null }
|
stolenBases: { type: Number, default: null },
|
||||||
|
inningsPitchedOuts: { type: Number, default: null },
|
||||||
|
earnedRunsAllowed: { type: Number, default: null },
|
||||||
|
hitsAllowed: { type: Number, default: null },
|
||||||
|
walksAllowed: { type: Number, default: null },
|
||||||
|
strikeoutsPitched: { type: Number, default: null }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
|
|||||||
Reference in New Issue
Block a user