import * as React from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth, isSameDay, addMonths, subMonths, setMonth, setYear, getMonth, getYear, isToday, } from "date-fns"; export interface CalendarProps { selected?: Date; onSelect?: (date: Date | undefined) => void; disabled?: (date: Date) => boolean; minDate?: Date; maxDate?: Date; className?: string; } const MONTH_LABELS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; export const Calendar: React.FC = ({ selected, onSelect, disabled, minDate, maxDate, className = "", }) => { // Open on the selected date (e.g. a saved date of birth lands on its own // year), falling back to today for a blank field. const [currentMonth, setCurrentMonth] = React.useState( () => selected ?? new Date(), ); // When the bound value changes (popover reopened with an existing value), // jump the grid to that month so the user isn't stranded on today. React.useEffect(() => { if (selected) setCurrentMonth(selected); }, [selected]); // Year range for the dropdown: honour min/max when provided, else a wide // span that comfortably covers dates of birth (back ~120 years) and future // trip dates (~10 years ahead). Listed newest-first. const today = new Date(); const fromYear = minDate ? getYear(minDate) : getYear(today) - 120; const toYear = maxDate ? getYear(maxDate) : getYear(today) + 10; const years: number[] = []; for (let y = toYear; y >= fromYear; y--) { years.push(y); } const handleMonthSelect = (monthIndex: number) => { setCurrentMonth(setMonth(currentMonth, monthIndex)); }; const handleYearSelect = (year: number) => { setCurrentMonth(setYear(currentMonth, year)); }; const monthStart = startOfMonth(currentMonth); const monthEnd = endOfMonth(currentMonth); const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 }); const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 }); const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd }); const isDateDisabled = (date: Date) => { if (disabled && disabled(date)) return true; if (minDate && date < minDate) return true; if (maxDate && date > maxDate) return true; return false; }; const handleDateClick = (date: Date) => { if (isDateDisabled(date)) return; onSelect?.(date); }; const previousMonth = () => { setCurrentMonth(subMonths(currentMonth, 1)); }; const nextMonth = () => { setCurrentMonth(addMonths(currentMonth, 1)); }; const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; return (
{/* Month dropdown — jump to any month in one click */} {/* Year dropdown — jump straight to e.g. 1990 without month-stepping */}
{weekDays.map((day) => (
{day}
))}
{days.map((day, dayIdx) => { const isCurrentMonth = isSameMonth(day, currentMonth); const isSelected = selected && isSameDay(day, selected); const isTodayDate = isToday(day); const isDisabled = isDateDisabled(day); return ( ); })}
); };