Initial project setup for PhaseFlow

Set up Next.js 16 project with TypeScript for a training decision app
that integrates menstrual cycle phases with Garmin biometrics for
Hashimoto's thyroiditis management.

Stack: Next.js 16, React 19, Tailwind/shadcn, PocketBase, Drizzle,
Zod, Resend, Vitest, Biome, Lefthook, Nix dev environment.

Includes:
- 7 page routes (dashboard, login, settings, calendar, history, plan)
- 12 API endpoints (garmin, user, cycle, calendar, overrides, cron)
- Core lib utilities (decision engine, cycle phases, nutrition, ICS)
- Type definitions and component scaffolding
- Python script for Garmin token bootstrapping
- Initial unit tests for cycle utilities

🤖 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-09 16:50:39 +00:00
commit f15e093254
63 changed files with 6061 additions and 0 deletions

73
src/lib/cycle.ts Normal file
View File

@@ -0,0 +1,73 @@
// ABOUTME: Cycle phase calculation utilities.
// ABOUTME: Determines current cycle day and phase from last period date.
import type { CyclePhase, PhaseConfig } from "@/types";
export const PHASE_CONFIGS: PhaseConfig[] = [
{
name: "MENSTRUAL",
days: [1, 3],
weeklyLimit: 30,
dailyAvg: 10,
trainingType: "Gentle rebounding only",
},
{
name: "FOLLICULAR",
days: [4, 14],
weeklyLimit: 120,
dailyAvg: 17,
trainingType: "Strength + rebounding",
},
{
name: "OVULATION",
days: [15, 16],
weeklyLimit: 80,
dailyAvg: 40,
trainingType: "Peak performance",
},
{
name: "EARLY_LUTEAL",
days: [17, 24],
weeklyLimit: 100,
dailyAvg: 14,
trainingType: "Moderate training",
},
{
name: "LATE_LUTEAL",
days: [25, 31],
weeklyLimit: 50,
dailyAvg: 8,
trainingType: "Gentle rebounding ONLY",
},
];
export function getCycleDay(
lastPeriodDate: Date,
cycleLength: number,
currentDate: Date = new Date(),
): number {
const diffMs = currentDate.getTime() - lastPeriodDate.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return (diffDays % cycleLength) + 1;
}
export function getPhase(cycleDay: number): CyclePhase {
for (const config of PHASE_CONFIGS) {
if (cycleDay >= config.days[0] && cycleDay <= config.days[1]) {
return config.name;
}
}
// Default to late luteal for any days beyond 31
return "LATE_LUTEAL";
}
export function getPhaseConfig(phase: CyclePhase): PhaseConfig {
const config = PHASE_CONFIGS.find((c) => c.name === phase);
if (!config) {
throw new Error(`Unknown phase: ${phase}`);
}
return config;
}
export function getPhaseLimit(phase: CyclePhase): number {
return getPhaseConfig(phase).weeklyLimit;
}