// ABOUTME: Compact calendar widget for the dashboard. // ABOUTME: Shows current month with color-coded cycle phases and navigation. "use client"; import { getCycleDay, getPhase } from "@/lib/cycle"; import type { CyclePhase } from "@/types"; interface MiniCalendarProps { lastPeriodDate: Date; cycleLength: number; year?: number; month?: number; onMonthChange?: (year: number, month: number) => void; } const PHASE_COLORS: Record = { MENSTRUAL: "bg-blue-100", FOLLICULAR: "bg-green-100", OVULATION: "bg-purple-100", EARLY_LUTEAL: "bg-yellow-100", LATE_LUTEAL: "bg-red-100", }; const COMPACT_DAY_NAMES = ["S", "M", "T", "W", "T", "F", "S"]; const PHASE_LEGEND = [ { name: "Menstrual", color: "bg-blue-100" }, { name: "Follicular", color: "bg-green-100" }, { name: "Ovulation", color: "bg-purple-100" }, { name: "Early Luteal", color: "bg-yellow-100" }, { name: "Late Luteal", color: "bg-red-100" }, ]; function getDaysInMonth(year: number, month: number): number { return new Date(year, month + 1, 0).getDate(); } function getFirstDayOfMonth(year: number, month: number): number { return new Date(year, month, 1).getDay(); } export function MiniCalendar({ lastPeriodDate, cycleLength, year, month, onMonthChange, }: MiniCalendarProps) { const today = new Date(); const displayYear = year ?? today.getFullYear(); const displayMonth = month ?? today.getMonth(); const daysInMonth = getDaysInMonth(displayYear, displayMonth); const firstDayOfWeek = getFirstDayOfMonth(displayYear, displayMonth); // Calculate current cycle day and phase for the header const currentCycleDay = getCycleDay(lastPeriodDate, cycleLength, today); const currentPhase = getPhase(currentCycleDay, cycleLength); const handlePreviousMonth = () => { if (displayMonth === 0) { onMonthChange?.(displayYear - 1, 11); } else { onMonthChange?.(displayYear, displayMonth - 1); } }; const handleNextMonth = () => { if (displayMonth === 11) { onMonthChange?.(displayYear + 1, 0); } else { onMonthChange?.(displayYear, displayMonth + 1); } }; const handleTodayClick = () => { onMonthChange?.(today.getFullYear(), today.getMonth()); }; // Build array of day cells const days: (Date | null)[] = []; // Add empty cells for days before the first day of the month for (let i = 0; i < firstDayOfWeek; i++) { days.push(null); } // Add the actual days of the month for (let day = 1; day <= daysInMonth; day++) { days.push(new Date(displayYear, displayMonth, day)); } return (
{/* Cycle info header */}

Day {currentCycleDay} · {currentPhase}

{/* Navigation with month/year */}
{new Date(displayYear, displayMonth).toLocaleDateString("en-US", { month: "long", year: "numeric", })}
{/* Compact day of week headers */}
{COMPACT_DAY_NAMES.map((dayName, index) => (
{dayName}
))}
{/* Calendar grid */}
{days.map((date, index) => { if (!date) { // biome-ignore lint/suspicious/noArrayIndexKey: Empty cells are always at fixed positions at the start of the month return
; } const cycleDay = getCycleDay(lastPeriodDate, cycleLength, date); const phase = getPhase(cycleDay, cycleLength); const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate(); return ( ); })}
{/* Compact phase legend */}
{PHASE_LEGEND.map((phase) => (
{phase.name}
))}
); }