Implement Dashboard page with real data integration (P1.7)

Wire up the Dashboard page with /api/today data:
- Fetch today's decision, biometrics, and nutrition on mount
- Display DecisionCard with status, icon, and reason
- Show DataPanel with HRV, Body Battery, intensity minutes
- Show NutritionPanel with seed cycling and carb guidance
- Integrate OverrideToggles with POST/DELETE /api/overrides
- Handle loading states, error states, and setup prompts
- Display cycle day and phase information

Add 23 unit tests for the Dashboard component covering:
- Data fetching from /api/today and /api/user
- Component rendering (DecisionCard, DataPanel, NutritionPanel)
- Override toggle functionality (POST/DELETE API calls)
- Error handling and loading states
- Cycle information display

Also fixed TypeScript error in login page tests (resolveAuth
initialization).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 19:25:50 +00:00
parent 933e39aed4
commit 9f3c2ecac9
4 changed files with 771 additions and 16 deletions

View File

@@ -1,6 +1,129 @@
// ABOUTME: Main dashboard page for PhaseFlow.
// ABOUTME: Displays today's training decision, biometrics, and quick actions.
"use client";
import { useCallback, useEffect, useState } from "react";
import { DataPanel } from "@/components/dashboard/data-panel";
import { DecisionCard } from "@/components/dashboard/decision-card";
import { NutritionPanel } from "@/components/dashboard/nutrition-panel";
import { OverrideToggles } from "@/components/dashboard/override-toggles";
import type {
CyclePhase,
Decision,
NutritionGuidance,
OverrideType,
PhaseConfig,
} from "@/types";
interface TodayData {
decision: Decision;
cycleDay: number;
phase: CyclePhase;
phaseConfig: PhaseConfig;
daysUntilNextPhase: number;
cycleLength: number;
biometrics: {
hrvStatus: string;
bodyBatteryCurrent: number;
bodyBatteryYesterdayLow: number;
weekIntensityMinutes: number;
phaseLimit: number;
};
nutrition: NutritionGuidance;
}
interface UserData {
id: string;
email: string;
activeOverrides: OverrideType[];
}
export default function Dashboard() {
const [todayData, setTodayData] = useState<TodayData | null>(null);
const [userData, setUserData] = useState<UserData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchTodayData = useCallback(async () => {
const response = await fetch("/api/today");
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to fetch today data");
}
return data as TodayData;
}, []);
const fetchUserData = useCallback(async () => {
const response = await fetch("/api/user");
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to fetch user data");
}
return data as UserData;
}, []);
useEffect(() => {
async function loadData() {
try {
setLoading(true);
setError(null);
const [today, user] = await Promise.all([
fetchTodayData(),
fetchUserData(),
]);
setTodayData(today);
setUserData(user);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setLoading(false);
}
}
loadData();
}, [fetchTodayData, fetchUserData]);
const handleOverrideToggle = async (override: OverrideType) => {
if (!userData) return;
const isActive = userData.activeOverrides.includes(override);
const method = isActive ? "DELETE" : "POST";
try {
const response = await fetch("/api/overrides", {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ override }),
});
if (!response.ok) {
throw new Error("Failed to update override");
}
const result = await response.json();
// Update local state with new overrides
setUserData((prev) =>
prev ? { ...prev, activeOverrides: result.activeOverrides } : null,
);
// Refetch today data to get updated decision
const newTodayData = await fetchTodayData();
setTodayData(newTodayData);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to toggle override",
);
}
};
return (
<div className="min-h-screen bg-zinc-50 dark:bg-black">
<header className="border-b bg-white dark:bg-zinc-900 px-6 py-4">
@@ -16,12 +139,63 @@ export default function Dashboard() {
</header>
<main className="container mx-auto p-6">
<div className="text-center py-12">
<p className="text-zinc-500">Dashboard placeholder</p>
<p className="text-sm text-zinc-400 mt-2">
Connect your Garmin and set your period date to get started.
</p>
</div>
{loading && (
<div className="text-center py-12">
<p className="text-zinc-500">Loading...</p>
</div>
)}
{error && (
<div role="alert" className="text-center py-12">
<p className="text-red-500">Error: {error}</p>
{error.includes("lastPeriodDate") && (
<p className="text-sm text-zinc-400 mt-2">
Please log your period start date to get started.
</p>
)}
</div>
)}
{!loading && !error && todayData && userData && (
<div className="space-y-6">
{/* Cycle Info */}
<div className="text-center">
<p className="text-lg font-medium">
Day {todayData.cycleDay} · {todayData.phase}
</p>
<p className="text-sm text-zinc-500">
{todayData.daysUntilNextPhase} days until next phase
</p>
</div>
{/* Decision Card */}
<DecisionCard decision={todayData.decision} />
{/* Data and Nutrition Grid */}
<div className="grid gap-4 md:grid-cols-2">
<DataPanel
bodyBatteryCurrent={todayData.biometrics.bodyBatteryCurrent}
bodyBatteryYesterdayLow={
todayData.biometrics.bodyBatteryYesterdayLow
}
hrvStatus={todayData.biometrics.hrvStatus}
weekIntensity={todayData.biometrics.weekIntensityMinutes}
phaseLimit={todayData.biometrics.phaseLimit}
remainingMinutes={
todayData.biometrics.phaseLimit -
todayData.biometrics.weekIntensityMinutes
}
/>
<NutritionPanel nutrition={todayData.nutrition} />
</div>
{/* Override Toggles */}
<OverrideToggles
activeOverrides={userData.activeOverrides}
onToggle={handleOverrideToggle}
/>
</div>
)}
</main>
</div>
);