Files
phaseflow/src/app/plan/page.tsx
Petru Paler 39198fdf8c Implement Plan page with phase overview and exercise reference (P2.13)
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>
2026-01-11 08:56:52 +00:00

302 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}