import * as React from "react"; import { format, parse, isWithinInterval, isSameDay } from "date-fns"; import { Calendar as CalendarIcon, X } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; import { Button } from "./button"; import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth, addMonths, subMonths, isToday, } from "date-fns"; import { ChevronLeft, ChevronRight } from "lucide-react"; export interface DateRangePickerProps { dateFrom?: string; // YYYY-MM-DD format dateTo?: string; // YYYY-MM-DD format onDateFromChange?: (value: string) => void; onDateToChange?: (value: string) => void; onClear?: () => void; placeholder?: string; disabled?: boolean; className?: string; error?: boolean; } export const DateRangePicker: React.FC = ({ dateFrom, dateTo, onDateFromChange, onDateToChange, onClear, placeholder = "Select date range", disabled = false, className = "", error = false, }) => { const [open, setOpen] = React.useState(false); const [currentMonth, setCurrentMonth] = React.useState(new Date()); const [hoverDate, setHoverDate] = React.useState(); // Parse dates safely let fromDate: Date | undefined = undefined; let toDate: Date | undefined = undefined; if (dateFrom && dateFrom.trim()) { try { const parsed = parse(dateFrom, "yyyy-MM-dd", new Date()); if (!isNaN(parsed.getTime())) { fromDate = parsed; } } catch (e) { fromDate = undefined; } } if (dateTo && dateTo.trim()) { try { const parsed = parse(dateTo, "yyyy-MM-dd", new Date()); if (!isNaN(parsed.getTime())) { toDate = parsed; } } catch (e) { toDate = undefined; } } const handleSelect = (date: Date) => { if (!fromDate || (fromDate && toDate)) { // Start new selection onDateFromChange?.(format(date, "yyyy-MM-dd")); onDateToChange?.(""); setHoverDate(undefined); } else if (fromDate && !toDate) { // Complete the range if (date < fromDate) { // If selected date is before start, swap them onDateToChange?.(format(fromDate, "yyyy-MM-dd")); onDateFromChange?.(format(date, "yyyy-MM-dd")); } else { onDateToChange?.(format(date, "yyyy-MM-dd")); } setHoverDate(undefined); setOpen(false); } }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); onClear?.(); setHoverDate(undefined); }; const isInRange = (date: Date) => { if (!fromDate) return false; const endDate = toDate || hoverDate; if (!endDate) return false; const start = fromDate < endDate ? fromDate : endDate; const end = fromDate < endDate ? endDate : fromDate; try { return isWithinInterval(date, { start, end }); } catch { return false; } }; const isRangeStart = (date: Date) => { return fromDate && isSameDay(date, fromDate); }; const isRangeEnd = (date: Date) => { const endDate = toDate || hoverDate; return endDate && isSameDay(date, endDate); }; // Helper to generate calendar days const getDaysForMonth = (monthDate: Date) => { const monthStart = startOfMonth(monthDate); const monthEnd = endOfMonth(monthDate); const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 }); const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 }); return eachDayOfInterval({ start: calendarStart, end: calendarEnd }); }; const nextMonthDate = addMonths(currentMonth, 1); const currentMonthDays = getDaysForMonth(currentMonth); const nextMonthDays = getDaysForMonth(nextMonthDate); const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; // Format display value let displayValue = ""; if (fromDate && !isNaN(fromDate.getTime())) { try { displayValue = format(fromDate, "MMM dd, yyyy"); if (toDate && !isNaN(toDate.getTime())) { displayValue += " - " + format(toDate, "MMM dd, yyyy"); } } catch (e) { displayValue = ""; } } const hasValue = dateFrom || dateTo; const renderMonth = (monthDate: Date, days: Date[]) => (
{format(monthDate, "MMMM yyyy")}
{weekDays.map((day) => (
{day}
))}
{days.map((day, idx) => { const isCurrentMonth = isSameMonth(day, monthDate); const isSelected = isRangeStart(day) || isRangeEnd(day); const inRange = isInRange(day); const isTodayDate = isToday(day); return ( ); })}
); return (
{renderMonth(currentMonth, currentMonthDays)}
{renderMonth(nextMonthDate, nextMonthDays)}
); };