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:
97
src/lib/ics.ts
Normal file
97
src/lib/ics.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// ABOUTME: ICS calendar feed generation for cycle phase events.
|
||||
// ABOUTME: Creates subscribable calendar with phase blocks and warnings.
|
||||
import { createEvents, type EventAttributes } from "ics";
|
||||
|
||||
import { getCycleDay, getPhase, PHASE_CONFIGS } from "./cycle";
|
||||
|
||||
const PHASE_EMOJIS: Record<string, string> = {
|
||||
MENSTRUAL: "🔵",
|
||||
FOLLICULAR: "🟢",
|
||||
OVULATION: "🟣",
|
||||
EARLY_LUTEAL: "🟡",
|
||||
LATE_LUTEAL: "🔴",
|
||||
};
|
||||
|
||||
interface IcsGeneratorOptions {
|
||||
lastPeriodDate: Date;
|
||||
cycleLength: number;
|
||||
monthsAhead?: number;
|
||||
}
|
||||
|
||||
export function generateIcsFeed(options: IcsGeneratorOptions): string {
|
||||
const { lastPeriodDate, cycleLength, monthsAhead = 3 } = options;
|
||||
const events: EventAttributes[] = [];
|
||||
|
||||
const endDate = new Date();
|
||||
endDate.setMonth(endDate.getMonth() + monthsAhead);
|
||||
|
||||
const currentDate = new Date(lastPeriodDate);
|
||||
let currentPhase = getPhase(
|
||||
getCycleDay(lastPeriodDate, cycleLength, currentDate),
|
||||
);
|
||||
let phaseStartDate = new Date(currentDate);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const cycleDay = getCycleDay(lastPeriodDate, cycleLength, currentDate);
|
||||
const phase = getPhase(cycleDay);
|
||||
|
||||
// Add warning events
|
||||
if (cycleDay === 22) {
|
||||
events.push({
|
||||
start: dateToArray(currentDate),
|
||||
end: dateToArray(currentDate),
|
||||
title: "⚠️ Late Luteal Phase Starts in 3 Days",
|
||||
description: "Begin reducing training intensity",
|
||||
});
|
||||
}
|
||||
|
||||
if (cycleDay === 25) {
|
||||
events.push({
|
||||
start: dateToArray(currentDate),
|
||||
end: dateToArray(currentDate),
|
||||
title: "🔴 CRITICAL PHASE - Gentle Rebounding Only!",
|
||||
description: "Late luteal phase - protect your cycle",
|
||||
});
|
||||
}
|
||||
|
||||
// Track phase changes
|
||||
if (phase !== currentPhase) {
|
||||
// Close previous phase event
|
||||
events.push(createPhaseEvent(currentPhase, phaseStartDate, currentDate));
|
||||
currentPhase = phase;
|
||||
phaseStartDate = new Date(currentDate);
|
||||
}
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
// Close final phase
|
||||
events.push(createPhaseEvent(currentPhase, phaseStartDate, currentDate));
|
||||
|
||||
const { value, error } = createEvents(events);
|
||||
if (error) {
|
||||
throw new Error(`ICS generation error: ${error}`);
|
||||
}
|
||||
|
||||
return value || "";
|
||||
}
|
||||
|
||||
function createPhaseEvent(
|
||||
phase: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): EventAttributes {
|
||||
const config = PHASE_CONFIGS.find((c) => c.name === phase);
|
||||
const emoji = PHASE_EMOJIS[phase] || "📅";
|
||||
|
||||
return {
|
||||
start: dateToArray(startDate),
|
||||
end: dateToArray(endDate),
|
||||
title: `${emoji} ${phase.replace("_", " ")}`,
|
||||
description: config?.trainingType || "",
|
||||
};
|
||||
}
|
||||
|
||||
function dateToArray(date: Date): [number, number, number, number, number] {
|
||||
return [date.getFullYear(), date.getMonth() + 1, date.getDate(), 0, 0];
|
||||
}
|
||||
Reference in New Issue
Block a user