125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { AppShell } from "@/components/AppShell";
|
|
import { EmptyState } from "@/components/EmptyState";
|
|
import { SectionCard } from "@/components/SectionCard";
|
|
import { StatusBadge } from "@/components/StatusBadge";
|
|
import { fetchJson } from "@/lib/client";
|
|
import type { DashboardResponse } from "@/lib/types";
|
|
|
|
export function DashboardClient() {
|
|
const [data, setData] = useState<DashboardResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
|
|
async function loadDashboard() {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetchJson<DashboardResponse>("/api/dashboard");
|
|
setData(response);
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load dashboard.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function seedSampleData() {
|
|
try {
|
|
const response = await fetchJson<{ seeded: boolean; reason?: string }>("/api/seed", { method: "POST" });
|
|
setMessage(response.seeded ? "Sample team loaded." : response.reason ?? "Seed skipped.");
|
|
await loadDashboard();
|
|
} catch (seedError) {
|
|
setError(seedError instanceof Error ? seedError.message : "Failed to seed data.");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void loadDashboard();
|
|
}, []);
|
|
|
|
return (
|
|
<AppShell
|
|
title="Game Day Snapshot"
|
|
subtitle="Check roster health, pitcher rest, and lineup readiness before first pitch."
|
|
actions={
|
|
<button className="button button-secondary" onClick={() => void seedSampleData()}>
|
|
Load Sample Team
|
|
</button>
|
|
}
|
|
>
|
|
{message ? <div className="banner banner-success">{message}</div> : null}
|
|
{error ? <div className="banner banner-error">{error}</div> : null}
|
|
|
|
{loading ? (
|
|
<SectionCard title="Loading" description="Pulling current roster and pitching data." />
|
|
) : data && data.rosterCount > 0 ? (
|
|
<>
|
|
<section className="stats-grid">
|
|
<article className="stat-card">
|
|
<span>Rostered Players</span>
|
|
<strong>{data.rosterCount}</strong>
|
|
</article>
|
|
<article className="stat-card">
|
|
<span>Active Today</span>
|
|
<strong>{data.activePlayers}</strong>
|
|
</article>
|
|
<article className="stat-card stat-card-success">
|
|
<span>Pitchers Available</span>
|
|
<strong>{data.availablePitchers}</strong>
|
|
</article>
|
|
<article className="stat-card stat-card-warning">
|
|
<span>Need Rest</span>
|
|
<strong>{data.needsRest}</strong>
|
|
</article>
|
|
</section>
|
|
|
|
<SectionCard
|
|
title="Pitcher Availability"
|
|
description="Availability is based on the latest appearance and your configured rest rules."
|
|
>
|
|
<div className="list-stack">
|
|
{data.recentPitching.map((entry) => (
|
|
<article key={entry.playerId} className="list-row">
|
|
<div>
|
|
<h3>{entry.playerName}</h3>
|
|
<p>
|
|
{entry.lastPitchedDate
|
|
? `${entry.pitchCount} pitches on ${entry.lastPitchedDate}`
|
|
: "No pitching history recorded yet."}
|
|
</p>
|
|
</div>
|
|
<div className="list-meta">
|
|
<StatusBadge status={entry.status} />
|
|
<small>
|
|
{entry.daysSinceLastAppearance === null
|
|
? "No history"
|
|
: `${entry.daysSinceLastAppearance} day(s) since last outing`}
|
|
</small>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
</>
|
|
) : (
|
|
<SectionCard title="No Team Yet" description="Start with sample data or add players on the roster screen.">
|
|
<EmptyState
|
|
title="Roster is empty"
|
|
description="Load the sample team to explore the app, then adjust players and pitching records as needed."
|
|
action={
|
|
<button className="button" onClick={() => void seedSampleData()}>
|
|
Load Sample Team
|
|
</button>
|
|
}
|
|
/>
|
|
</SectionCard>
|
|
)}
|
|
</AppShell>
|
|
);
|
|
}
|