Initial commit: smartrun MVP
Garmin run classification and progression tracker. Go backend (MCP client to mcp-garmin, SQLite store, deterministic rule engine, REST API) and React/TS frontend (Dashboard, Review Queue, Workout Kinds). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
86
frontend/src/pages/Dashboard.tsx
Normal file
86
frontend/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api/client";
|
||||
import { ProgressionChart } from "../components/charts/ProgressionChart";
|
||||
import type { ProgressionMetric, ProgressionPoint, WorkoutKind } from "../types/api";
|
||||
|
||||
const METRICS: { key: ProgressionMetric; label: string }[] = [
|
||||
{ key: "pace", label: "Pace" },
|
||||
{ key: "hr", label: "Heart Rate" },
|
||||
{ key: "vo2max", label: "VO2max" },
|
||||
{ key: "aerobic_te", label: "Aerobic Training Effect" },
|
||||
{ key: "anaerobic_te", label: "Anaerobic Training Effect" },
|
||||
];
|
||||
|
||||
export function Dashboard() {
|
||||
const [kinds, setKinds] = useState<WorkoutKind[]>([]);
|
||||
const [selectedKindId, setSelectedKindId] = useState<number | null>(null);
|
||||
const [metric, setMetric] = useState<ProgressionMetric>("pace");
|
||||
const [points, setPoints] = useState<ProgressionPoint[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.listWorkoutKinds()
|
||||
.then((k) => {
|
||||
setKinds(k);
|
||||
if (k.length > 0) setSelectedKindId(k[0].ID);
|
||||
})
|
||||
.catch((e) => setError(String(e)));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedKindId == null) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
api
|
||||
.progression(selectedKindId, metric)
|
||||
.then(setPoints)
|
||||
.catch((e) => setError(String(e)))
|
||||
.finally(() => setLoading(false));
|
||||
}, [selectedKindId, metric]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>Progression</h2>
|
||||
|
||||
{kinds.length === 0 ? (
|
||||
<p className="empty-state">
|
||||
No workout kinds defined yet. Create one on the Workout Kinds tab to start seeing progression here.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="controls">
|
||||
<label>
|
||||
Workout kind
|
||||
<select
|
||||
value={selectedKindId ?? ""}
|
||||
onChange={(e) => setSelectedKindId(Number(e.target.value))}
|
||||
>
|
||||
{kinds.map((k) => (
|
||||
<option key={k.ID} value={k.ID}>
|
||||
{k.Name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Metric
|
||||
<select value={metric} onChange={(e) => setMetric(e.target.value as ProgressionMetric)}>
|
||||
{METRICS.map((m) => (
|
||||
<option key={m.key} value={m.key}>
|
||||
{m.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
{loading ? <p>Loading...</p> : <ProgressionChart points={points} metric={metric} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user