import React, { useCallback, useMemo } from 'react' import { Calendar as RNCalendar, DateData } from 'react-native-calendars' import { AnyRecord, IJSX, StyledComponentProps } from '@codeleap/styles' import { MobileStyleRegistry } from '../../Registry' import { useStylesFor } from '../../hooks' import { CalendarProps } from './types' import dayjs, { Dayjs } from 'dayjs' import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' import { dateUtils } from '@codeleap/utils' dayjs.extend(isSameOrBefore) export * from './styles' export * from './types' /** react-native-calendars requires dates keyed as YYYY-MM-DD strings in markedDates; any other format silently produces no highlights. */ const DATE_FORMAT = 'YYYY-MM-DD' export const Calendar = (props: CalendarProps) => { const { style, value, onValueChange, ...calendarProps } = props const styles = useStylesFor(Calendar.styleRegistryName, style) const isRange = Array.isArray(value) /** `removeTimezoneAndFormat` strips the timezone offset before formatting so that a UTC midnight doesn't shift to the previous calendar day in negative-offset locales. */ const stringValue = useMemo(() => { if (!value) return isRange ? [] : '' if (isRange) { return (value as any).map((v) => dateUtils.removeTimezoneAndFormat(v, DATE_FORMAT)) } return dateUtils.removeTimezoneAndFormat(value, DATE_FORMAT) }, [value, isRange]) const markedDates = useMemo(() => { if (!isRange) { return stringValue ? { [stringValue as string]: { selected: true } } : {} } const rangeValues = stringValue as string[] if (rangeValues.length === 0) return {} if (rangeValues.length === 1) { return { [rangeValues[0]]: { selected: true } } } const [start, end] = rangeValues const startDate = dayjs(start) const endDate = dayjs(end) const marked: Record = {} let current = startDate while (current.isSameOrBefore(endDate)) { const dateStr = dateUtils.removeTimezoneAndFormat(current, DATE_FORMAT) marked[dateStr] = { selected: true, ...(current.isSame(startDate) && { startingDay: true }), ...(current.isSame(endDate) && { endingDay: true }), } current = current.add(1, 'day') } return marked }, [stringValue, isRange]) /** * Range selection uses a two-tap model: first tap sets start (array of 1), second tap completes * the range (array of 2). Tapping again resets to a new start. Always sorted so start ≤ end. */ const handleDateChange = useCallback((date: DateData) => { if (!onValueChange) return const selected = dayjs(date.dateString).startOf('day') if (isRange) { const current = Array.isArray(value) ? value : [] let newDates: Dayjs[] = [] if (current.length === 0 || current.length === 2) { newDates = [selected] } else if (current.length === 1) { const first = dayjs(current[0]).startOf('day') newDates = first.isAfter(selected) ? [selected, first] : [first, selected] } onValueChange(newDates) } else { onValueChange(selected) } }, [onValueChange, value, isRange]) const currentValue = useMemo(() => { return isRange ? (Array.isArray(stringValue) ? stringValue[0] : '') : stringValue }, [stringValue, isRange]) return ( ) } Calendar.styleRegistryName = 'Calendar' Calendar.elements = ['wrapper', 'header', 'calendar'] Calendar.rootElement = 'wrapper' Calendar.withVariantTypes = (styles: S) => { return Calendar as (props: StyledComponentProps) => IJSX } Calendar.defaultProps = {} as Partial MobileStyleRegistry.registerComponent(Calendar)