import moment from 'moment' import * as React from 'react' import { DayPicker, useDayPicker } from 'react-day-picker' import styles from './_calendar.module.scss' import Button from '../Button/Button' import Icon from '../Icons/Icon' import { c } from '../../translations/LibraryTranslationService' import { type CalendarProps } from './DatePicker.models' function Calendar({ className, showOutsideDays = false, showYearSwitcher = true, yearRange = 12, numberOfMonths, ...props }: CalendarProps) { const [navView, setNavView] = React.useState<'days' | 'months' | 'years'>( 'days', ) const [selectedYear, setSelectedYear] = React.useState( new Date().getFullYear(), ) const [displayYears, setDisplayYears] = React.useState<{ from: number to: number }>( React.useMemo(() => { return { from: selectedYear - Math.floor(yearRange / 2 - 1), to: selectedYear + Math.ceil(yearRange / 2), } }, [selectedYear, yearRange]), ) const { onNextClick, onPrevClick, startMonth, endMonth, mode, selected, dateRestrictions, } = props const columnsDisplayed = navView === 'days' && mode === 'range' ? numberOfMonths : 1 const [isTodaySelected, setIsTodaySelected] = React.useState(false) const [isOnlyStartSelected, setIsOnlyStartSelected] = React.useState(false) React.useEffect(() => { if (mode === 'range' && selected) { const range = selected as { from: Date; to: Date | undefined } const onlyStartSelected = Boolean( range.from && range.to && range.from.getTime() === range.to.getTime(), ) setIsOnlyStartSelected(onlyStartSelected) } else { setIsOnlyStartSelected(false) } const today = new Date() let isToday = false if (mode === 'single' && selected) { isToday = selected.getDate() === today.getDate() && selected.getMonth() === today.getMonth() && selected.getFullYear() === today.getFullYear() } setIsTodaySelected(isToday) }, [mode, selected]) function isYearSelected(year: number): string | false { if (mode === 'single' && selected) { return selected.getFullYear() === year ? 'selectedYear' : false } else if (mode === 'range' && selected) { const range = selected as { from: Date; to: Date | undefined } if (range.from && range.to) { const startYear = range.from.getFullYear() const endYear = range.to.getFullYear() if (startYear === endYear && year === startYear) { return 'sameMonthYear' } if (year === startYear) return 'rangeStartYear' if (year === endYear) return 'rangeEndYear' if (year > startYear && year < endYear) return 'rangeMiddleYear' } else if (range.from) { return range.from.getFullYear() === year ? 'rangeStartYear' : false } } return false } function isMonthSelected(month: number, year: number): string | false { if (mode === 'single' && selected) { return selected.getMonth() === month && selected.getFullYear() === year ? 'selectedMonth' : false } else if (mode === 'range' && selected) { const range = selected as { from: Date; to: Date | undefined } if (range.from && range.to) { const startDate = range.from const endDate = range.to const currentDate = new Date(year, month, 1) // Check if both dates are in the same month and year if ( startDate.getFullYear() === endDate.getFullYear() && startDate.getMonth() === endDate.getMonth() && year === startDate.getFullYear() && month === startDate.getMonth() ) { return 'sameMonthMonth' } if ( year === startDate.getFullYear() && month === startDate.getMonth() ) { return 'rangeStartMonth' } if (year === endDate.getFullYear() && month === endDate.getMonth()) { return 'rangeEndMonth' } if (currentDate > startDate && currentDate < endDate) { return 'rangeMiddleMonth' } } else if (range.from) { return range.from.getMonth() === month && range.from.getFullYear() === year ? 'rangeStartMonth' : false } } return false } function isMonthRestricted(month: number, year: number) { const monthEnd = new Date(year, month + 1, 0) // Check if all dates in the month are restricted const allDatesRestricted = Array.from( { length: monthEnd.getDate() }, (_, i) => { const date = new Date(year, month, i + 1) const isBeforeRestricted = dateRestrictions?.beforeDate && date < dateRestrictions.beforeDate const isAfterRestricted = dateRestrictions?.afterDate && date > dateRestrictions.afterDate return isBeforeRestricted || isAfterRestricted }, ).every((isRestricted) => isRestricted) return allDatesRestricted } function isYearRestricted(year: number) { // Check if all months in the year are restricted const allMonthsRestricted = Array.from({ length: 12 }, (_, month) => { return isMonthRestricted(month, year) }).every((isRestricted) => isRestricted) return allMonthsRestricted } const disabled = React.useMemo( () => [ ...(dateRestrictions?.beforeDate ? [{ before: dateRestrictions.beforeDate }] : []), ...(dateRestrictions?.afterDate ? [{ after: dateRestrictions.afterDate }] : []), ], [dateRestrictions?.beforeDate, dateRestrictions?.afterDate], ) const formatters = React.useMemo( () => ({ formatWeekdayName: (weekday: Date) => { const shortName = weekday .toLocaleDateString('en', { weekday: 'short' }) .toLowerCase() .slice(0, 3) return c(shortName) }, formatMonthCaption: (month: Date) => { // Always use full month name for the main calendar header const monthName = month .toLocaleDateString('en', { month: 'long' }) .toLowerCase() return `${c(monthName)} ${month.getFullYear()}` }, }), [], ) /** * Generate dynamic CSS for selected, disabled single date * * Why we use dangerouslySetInnerHTML instead of pure CSS: * - CSS cannot dynamically match data attribute values (e.g., data-day="X" where X is from JS state) * - CSS is declarative/static and cannot read JavaScript state about which date is selected * - This approach is performant (one small style tag) and works with CSS modules * * Safety: The HTML content is completely controlled by us (no user input), making it safe to use. */ const selectedDisabledStyle = React.useMemo(() => { if (mode === 'single' && selected) { const singleDate = selected as Date // Check if the selected date is disabled by date restrictions const isDisabled = disabled.some((disabledRule) => { if (disabledRule.before && singleDate < disabledRule.before) return true if (disabledRule.after && singleDate > disabledRule.after) return true return false }) if (isDisabled) { // Format date as YYYY-MM-DD to match react-day-picker's data-day attribute const year = singleDate.getFullYear() const month = String(singleDate.getMonth() + 1).padStart(2, '0') const day = String(singleDate.getDate()).padStart(2, '0') const dataDay = `${year}-${month}-${day}` // Generate CSS that targets the specific disabled date and applies selected styling return { __html: ` .${styles.calendar} [data-day="${dataDay}"] .${styles.dayButton} { background-color: var(--dark-blue) !important; color: var(--white) !important; cursor: default; } `, } } } return null }, [selected, mode, disabled]) return ( <> {/* Inject dynamic CSS for selected disabled dates This ensures users can see that their date selection was registered even when the date is outside the allowed restrictions */} {selectedDisabledStyle && (