import moment from 'moment' import React, { useEffect, useMemo, useState } from 'react' import { DatePicker } from './Calendar/DatePicker' import { DatePickerStateProvider } from '@rehookify/datepicker' import { Menu, MenuButtonProps, useMenu } from '../Menu/Menu' import { Text } from '../Text/Text' import { IconSolidCheck, IconSolidCheveronDown, IconSolidCheveronRight, IconSolidClock, } from '../icons' import { Stack } from '../Stack/Stack' import { Box } from '../Box/Box' import { colors } from '../../css/colors' import { TimeInput } from './TimeInput' import { DateInput } from './DateInput' import { Form } from '../Form/Form' import * as Ariakit from '@ariakit/react' export { defaultPresets, getNow, resetRelativeDates } from './utils' const DATE_INPUT_FORMAT_WITH_COMMA = 'MMM DD, YYYY' const DATE_INPUT_FORMAT_WITH_SINGLE_DAY = 'MMM D, YYYY' const DATE_INPUT_FORMAT_WITH_SINGLE_DAY_AND_NO_COMMA = 'MMM D YYYY' const DATE_INPUT_FORMAT_WITH_NO_COMMA = 'MMM DD YYYY' const DATE_INPUT_FORMAT_WITH_SLASH = 'MM/DD/YYYY' const DATE_INPUT_FORMAT_WITH_DASH = 'MM-DD-YYYY' const DATE_INPUT_FORMAT_WITH_DOT = 'MM.DD.YYYY' const TIME_INPUT_FORMAT = 'HH:mm a' const TIME_INPUT_FORMAT_NO_SPACE = 'HH:mma' const TIME_INPUT_FORMAT_12_HOUR = 'h:mm a' const TIME_INPUT_FORMAT_12_HOUR_NO_SPACE = 'h:mma' const TIME_INPUT_FORMAT_24_HOURS_MINUTES_NO_AM_PM_24 = 'HH:mm' const TIME_INPUT_FORMAT_12_HOURS_NO_AM_PM = 'h:mm' const TIME_INPUT_FORMAT_HOURS_NO_MINUTES = 'h a' const TIME_INPUT_FORMAT_HOURS_NO_MINUTES_NO_AM_PM = 'h' const TIME_INPUT_FORMAT_HOURS_NO_MINUTES_NO_SPACE = 'ha' const TIME_INPUT_FORMAT_HOURS_NO_MINUTES_NO_AM_PM_24_HOUR = 'HH' const TIME_DISPLAY_FORMAT = 'hh:mm a' const now = new Date() export type Preset = { label: string startDate: Date } enum MenuState { Default, Custom, } const isPresetSelected = ({ preset, selectedDates, }: { preset: Preset selectedDates: Date[] }) => { return preset.startDate.getTime() === selectedDates[0].getTime() } const isCustomSelected = ({ presets, selectedDates, }: { presets: Preset[] selectedDates: Date[] }) => { const foundPreset = presets.find((preset) => { return isPresetSelected({ preset, selectedDates }) }) return !foundPreset } function toDateTimeString(date: Date, showYear: boolean) { const options: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', } if (showYear) { options.year = 'numeric' } return date.toLocaleDateString('en-us', options) } /** * Takes in a time string in the format HH:mm aa or HH:mmaa and returns an object with the hour, minute, time of day, and 24 hour time * @param timeString * @returns { hour: number, minute: number, timeOfDay: string, hour24: number} */ export const getTimeInfo = (timeString: string) => { const date = moment(timeString, TIME_INPUT_FORMAT) return { hour: date.format('HH'), minute: date.minute(), timeOfDay: date.format('a'), hour24: date.hour(), } } /** * Takes in a date and a time string in the format HH:mm aa or HH:mmaa and returns a new date with the time set * @param date Date * @param hour number * @param minute number * @returns new date with time set */ const setTimeOnDate = (date: Date, hour: number, minute: number) => { const newDate = new Date(date) newDate.setHours(hour) newDate.setMinutes(minute) return newDate } /** * Converts a Date object to a string in the format "HH:mm aa". * * @param {Date} date - The Date object to convert. * @returns {string} A string in the format "HH:mm aa". */ const getTimeStringFromDate = (date: Date): string => { if (!date) { return '12:00 AM' } return moment(date).format(TIME_DISPLAY_FORMAT) } /** * Format date to be displayed in the input. * @param date {Date} * @returns {string} */ const formatDisplayedDate = (date: Date) => { if (!date) { return '' } return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }) } export const getLabel = ({ selectedDates, presets, }: { selectedDates: Date[] presets: Preset[] }) => { const foundPreset = presets.find((preset) => { return isPresetSelected({ preset, selectedDates }) }) if (foundPreset) { return foundPreset.label } if (selectedDates.length == 2 && selectedDates[1] && selectedDates[0]) { const showYear = selectedDates[1].getFullYear() > selectedDates[0].getFullYear() return `${toDateTimeString( selectedDates[0], showYear, )} - ${toDateTimeString(selectedDates[1], showYear)}` } return '' } type Props = { selectedDates: Date[] onDatesChange: (selectedDates: Date[]) => void presets: Preset[] minDate: Date noCustom?: boolean } & Omit export const PreviousDateRangePicker: React.FC = (props) => ( {/* Rendering inside wrapper so we can work with menu store via useMenu. */} ) const CheckboxIconIfSelected = ({ isSelected, size = 16, }: { isSelected: boolean size?: number }) => { return ( {isSelected && } ) } const PreviousDateRangePickerImpl = ({ selectedDates, onDatesChange, presets, noCustom, minDate, ...props }: Props) => { const [menuState, setMenuState] = React.useState( MenuState.Default, ) const [showingTime, setShowingTime] = useState(false) const [startTimeIsValid, setStartTimeIsValid] = useState(true) const [endTimeIsValid, setEndTimeIsValid] = useState(true) const menu = useMenu() const open = menu.getState().open const formStore = Ariakit.useFormStore({}) useEffect(() => { if (!open) { setMenuState(MenuState.Default) } }, [open]) // Close the time picker when the menu is closed useEffect(() => { if (menuState === MenuState.Default) { setShowingTime(false) } }, [menuState]) useEffect(() => { if (showingTime === false) { setStartTimeIsValid(true) setEndTimeIsValid(true) } }, [showingTime]) const startDatePlaceholder = useMemo( () => formatDisplayedDate(selectedDates[0]), [selectedDates[0]], ) const endDatePlaceholder = useMemo( () => formatDisplayedDate(selectedDates[1]), [selectedDates[1]], ) const startTimePlaceholder = useMemo( () => getTimeStringFromDate(selectedDates[0]), [selectedDates[0]], ) const endTimePlaceholder = useMemo( () => getTimeStringFromDate(selectedDates[1]), [selectedDates[1]], ) const isTimepickerDisabled = useMemo( () => selectedDates.length != 2, [selectedDates], ) const [buttonLabel, setButtonLabel] = useState( getLabel({ selectedDates, presets }), ) const handleShowingTimeToggle = () => { setShowingTime((prevShowingTime) => !prevShowingTime) } const handleDatesChange = (dates: Date[]) => { onDatesChange(dates) if (dates.length == 2) { menu.setOpen(false) setMenuState(MenuState.Default) } } const handleStartDateInput = (value: string) => { const isValidDateInput = [ DATE_INPUT_FORMAT_WITH_SLASH, DATE_INPUT_FORMAT_WITH_DASH, DATE_INPUT_FORMAT_WITH_DOT, DATE_INPUT_FORMAT_WITH_COMMA, DATE_INPUT_FORMAT_WITH_NO_COMMA, DATE_INPUT_FORMAT_WITH_SINGLE_DAY_AND_NO_COMMA, DATE_INPUT_FORMAT_WITH_SINGLE_DAY, ].some((format) => moment(value, format, true).isValid()) if (isValidDateInput) { const newDate = moment(value).toDate() const newDates = [newDate, selectedDates[1]] onDatesChange(newDates) } } const handleEndDateInput = (value: string) => { const isValidDateInput = [ DATE_INPUT_FORMAT_WITH_SLASH, DATE_INPUT_FORMAT_WITH_DASH, DATE_INPUT_FORMAT_WITH_DOT, DATE_INPUT_FORMAT_WITH_COMMA, DATE_INPUT_FORMAT_WITH_NO_COMMA, DATE_INPUT_FORMAT_WITH_SINGLE_DAY_AND_NO_COMMA, DATE_INPUT_FORMAT_WITH_SINGLE_DAY, ] .map((format) => moment(value, format, true).isValid()) .some((isValid) => isValid) if (isValidDateInput) { const newDate = moment(value).toDate() const newDates = [selectedDates[0], newDate] onDatesChange(newDates) } } const handleTimeChange = (value: string, input: 'start' | 'end') => { const isValid = [ TIME_INPUT_FORMAT, TIME_INPUT_FORMAT_NO_SPACE, TIME_INPUT_FORMAT_12_HOUR, TIME_INPUT_FORMAT_12_HOUR_NO_SPACE, TIME_INPUT_FORMAT_HOURS_NO_MINUTES, TIME_INPUT_FORMAT_HOURS_NO_MINUTES_NO_SPACE, TIME_INPUT_FORMAT_HOURS_NO_MINUTES_NO_AM_PM, TIME_INPUT_FORMAT_HOURS_NO_MINUTES_NO_AM_PM_24_HOUR, TIME_INPUT_FORMAT_24_HOURS_MINUTES_NO_AM_PM_24, TIME_INPUT_FORMAT_12_HOURS_NO_AM_PM, ] .map((format) => moment(value, format, true).isValid()) .some((isValid) => isValid) if (input === 'start') { setStartTimeIsValid(isValid) if (!isValid) { return } const timeInfo = getTimeInfo(value) const startDate = setTimeOnDate( selectedDates[0], timeInfo.hour24, timeInfo.minute, ) onDatesChange([startDate, selectedDates[1]]) return } setEndTimeIsValid(isValid) if (!isValid) { return } const timeInfo = getTimeInfo(value) const endDate = setTimeOnDate( selectedDates[1], timeInfo.hour24, timeInfo.minute, ) onDatesChange([selectedDates[0], endDate]) } useEffect(() => { if (selectedDates.length == 2) { setButtonLabel(getLabel({ selectedDates, presets })) } }, [selectedDates[0]?.getTime(), selectedDates[1]?.getTime()]) const hasSelectedRange = useMemo( () => selectedDates.length === 2 && selectedDates.filter((date) => moment(date).isValid()).length === 2, [selectedDates], ) return ( } style={{ whiteSpace: 'nowrap', overflow: 'hidden', }} {...(props as Omit)} > {buttonLabel} {menuState === MenuState.Default ? ( <> {presets.map((preset) => { return ( { e.preventDefault() e.stopPropagation() handleDatesChange([ preset.startDate, now, ]) }} > {preset.label} ) })} {noCustom ? null : ( { e.preventDefault() e.stopPropagation() setMenuState(MenuState.Custom) }} > Custom )} ) : (
{showingTime ? ( ) : null} {showingTime ? ( ) : null} { e.preventDefault() e.stopPropagation() }} >
)}
) }