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:
186
src/app/page.tsx
186
src/app/page.tsx
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user