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 { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
import type { Slots } from "@calcom/features/calendars/lib/types";
import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import type { PeriodData } from "@calcom/types/Event";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { Tooltip } from "@calcom/ui/components/tooltip";
import NoAvailabilityDialog from "./NoAvailabilityDialog";
import { useSlotsViewOnSmallScreen } from "@calcom/embed-core/embed-iframe";
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, omitUpdatingParams?: boolean) => 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?: Date;
/** Furthest date selectable in the future, default = UNLIMITED */
maxDate?: Date;
/** 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;
/** To identify days that are not available and should display OOO and redirect if toUser exists */
slots?: Slots;
periodData?: PeriodData;
// Whether this is a compact sidebar view or main monthly view
isCompact?: boolean;
// Whether to show the no availability dialog
showNoAvailabilityDialog?: boolean;
};
const Day = ({
date,
active,
disabled,
away,
emoji,
customClassName,
showMonthTooltip,
isFirstDayOfNextMonth,
...props
}: JSX.IntrinsicElements["button"] & {
active: boolean;
date: Dayjs;
away?: boolean;
emoji?: string | null;
customClassName?: {
dayContainer?: string;
dayActive?: string;
};
showMonthTooltip?: boolean;
isFirstDayOfNextMonth?: boolean;
}) => {
const { t } = useLocale();
const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton");
const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton");
const buttonContent = (
);
const content = showMonthTooltip ? (
{buttonContent}
) : (
buttonContent
);
return (
<>
{isFirstDayOfNextMonth && (
{date.format("MMM")}
)}
{content}
>
);
};
const Days = ({
minDate,
excludedDates = [],
browsingDate,
weekStart,
DayComponent = Day,
selected,
month,
nextMonthButton,
eventSlug,
slots,
customClassName,
isBookingInPast,
periodData,
isCompact,
showNoAvailabilityDialog = true,
...props
}: Omit & {
DayComponent?: React.FC>;
browsingDate: Dayjs;
weekStart: number;
month: string | null;
nextMonthButton: () => void;
customClassName?: {
datePickerDate?: string;
datePickerDateActive?: string;
};
scrollToTimeSlots?: () => void;
isBookingInPast: boolean;
periodData: PeriodData;
isCompact?: boolean;
}) => {
const slotsViewOnSmallScreen = useSlotsViewOnSmallScreen();
const layout = useBookerStoreContext((state) => state.layout);
const isMobile = layout === "mobile";
const includedDates = getAvailableDatesInMonth({
browsingDate: browsingDate.toDate(),
minDate,
includedDates: props.includedDates,
});
const today = dayjs();
const firstDayOfMonth = browsingDate.startOf("month");
const isSecondWeekOver = today.isAfter(firstDayOfMonth.add(2, "week"));
let days: (Dayjs | null)[] = [];
const getPadding = (day: number) => (browsingDate.set("date", day).day() - weekStart + 7) % 7;
const totalDays = daysInMonth(browsingDate);
const showNextMonthDays = isSecondWeekOver && !isCompact;
// Only apply end-of-month logic for main monthly view (not compact sidebar)
if (showNextMonthDays) {
const startDay = 8;
const pad = getPadding(startDay);
days = Array(pad).fill(null);
for (let day = startDay; day <= totalDays; day++) {
days.push(browsingDate.set("date", day));
}
const remainingInRow = days.length % 7;
const extraDays = (remainingInRow > 0 ? 7 - remainingInRow : 0) + 7;
const nextMonth = browsingDate.add(1, "month");
// Add days starting from day 1 of next month
for (let i = 0; i < extraDays; i++) {
days.push(nextMonth.set("date", 1 + i));
}
} else {
// Traditional calendar grid logic for compact sidebar or early in month
const pad = getPadding(1);
days = Array(pad).fill(null);
for (let day = 1; day <= totalDays; day++) {
days.push(browsingDate.set("date", day));
}
}
const [selectedDatesAndTimes] = useBookerStoreContext((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 };
const dateKey = yyyymmdd(day);
const daySlots = slots?.[dateKey] || [];
const oooInfo = daySlots.find((slot) => slot.away) || null;
const isNextMonth = day.month() !== browsingDate.month();
const isFirstDayOfNextMonth = isSecondWeekOver && !isCompact && isNextMonth && day.date() === 1;
const included = includedDates?.includes(dateKey);
const excluded = excludedDates.includes(dateKey);
const hasAvailableSlots = daySlots.some((slot) => !slot.away);
const isOOOAllDay = daySlots.length > 0 && daySlots.every((slot) => slot.away);
const away = isOOOAllDay;
// OOO dates are selectable only if there's a redirect user OR the note is public
const oooIsSelectable = oooInfo?.toUser || oooInfo?.showNotePublicly;
const disabled = away ? !oooIsSelectable : isNextMonth ? !hasAvailableSlots : !included || excluded;
return {
day,
disabled,
away,
emoji: oooInfo?.emoji,
isFirstDayOfNextMonth,
};
});
/**
* Takes care of selecting a valid date in the month if the selected date is not available in the month
*/
const useHandleInitialDateSelection = () => {
// Don't auto-select date when slots view on small screen is enabled on mobile
if (slotsViewOnSmallScreen) {
return;
}
// 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
const shouldOmitUpdatingParams = selected?.isValid() ? false : true; // In case a date is selected and it is not available, then we have to change search params
props.onChange(firstAvailableDateOfTheMonth, shouldOmitUpdatingParams);
}
if (isSelectedDateAvailable) {
props.onChange(dayjs(selected), true);
}
if (!firstAvailableDateOfTheMonth) {
props.onChange(null);
}
};
useEffect(useHandleInitialDateSelection);
return (
<>
{daysToRenderForTheMonth.map(({ day, disabled, away, emoji, isFirstDayOfNextMonth }, idx) => (