import { endOfDay, endOfMonth, getMonth, getYear, isAfter, isEqual, isWithinInterval, Locale, startOfDay } from 'date-fns'; import { isNil } from 'lodash'; import React, { useContext, useEffect, useState } from 'react'; import Calendar from './calendar'; import SettingsContext from '../../settings-context'; interface DateRangePickerProps { fromDate?: Date; toDate?: Date; focusMonth?: { month: number; year: number }; locale?: Locale; duration?: number; disabledDaysFunction?: (date: Date) => boolean; extraClassNamesFunction?: (date: Date) => string[]; dayContentFunction?: (date: Date) => JSX.Element | null | undefined; onSelectionChange?: (fromDate: Date, toDate: Date) => void; onFromDateChange?: (date?: Date) => void; onToDateChange?: (date?: Date) => void; onFocusMonthChange?: (focusMonth: { month: number; year: number }) => void; toDateByFromDate?: (fromDate?: Date) => Date | undefined; } const DateRangePicker: React.FC = (props) => { const [fromDate, setFromDate] = useState(props.fromDate); const [toDate, setToDate] = useState(props.toDate); const [focusMonth, setFocusMonth] = useState<{ month: number; year: number }>( props.focusMonth ?? { year: getYear(new Date()), month: getMonth(new Date()) } ); const [isWaitingForToDate, setWaitingForToDate] = useState(false); const { searchType = 0 } = useContext(SettingsContext); const handleDayClick = (day: Date) => { const { onSelectionChange } = props; if (isWaitingForToDate && !isNil(fromDate) && isAfter(day, fromDate)) { if (searchType === 1) { // Only allow selecting the exact toDate const expectedToDate = props.toDateByFromDate?.(fromDate); if (expectedToDate && isEqual(day, expectedToDate)) { setToDate(day); setWaitingForToDate(false); props.onToDateChange?.(undefined); onSelectionChange?.(fromDate, day); } } else { // searchType === 0 (original behavior) setToDate(day); setWaitingForToDate(false); props.onToDateChange?.(undefined); onSelectionChange?.(fromDate, day); } } else { setFromDate(day); if (searchType === 1 && props.toDateByFromDate) { const to = props.toDateByFromDate(day); setToDate(to); if (to) { onSelectionChange?.(day, to); } } else if (props.duration) { const to = new Date(Date.UTC(day.getFullYear(), day.getMonth(), day.getDate() + props.duration)); setToDate(to); onSelectionChange?.(day, to); } else { setToDate(undefined); setWaitingForToDate(true); } props.onFromDateChange?.(day); } }; const handleDayMouseOver = (day: Date) => { if (isWaitingForToDate && (isNil(fromDate) || isEqual(day, fromDate) || isAfter(day, fromDate))) { setToDate(day); } }; const handlePreviousClick = () => { const { month, year } = focusMonth; const previousMonth = (month - 1) % 12; const previousMonthsYear = previousMonth > month ? year - 1 : year; const newFocusMonth = { year: previousMonthsYear, month: previousMonth }; setFocusMonth(newFocusMonth); if (props.onFocusMonthChange) { props.onFocusMonthChange(newFocusMonth); } }; const handleNextClick = () => { const { month, year } = focusMonth; const nextMonth = (month + 1) % 12; const nextMonthsYear = nextMonth < month ? year + 1 : year; const newFocusMonth = { year: nextMonthsYear, month: nextMonth }; setFocusMonth(newFocusMonth); if (props.onFocusMonthChange) { props.onFocusMonthChange(newFocusMonth); } }; const today = startOfDay(new Date()); const firstCalendarYear = focusMonth.year; const firstCalendarMonth = focusMonth.month; const secondCalendarMonth = (firstCalendarMonth + 1) % 12; const secondCalendarYear = secondCalendarMonth < firstCalendarMonth ? firstCalendarYear + 1 : firstCalendarYear; const checkIfDateIsSelected = (date: Date) => isNil(toDate) ? !isNil(fromDate) && isEqual(date, fromDate) : !isNil(fromDate) && isWithinInterval(date, { start: startOfDay(fromDate), end: endOfDay(toDate) }); useEffect(() => { setFromDate(props.fromDate); setToDate(props.toDate); }, [props.fromDate?.valueOf(), props.toDate?.valueOf()]); useEffect(() => { if (props.fromDate) { setFocusMonth({ month: props.fromDate.getMonth(), year: props.fromDate.getFullYear() }); } }, [props.fromDate?.valueOf()]); return (
); }; export default DateRangePicker;