This commit is contained in:
Zeeshaun
2026-05-06 15:08:32 +00:00
parent b05a1a954b
commit 0de593c7d3
54 changed files with 4522 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell";
import { EmptyState } from "@/components/EmptyState";
import { SectionCard } from "@/components/SectionCard";
import { StatusBadge } from "@/components/StatusBadge";
import { FIELD_POSITIONS } from "@/lib/constants";
import { fetchJson } from "@/lib/client";
import { formatPlayerName } from "@/lib/utils";
import type { GeneratedLineup, LineupRecord, PlayerRecord } from "@/lib/types";
export function LineupClient() {
const [gameDate, setGameDate] = useState(new Date().toISOString().slice(0, 10));
const [players, setPlayers] = useState<PlayerRecord[]>([]);
const [selectedPlayerIds, setSelectedPlayerIds] = useState<string[]>([]);
const [startingPitcherId, setStartingPitcherId] = useState<string>("");
const [showAllPitchers, setShowAllPitchers] = useState(false);
const [generated, setGenerated] = useState<GeneratedLineup | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function loadPlayers(targetDate: string) {
try {
const response = await fetchJson<PlayerRecord[]>(`/api/players?gameDate=${targetDate}`);
setPlayers(response);
const activeIds = response.filter((player) => player.isActive).map((player) => player._id);
setSelectedPlayerIds(activeIds);
const firstEligible = response.find((player) => player.pitcherAvailability.status === "available");
setStartingPitcherId(firstEligible?._id ?? response[0]?._id ?? "");
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load players.");
}
}
useEffect(() => {
void loadPlayers(gameDate);
}, [gameDate]);
const selectablePitchers = useMemo(
() =>
players.filter((player) =>
selectedPlayerIds.includes(player._id) &&
(showAllPitchers || player.pitcherAvailability.status !== "needs-rest")
),
[players, selectedPlayerIds, showAllPitchers]
);
const selectedPlayers = useMemo(
() => players.filter((player) => selectedPlayerIds.includes(player._id)),
[players, selectedPlayerIds]
);
function updateGenerated(next: GeneratedLineup | null) {
setGenerated(next);
}
async function generate() {
try {
const response = await fetchJson<GeneratedLineup>("/api/lineups/generate", {
method: "POST",
body: JSON.stringify({
gameDate,
playerIds: selectedPlayerIds,
startingPitcherId: startingPitcherId || null
})
});
setGenerated(response);
setMessage("Suggested lineup generated.");
} catch (generateError) {
setError(generateError instanceof Error ? generateError.message : "Unable to generate lineup.");
}
}
async function save() {
if (!generated) return;
try {
await fetchJson<LineupRecord>("/api/lineups", {
method: "POST",
body: JSON.stringify({
...generated,
playerIds: selectedPlayerIds
})
});
setMessage("Lineup saved.");
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "Unable to save lineup.");
}
}
return (
<AppShell
title="Generate Lineup"
subtitle="Choose the active group, pick an eligible starter, then adjust the suggested lineup as needed."
>
{message ? <div className="banner banner-success">{message}</div> : null}
{error ? <div className="banner banner-error">{error}</div> : null}
<SectionCard title="Game Setup" description="Start with the players who are available today.">
<div className="form-grid">
<label>
<span>Game Date</span>
<input type="date" value={gameDate} onChange={(event) => setGameDate(event.target.value)} />
</label>
<label>
<span>Starting Pitcher</span>
<select value={startingPitcherId} onChange={(event) => setStartingPitcherId(event.target.value)}>
{selectablePitchers.map((player) => (
<option key={player._id} value={player._id}>
{formatPlayerName(player.firstName, player.lastName)}
</option>
))}
</select>
</label>
<label className="checkbox-row">
<input type="checkbox" checked={showAllPitchers} onChange={(event) => setShowAllPitchers(event.target.checked)} />
<span>Show pitchers who need rest</span>
</label>
<div className="full-width action-row">
<button className="button" disabled={selectedPlayerIds.length === 0} onClick={() => void generate()}>
Generate Suggested Lineup
</button>
<button className="button button-secondary" disabled={!generated} onClick={() => void save()}>
Save Lineup
</button>
</div>
</div>
<div className="picker-grid">
{players.map((player) => (
<label key={player._id} className="picker-card">
<input
type="checkbox"
checked={selectedPlayerIds.includes(player._id)}
onChange={(event) =>
setSelectedPlayerIds((current) =>
event.target.checked ? [...current, player._id] : current.filter((id) => id !== player._id)
)
}
/>
<div>
<strong>{formatPlayerName(player.firstName, player.lastName)}</strong>
<p>
#{player.jerseyNumber} {player.primaryPosition}
</p>
</div>
<StatusBadge status={player.pitcherAvailability.status} />
</label>
))}
</div>
</SectionCard>
{generated ? (
<>
<SectionCard title="Batting Order" description="Adjust any slot manually before saving.">
<div className="list-stack">
{generated.battingOrder.map((slot, index) => (
<div key={`${slot.playerId}-${index}`} className="list-row">
<div>
<h3>Spot {index + 1}</h3>
</div>
<select
value={slot.playerId}
onChange={(event) => {
const nextPlayer = selectedPlayers.find((player) => player._id === event.target.value);
if (!nextPlayer) return;
updateGenerated({
...generated,
battingOrder: generated.battingOrder.map((entry, entryIndex) =>
entryIndex === index
? {
...entry,
playerId: nextPlayer._id,
playerName: formatPlayerName(nextPlayer.firstName, nextPlayer.lastName)
}
: entry
)
});
}}
>
{selectedPlayers.map((player) => (
<option key={player._id} value={player._id}>
{formatPlayerName(player.firstName, player.lastName)}
</option>
))}
</select>
</div>
))}
</div>
</SectionCard>
<SectionCard title="Defensive Alignment" description="Assignments follow position preferences, but every field is editable.">
<div className="list-stack">
{FIELD_POSITIONS.map((position) => {
const assignment = generated.defensiveAssignments.find((entry) => entry.position === position);
return (
<div key={position} className="list-row">
<div>
<h3>{position}</h3>
</div>
<select
value={assignment?.playerId ?? ""}
onChange={(event) => {
const nextPlayer = selectedPlayers.find((player) => player._id === event.target.value);
if (!nextPlayer) return;
const nextAssignments = generated.defensiveAssignments.filter((entry) => entry.position !== position);
nextAssignments.push({
playerId: nextPlayer._id,
playerName: formatPlayerName(nextPlayer.firstName, nextPlayer.lastName),
position
});
updateGenerated({
...generated,
defensiveAssignments: nextAssignments
});
}}
>
<option value="">Unassigned</option>
{selectedPlayers.map((player) => (
<option key={player._id} value={player._id}>
{formatPlayerName(player.firstName, player.lastName)}
</option>
))}
</select>
</div>
);
})}
</div>
{generated.notes.length > 0 ? (
<div className="note-block">
{generated.notes.map((note) => (
<p key={note}>{note}</p>
))}
</div>
) : null}
</SectionCard>
</>
) : (
<SectionCard title="No Lineup Yet" description="Generate a lineup once your game-day roster is selected.">
<EmptyState
title="Waiting on lineup"
description="Select your active players, choose a starter, and generate a suggested batting order and field setup."
/>
</SectionCard>
)}
</AppShell>
);
}