import { useEffect, useMemo, useRef, useState } from 'react' import type { DateRange } from '../lib/types' const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] function dateKey(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` } function normalize(from: string, to: string): DateRange { return from <= to ? { from, to } : { from: to, to: from } } function addDays(date: Date, days: number): Date { return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days) } export function RangeCalendar({ value, onSelect }: { value: DateRange | null; onSelect: (range: DateRange) => void }) { const today = useMemo(() => new Date(), []) const todayKey = dateKey(today) const [month, setMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1)) const [preview, setPreview] = useState(value) const dragAnchor = useRef(null) const clickAnchor = useRef(null) useEffect(() => setPreview(value), [value]) useEffect(() => { const finishDrag = () => { dragAnchor.current = null } document.addEventListener('mouseup', finishDrag) return () => document.removeEventListener('mouseup', finishDrag) }, []) const first = new Date(month.getFullYear(), month.getMonth(), 1) const gridStart = addDays(first, -first.getDay()) const days = Array.from({ length: 42 }, (_, index) => addDays(gridStart, index)) const shown = preview ?? value const commit = (from: string, to: string) => { const range = normalize(from, to) clickAnchor.current = null dragAnchor.current = null setPreview(range) onSelect(range) } return (
{month.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
{WEEKDAYS.map(day => {day})} {days.map(day => { const key = dateKey(day) const outside = day.getMonth() !== month.getMonth() const disabled = key > todayKey const endpoint = shown ? key === shown.from || key === shown.to : false const inRange = shown ? key >= shown.from && key <= shown.to : false const className = [ 'calendar-day', outside ? 'outside' : '', inRange ? 'in-range' : '', endpoint ? 'endpoint' : '', ].filter(Boolean).join(' ') return ( ) })}
) }