Add comprehensive training plan reference page that displays: - Current phase status (day, phase name, training type, weekly limit) - Phase overview cards for all 5 cycle phases with weekly intensity limits - Strength training exercises reference with sets and reps - Rebounding techniques organized by phase - Weekly training guidelines for each phase The page fetches cycle data from /api/cycle/current and highlights the current phase. Implements full TDD with 16 tests covering loading states, error handling, phase display, and exercise reference sections. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
// ABOUTME: Exercise plan reference page.
|
||
// ABOUTME: Displays the full monthly exercise plan by phase with current phase highlighted.
|
||
"use client";
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import type { CyclePhase, PhaseConfig } from "@/types";
|
||
|
||
interface CycleData {
|
||
cycleDay: number;
|
||
phase: CyclePhase;
|
||
phaseConfig: PhaseConfig;
|
||
daysUntilNextPhase: number;
|
||
cycleLength: number;
|
||
}
|
||
|
||
// Phase configurations for display
|
||
const PHASES: Array<{
|
||
name: CyclePhase;
|
||
displayName: string;
|
||
weeklyLimit: number;
|
||
trainingType: string;
|
||
description: string;
|
||
}> = [
|
||
{
|
||
name: "MENSTRUAL",
|
||
displayName: "Menstrual",
|
||
weeklyLimit: 30,
|
||
trainingType: "Gentle rebounding only",
|
||
description: "Focus on rest and gentle movement. Light lymphatic drainage.",
|
||
},
|
||
{
|
||
name: "FOLLICULAR",
|
||
displayName: "Follicular",
|
||
weeklyLimit: 120,
|
||
trainingType: "Strength + rebounding",
|
||
description:
|
||
"Building phase - increase intensity and add strength training.",
|
||
},
|
||
{
|
||
name: "OVULATION",
|
||
displayName: "Ovulation",
|
||
weeklyLimit: 80,
|
||
trainingType: "Peak performance",
|
||
description: "Peak energy - maximize intensity and plyometric movements.",
|
||
},
|
||
{
|
||
name: "EARLY_LUTEAL",
|
||
displayName: "Early Luteal",
|
||
weeklyLimit: 100,
|
||
trainingType: "Moderate training",
|
||
description:
|
||
"Maintain intensity but listen to your body for signs of fatigue.",
|
||
},
|
||
{
|
||
name: "LATE_LUTEAL",
|
||
displayName: "Late Luteal",
|
||
weeklyLimit: 50,
|
||
trainingType: "Gentle rebounding ONLY",
|
||
description:
|
||
"Wind down phase - focus on stress relief and gentle movement.",
|
||
},
|
||
];
|
||
|
||
const STRENGTH_EXERCISES = [
|
||
{ name: "Squats", sets: "3x8-12" },
|
||
{ name: "Push-ups", sets: "3x5-10" },
|
||
{ name: "Single-leg Deadlifts", sets: "3x6-8 each" },
|
||
{ name: "Plank", sets: "3x20-45s" },
|
||
{ name: "Kettlebell Swings", sets: "2x10-15" },
|
||
];
|
||
|
||
const REBOUNDING_TECHNIQUES = [
|
||
{
|
||
phase: "Menstrual",
|
||
techniques: "Health bounce, lymphatic drainage",
|
||
},
|
||
{
|
||
phase: "Follicular",
|
||
techniques: "Strength bounce, intervals",
|
||
},
|
||
{
|
||
phase: "Ovulation",
|
||
techniques: "Maximum intensity, plyometric",
|
||
},
|
||
{
|
||
phase: "Luteal",
|
||
techniques: "Therapeutic, stress relief",
|
||
},
|
||
];
|
||
|
||
export default function PlanPage() {
|
||
const [cycleData, setCycleData] = useState<CycleData | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const fetchCycleData = useCallback(async () => {
|
||
const response = await fetch("/api/cycle/current");
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.error || "Failed to fetch cycle data");
|
||
}
|
||
|
||
return data as CycleData;
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
async function loadData() {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const data = await fetchCycleData();
|
||
setCycleData(data);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "An error occurred");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
loadData();
|
||
}, [fetchCycleData]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="container mx-auto p-8">
|
||
<h1 className="text-2xl font-bold mb-8">Exercise Plan</h1>
|
||
<p className="text-zinc-500">Loading...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="container mx-auto p-8">
|
||
<h1 className="text-2xl font-bold mb-8">Exercise Plan</h1>
|
||
<div role="alert" className="text-red-500">
|
||
Error: {error}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="container mx-auto p-8">
|
||
<h1 className="text-2xl font-bold mb-8">Exercise Plan</h1>
|
||
|
||
{cycleData && (
|
||
<div className="space-y-8">
|
||
{/* Current Phase Status */}
|
||
<section className="bg-zinc-100 dark:bg-zinc-800 rounded-lg p-6">
|
||
<h2 className="text-lg font-semibold mb-2">Current Status</h2>
|
||
<p className="text-xl font-medium">
|
||
Day {cycleData.cycleDay} · {cycleData.phase.replace("_", " ")}
|
||
</p>
|
||
<p className="text-zinc-600 dark:text-zinc-400">
|
||
{cycleData.daysUntilNextPhase} days until next phase
|
||
</p>
|
||
<p className="mt-2">
|
||
<span className="font-medium">Training type:</span>{" "}
|
||
{cycleData.phaseConfig.trainingType}
|
||
</p>
|
||
<p>
|
||
<span className="font-medium">Weekly limit:</span>{" "}
|
||
{cycleData.phaseConfig.weeklyLimit} min/week
|
||
</p>
|
||
</section>
|
||
|
||
{/* Phase Overview */}
|
||
<section>
|
||
<h2 className="text-lg font-semibold mb-4">Phase Overview</h2>
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{PHASES.map((phase) => (
|
||
<div
|
||
key={phase.name}
|
||
data-testid={`phase-${phase.name}`}
|
||
className={`rounded-lg p-4 border ${
|
||
cycleData.phase === phase.name
|
||
? "ring-2 ring-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
||
: "bg-white dark:bg-zinc-900"
|
||
}`}
|
||
>
|
||
<h3 className="font-semibold text-base">
|
||
{phase.displayName}
|
||
</h3>
|
||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
|
||
{phase.trainingType}
|
||
</p>
|
||
<p className="text-sm font-medium mt-2">
|
||
{phase.weeklyLimit} min/week
|
||
</p>
|
||
<p className="text-xs text-zinc-500 mt-2">
|
||
{phase.description}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Strength Training Reference */}
|
||
<section>
|
||
<h2 className="text-lg font-semibold mb-4">
|
||
Strength Training (Follicular Phase)
|
||
</h2>
|
||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||
Mon/Wed/Fri during follicular phase (20-25 min per session)
|
||
</p>
|
||
<div className="bg-white dark:bg-zinc-900 rounded-lg border">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b">
|
||
<th className="text-left p-3 font-medium">Exercise</th>
|
||
<th className="text-left p-3 font-medium">Sets × Reps</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{STRENGTH_EXERCISES.map((exercise) => (
|
||
<tr key={exercise.name} className="border-b last:border-0">
|
||
<td className="p-3">{exercise.name}</td>
|
||
<td className="p-3 text-zinc-600 dark:text-zinc-400">
|
||
{exercise.sets}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Rebounding Techniques */}
|
||
<section>
|
||
<h2 className="text-lg font-semibold mb-4">
|
||
Rebounding Techniques
|
||
</h2>
|
||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
|
||
Adjust your rebounding style based on your current phase
|
||
</p>
|
||
<div className="grid gap-3 md:grid-cols-2">
|
||
{REBOUNDING_TECHNIQUES.map((item) => (
|
||
<div
|
||
key={item.phase}
|
||
className="bg-white dark:bg-zinc-900 rounded-lg border p-4"
|
||
>
|
||
<h3 className="font-medium">{item.phase}</h3>
|
||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
|
||
{item.techniques}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Weekly Schedule Reference */}
|
||
<section>
|
||
<h2 className="text-lg font-semibold mb-4">Weekly Guidelines</h2>
|
||
<div className="space-y-4">
|
||
<div className="bg-white dark:bg-zinc-900 rounded-lg border p-4">
|
||
<h3 className="font-medium">Menstrual Phase (Days 1-3)</h3>
|
||
<ul className="text-sm text-zinc-600 dark:text-zinc-400 mt-2 list-disc pl-5 space-y-1">
|
||
<li>Morning: 10-15 min gentle rebounding</li>
|
||
<li>Evening: 15-20 min restorative movement</li>
|
||
<li>No strength training</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-zinc-900 rounded-lg border p-4">
|
||
<h3 className="font-medium">Follicular Phase (Days 4-14)</h3>
|
||
<ul className="text-sm text-zinc-600 dark:text-zinc-400 mt-2 list-disc pl-5 space-y-1">
|
||
<li>Mon/Wed/Fri: Strength training (20-25 min)</li>
|
||
<li>Tue/Thu: Active recovery rebounding (20 min)</li>
|
||
<li>Weekend: Choose your adventure</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-zinc-900 rounded-lg border p-4">
|
||
<h3 className="font-medium">
|
||
Ovulation + Early Luteal (Days 15-24)
|
||
</h3>
|
||
<ul className="text-sm text-zinc-600 dark:text-zinc-400 mt-2 list-disc pl-5 space-y-1">
|
||
<li>Days 15-16: Peak performance (25-30 min strength)</li>
|
||
<li>
|
||
Days 17-21: Modified strength (reduce intensity 10-20%)
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-zinc-900 rounded-lg border p-4">
|
||
<h3 className="font-medium">Late Luteal Phase (Days 22-28)</h3>
|
||
<ul className="text-sm text-zinc-600 dark:text-zinc-400 mt-2 list-disc pl-5 space-y-1">
|
||
<li>Daily: Gentle rebounding only (15-20 min)</li>
|
||
<li>Optional light bodyweight Mon/Wed if feeling good</li>
|
||
<li>Rest days: Tue/Thu/Sat/Sun</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|