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:
73
src/lib/cycle.ts
Normal file
73
src/lib/cycle.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user