import { useEffect } from "react"; import { shallow } from "zustand/shallow"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { useEmbedStyles } from "@calcom/embed-core/embed-iframe"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import classNames from "@calcom/lib/classNames"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { weekdayNames } from "@calcom/lib/weekday"; import { Button, SkeletonText } from "@calcom/ui"; import { ChevronLeft, ChevronRight } from "@calcom/ui/components/icon"; import { ArrowRight } from "@calcom/ui/components/icon"; export type DatePickerProps = { /** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */ weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6; /** Fires whenever a selected date is changed. */ onChange: (date: Dayjs | null) => void; /** Fires when the month is changed. */ onMonthChange?: (date: Dayjs) => void; /** which date or dates are currently selected (not tracked from here) */ selected?: Dayjs | Dayjs[] | null; /** defaults to current date. */ minDate?: Dayjs; /** Furthest date selectable in the future, default = UNLIMITED */ maxDate?: Dayjs; /** locale, any IETF language tag, e.g. "hu-HU" - defaults to Browser settings */ locale: string; /** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */ excludedDates?: string[]; /** defaults to all, which dates are bookable (inverse of excludedDates) */ includedDates?: string[]; /** allows adding classes to the container */ className?: string; /** Shows a small loading spinner next to the month name */ isLoading?: boolean; /** used to query the multiple selected dates */ eventSlug?: string; }; export const Day = ({ date, active, disabled, ...props }: JSX.IntrinsicElements["button"] & { active: boolean; date: Dayjs; }) => { const { t } = useLocale(); const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton"); const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton"); return ( ); }; const NoAvailabilityOverlay = ({ month, nextMonthButton, }: { month: string | null; nextMonthButton: () => void; }) => { const { t } = useLocale(); return (

{t("no_availability_in_month", { month: month })}

); }; const Days = ({ minDate = dayjs.utc(), excludedDates = [], browsingDate, weekStart, DayComponent = Day, selected, month, nextMonthButton, eventSlug, ...props }: Omit & { DayComponent?: React.FC>; browsingDate: Dayjs; weekStart: number; month: string | null; nextMonthButton: () => void; }) => { // Create placeholder elements for empty days in first week const weekdayOfFirst = browsingDate.date(1).day(); const currentDate = minDate.utcOffset(browsingDate.utcOffset()); const availableDates = (includedDates: string[] | undefined) => { const dates = []; const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate)); for ( let date = currentDate; date.isBefore(lastDateOfMonth) || date.isSame(lastDateOfMonth, "day"); date = date.add(1, "day") ) { // even if availableDates is given, filter out the passed included dates if (includedDates && !includedDates.includes(yyyymmdd(date))) { continue; } dates.push(yyyymmdd(date)); } return dates; }; const includedDates = currentDate.isSame(browsingDate, "month") ? availableDates(props.includedDates) : props.includedDates; const days: (Dayjs | null)[] = Array((weekdayOfFirst - weekStart + 7) % 7).fill(null); for (let day = 1, dayCount = daysInMonth(browsingDate); day <= dayCount; day++) { const date = browsingDate.set("date", day); days.push(date); } const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow); const isActive = (day: dayjs.Dayjs) => { // for selecting a range of dates if (Array.isArray(selected)) { return Array.isArray(selected) && selected?.some((e) => yyyymmdd(e) === yyyymmdd(day)); } if (selected && yyyymmdd(selected) === yyyymmdd(day)) { return true; } // for selecting multiple dates for an event if ( eventSlug && selectedDatesAndTimes && selectedDatesAndTimes[eventSlug as string] && Object.keys(selectedDatesAndTimes[eventSlug as string]).length > 0 ) { return Object.keys(selectedDatesAndTimes[eventSlug as string]).some((date) => { return yyyymmdd(dayjs(date)) === yyyymmdd(day); }); } return false; }; const daysToRenderForTheMonth = days.map((day) => { if (!day) return { day: null, disabled: true }; return { day: day, disabled: (includedDates && !includedDates.includes(yyyymmdd(day))) || excludedDates.includes(yyyymmdd(day)), }; }); /** * Takes care of selecting a valid date in the month if the selected date is not available in the month */ const useHandleInitialDateSelection = () => { // Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment if (selected instanceof Array) { return; } const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day; const isSelectedDateAvailable = selected ? daysToRenderForTheMonth.some(({ day, disabled }) => { if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true; }) : false; if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) { // If selected date not available in the month, select the first available date of the month props.onChange(firstAvailableDateOfTheMonth); } if (!firstAvailableDateOfTheMonth) { props.onChange(null); } }; useEffect(useHandleInitialDateSelection); return ( <> {daysToRenderForTheMonth.map(({ day, disabled }, idx) => (
{day === null ? (
) : props.isLoading ? ( ) : ( { props.onChange(day); }} disabled={disabled} active={isActive(day)} /> )}
))} {!props.isLoading && includedDates && includedDates?.length === 0 && ( )} ); }; const DatePicker = ({ weekStart = 0, className, locale, selected, onMonthChange, ...passThroughProps }: DatePickerProps & Partial>) => { const browsingDate = passThroughProps.browsingDate || dayjs().startOf("month"); const { i18n } = useLocale(); const changeMonth = (newMonth: number) => { if (onMonthChange) { onMonthChange(browsingDate.add(newMonth, "month")); } }; const month = browsingDate ? new Intl.DateTimeFormat(i18n.language, { month: "long" }).format( new Date(browsingDate.year(), browsingDate.month()) ) : null; return (
{browsingDate ? ( <> {month}{" "} {browsingDate.format("YYYY")} ) : ( )}
{weekdayNames(locale, weekStart, "short").map((weekDay) => (
{weekDay}
))}
changeMonth(+1)} />
); }; export default DatePicker;