import React from 'react'; // date-fns import { addWeeks, eachDayOfInterval, endOfMonth, endOfWeek, format, getMonth, getYear, isSameMonth, startOfDay, startOfMonth, startOfWeek } from 'date-fns'; import { Locale } from 'date-fns'; import { enUS } from 'date-fns/locale'; import { nl } from 'date-fns/locale'; import { fr } from 'date-fns/locale'; // redux import { useSelector } from 'react-redux'; import { QSMRootState } from '../../store/qsm-store'; // internal component import CalendarDay from './calendar-day'; // Props interface interface CalendarProps { year?: number; month?: number; hasPreviousButton?: boolean; hasNextButton?: boolean; hasFixedHeight?: boolean; weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; isStartDate?: (date: Date) => boolean; isEndDate?: (date: Date) => boolean; isInRange?: (date: Date) => boolean; onDayClick?: (date: Date) => void; onDayMouseOver?: (date: Date) => void; onNextClick?: (year: number, month: number) => void; onPreviousClick?: (year: number, month: number) => void; locale?: Locale; extraClassNamesFunction?: (date: Date) => string[]; isMobile?: boolean; minDate?: Date; maxDate?: Date; } const Calendar: React.FC = ({ year = getYear(new Date()), month = getMonth(new Date()), hasPreviousButton = true, hasNextButton = true, hasFixedHeight = true, weekStartsOn = 1, isStartDate, isEndDate, isInRange, onDayClick, onDayMouseOver, onNextClick, onPreviousClick, extraClassNamesFunction, isMobile = false, minDate, maxDate }) => { const language = useSelector((state: QSMRootState) => state.qsm.language); const languageCode = language.split('-')[0]; const localeMap: Record = { en: enUS, nl: nl, fr: fr }; const currentLocale = localeMap[languageCode] || enUS; // Date range setup const focusDate = new Date(year, month); const firstDay = startOfWeek(startOfMonth(focusDate), { weekStartsOn }); const lastDay = hasFixedHeight ? endOfWeek(addWeeks(firstDay, 5), { weekStartsOn }) : endOfWeek(endOfMonth(focusDate), { weekStartsOn }); const calendarDays = eachDayOfInterval({ start: firstDay, end: lastDay }); // Event handlers const handleDayClick = (day: Date) => onDayClick?.(day); const handleDayMouseOver = (day: Date) => onDayMouseOver?.(day); const handlePreviousClick = () => { const prevMonth = (month - 1 + 12) % 12; const prevYear = prevMonth > month ? year - 1 : year; onPreviousClick?.(prevYear, prevMonth); }; const handleNextClick = () => { const baseMonth = isMobile ? month + 2 : month + 1; const nextMonth = baseMonth % 12; const nextYear = nextMonth < month ? year + 1 : year; onNextClick?.(nextYear, nextMonth); }; const isBeforeMin = (date: Date) => (minDate ? startOfDay(date) < startOfDay(minDate) : false); const isAfterMax = (date: Date) => (maxDate ? startOfDay(date) > startOfDay(maxDate) : false); const isOutOfBounds = (date: Date) => isBeforeMin(date) || isAfterMax(date); // Build weeks for rendering const weekRows = []; for (let i = 0; i < calendarDays.length; i += 7) { weekRows.push(calendarDays.slice(i, i + 7)); } return (
{/* Header */}
{hasPreviousButton && ( )}
{format(focusDate, 'MMMM yyyy', { locale: currentLocale }).replace(/^(.)(.*)$/, (_, a, b) => `${a.toUpperCase()}${b}`)}
{hasNextButton && ( )}
{/* Week day labels */}
{[0, 1, 2, 3, 4, 5, 6].map((i) => (
{format(calendarDays[i], 'eee', { locale: currentLocale }) .slice(0, 2) .replace(/^(.)(.)$/, (_, a, b) => `${a.toUpperCase()}${b.toLowerCase()}`)}
))}
{/* Calendar grid */}
{weekRows.map((week, index) => (
{week.map((day) => { const isDisabled = isOutOfBounds(day); const isSelected = isStartDate?.(day) || isEndDate?.(day); const inRange = isInRange?.(day) ?? false; const isOutsideMonth = !isSameMonth(day, focusDate); let extraClassNames: string[] = []; if (inRange) extraClassNames.push('calendar__day--range'); if (isSelected) extraClassNames.push('calendar__day--selected'); if (extraClassNamesFunction) { const additional = extraClassNamesFunction(day); if (Array.isArray(additional)) { extraClassNames = extraClassNames.concat(additional); } } return ( ); })}
))}
); }; export default Calendar;