diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx
index e36281c2c8..f54395e553 100644
--- a/packages/features/bookings/Booker/Booker.tsx
+++ b/packages/features/bookings/Booker/Booker.tsx
@@ -410,7 +410,12 @@ const BookerComponent = ({
{layout !== BookerLayouts.MONTH_VIEW &&
!(layout === "mobile" && bookerState === "booking") && (
-
+
)}
@@ -442,7 +447,8 @@ const BookerComponent = ({
datePickerToggle: customClassNames?.datePickerCustomClassNames?.datePickerToggle,
}}
event={event}
- schedule={schedule}
+ slots={schedule?.data?.slots}
+ isLoading={schedule.isPending}
scrollToTimeSlots={scrollToTimeSlots}
/>
diff --git a/packages/features/bookings/Booker/components/DatePicker.test.tsx b/packages/features/bookings/Booker/components/DatePicker.test.tsx
new file mode 100644
index 0000000000..26c645dee3
--- /dev/null
+++ b/packages/features/bookings/Booker/components/DatePicker.test.tsx
@@ -0,0 +1,108 @@
+import { render } from "@testing-library/react";
+import { vi, afterEach } from "vitest";
+
+import dayjs from "@calcom/dayjs";
+import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
+
+import { DatePicker } from "./DatePicker";
+
+vi.mock("@calcom/features/calendars/DatePicker", () => {
+ return {
+ DatePicker: vi.fn(() => ),
+ };
+});
+
+const noop = () => {
+ /* noop */
+};
+
+describe("Tests for DatePicker Component", () => {
+ afterEach(() => {
+ DatePickerComponent.mockClear();
+ });
+
+ test("It passes the loading prop to the DatePicker component", async () => {
+ render();
+
+ expect(DatePickerComponent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ isLoading: true,
+ }),
+ expect.anything()
+ );
+ });
+
+ test("happy path, slots in current month", async () => {
+ // today current time.
+ const testDate = dayjs();
+ render(
+
+ );
+ // slot date is next month, so it'll have skipped the current month
+ // by setting the browsingDate to the next month.
+ expect(DatePickerComponent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ browsingDate: dayjs().startOf("month"),
+ }),
+ expect.anything()
+ );
+ });
+
+ test("when there are only slots in the next month, skip the current month", async () => {
+ // there'll be one slot open on this day, next month.
+ const slotDate = dayjs().add("1", "month");
+ render(
+
+ );
+ // slot date is next month, so it'll have skipped the current month
+ // by setting the browsingDate to the next month.
+ expect(DatePickerComponent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ browsingDate: slotDate.startOf("month"),
+ }),
+ expect.anything()
+ );
+ });
+
+ test("when there are no slots, check for infinite loop (skip only 1 month)", async () => {
+ render(
+
+ );
+ // slot date is next month, so it'll have skipped the current month
+ // by setting the browsingDate to the next month.
+ expect(DatePickerComponent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ browsingDate: dayjs().add("1", "month").startOf("month"),
+ }),
+ expect.anything()
+ );
+ });
+});
diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx
index 4eaeac0c2d..3317f1c75d 100644
--- a/packages/features/bookings/Booker/components/DatePicker.tsx
+++ b/packages/features/bookings/Booker/components/DatePicker.tsx
@@ -2,25 +2,65 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
-import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
+import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import { weekdayToWeekIndex } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { User } from "@calcom/prisma/client";
+import type { Slots } from "../../types";
import { useBookerStore } from "../store";
-import type { useScheduleForEventReturnType } from "../utils/event";
+
+const useMoveToNextMonthOnNoAvailability = ({
+ browsingDate,
+ nonEmptyScheduleDays,
+ onMonthChange,
+ isLoading,
+}: {
+ browsingDate: Dayjs;
+ nonEmptyScheduleDays: string[];
+ isLoading: boolean;
+ onMonthChange: (date: Dayjs) => void;
+}) => {
+ if (isLoading) {
+ return {
+ moveToNextMonthOnNoAvailability: () => {
+ /* return noop until ready */
+ },
+ };
+ }
+
+ const nonEmptyScheduleDaysInBrowsingMonth = nonEmptyScheduleDays.filter((date) =>
+ dayjs(date).isSame(browsingDate, "month")
+ );
+
+ const moveToNextMonthOnNoAvailability = () => {
+ const currentMonth = dayjs().startOf("month").format("YYYY-MM");
+ const browsingMonth = browsingDate.format("YYYY-MM");
+ // Not meeting the criteria to move to next month
+ // Has to be currentMonth and it must have all days unbookable
+ if (currentMonth != browsingMonth || nonEmptyScheduleDaysInBrowsingMonth.length) {
+ return;
+ }
+ onMonthChange(browsingDate.add(1, "month"));
+ };
+ return {
+ moveToNextMonthOnNoAvailability,
+ };
+};
export const DatePicker = ({
event,
- schedule,
+ slots = {},
+ isLoading,
classNames,
scrollToTimeSlots,
}: {
event: {
data?: { subsetOfUsers: Pick[] } | null;
};
- schedule: useScheduleForEventReturnType;
+ slots?: Slots;
+ isLoading?: boolean;
classNames?: {
datePickerContainer?: string;
datePickerTitle?: string;
@@ -33,12 +73,11 @@ export const DatePicker = ({
}) => {
const { i18n } = useLocale();
const [month, selectedDate] = useBookerStore((state) => [state.month, state.selectedDate], shallow);
+
const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
shallow
);
- const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
- const browsingDate = month ? dayjs(month) : dayjs().startOf("month");
const onMonthChange = (date: Dayjs) => {
setMonth(date.format("YYYY-MM"));
@@ -46,26 +85,16 @@ export const DatePicker = ({
setDayCount(null); // Whenever the month is changed, we nullify getting X days
};
- const moveToNextMonthOnNoAvailability = () => {
- const currentMonth = dayjs().startOf("month").format("YYYY-MM");
- const browsingMonth = browsingDate.format("YYYY-MM");
-
- // Insufficient data case
- if (!schedule?.data?.slots) {
- return;
- }
-
- // Not meeting the criteria to move to next month
- // Has to be currentMonth and it must have all days unbookable
- if (currentMonth != browsingMonth || nonEmptyScheduleDays.length) {
- return;
- }
-
- onMonthChange(browsingDate.add(1, "month"));
- };
+ const nonEmptyScheduleDays = useNonEmptyScheduleDays(slots);
+ const browsingDate = month ? dayjs(month) : dayjs().startOf("month");
+ const { moveToNextMonthOnNoAvailability } = useMoveToNextMonthOnNoAvailability({
+ browsingDate,
+ nonEmptyScheduleDays,
+ onMonthChange,
+ isLoading: isLoading ?? true,
+ });
moveToNextMonthOnNoAvailability();
-
return (
{
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"), omitUpdatingParams);
}}
@@ -86,7 +115,7 @@ export const DatePicker = ({
browsingDate={month ? dayjs(month) : undefined}
selected={dayjs(selectedDate)}
weekStart={weekdayToWeekIndex(event?.data?.subsetOfUsers?.[0]?.weekStart)}
- slots={schedule?.data?.slots}
+ slots={slots}
scrollToTimeSlots={scrollToTimeSlots}
/>
);
diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts
index 584a9f9640..9bc1603ce2 100644
--- a/packages/features/bookings/Booker/store.ts
+++ b/packages/features/bookings/Booker/store.ts
@@ -1,7 +1,7 @@
"use client";
import { useEffect } from "react";
-import { create } from "zustand";
+import { createWithEqualityFn } from "zustand/traditional";
import dayjs from "@calcom/dayjs";
import { BOOKER_NUMBER_OF_DAYS_TO_LOAD } from "@calcom/lib/constants";
@@ -174,7 +174,7 @@ export type BookerStore = {
*
* See comments in interface above for more information on it's specific values.
*/
-export const useBookerStore = create((set, get) => ({
+export const useBookerStore = createWithEqualityFn((set, get) => ({
state: "loading",
setState: (state: BookerState) => set({ state }),
layout: BookerLayouts.MONTH_VIEW,
diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx
index df800b5753..0eee6a9e86 100644
--- a/packages/features/bookings/components/AvailableTimes.tsx
+++ b/packages/features/bookings/components/AvailableTimes.tsx
@@ -13,16 +13,16 @@ import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
import type { IOutOfOfficeData } from "@calcom/lib/getUserAvailability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
-import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
+import classNames from "@calcom/ui/classNames";
+import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton";
-import { Button } from "@calcom/ui/components/button";
-import classNames from "@calcom/ui/classNames";
import { useBookerTime } from "../Booker/components/hooks/useBookerTime";
import { useBookerStore } from "../Booker/store";
import { getQueryParam } from "../Booker/utils/query-param";
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
+import type { Slots } from "../types";
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";
type TOnTimeSelect = (
@@ -45,7 +45,7 @@ type TOnTentativeTimeSelect = ({
}) => void;
export type AvailableTimesProps = {
- slots: IGetAvailableSlots["slots"][string];
+ slots: Slots[string];
showTimeFormatToggle?: boolean;
className?: string;
// It is called when a slot is selected, but it is not a confirmation and a confirm button will be shown besides it.
diff --git a/packages/features/bookings/types.ts b/packages/features/bookings/types.ts
index 783f01810c..c6bbde38de 100644
--- a/packages/features/bookings/types.ts
+++ b/packages/features/bookings/types.ts
@@ -5,6 +5,8 @@ import type { BookingCreateBody } from "@calcom/prisma/zod/custom/booking";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppsStatus } from "@calcom/types/Calendar";
+import type { DatePickerProps } from "../calendars/DatePicker";
+
export type PublicEvent = NonNullable;
export type BookerEventQuery = {
@@ -26,6 +28,9 @@ type BookerEventUser = Pick<
type BookerEventProfile = Pick;
+// marked as required to keep responsibility on consumers to handle the case where slots is undefined
+export type Slots = Required>;
+
export type BookerEvent = Pick<
PublicEvent,
| "id"
diff --git a/packages/features/calendars/DatePicker.test.tsx b/packages/features/calendars/DatePicker.test.tsx
new file mode 100644
index 0000000000..07a1cffdfc
--- /dev/null
+++ b/packages/features/calendars/DatePicker.test.tsx
@@ -0,0 +1,41 @@
+import { render } from "@testing-library/react";
+
+import dayjs from "@calcom/dayjs";
+
+import { DatePicker } from "./DatePicker";
+
+const noop = () => {
+ /* noop */
+};
+
+describe("Tests for DatePicker Component", () => {
+ test("Should render correctly with default date", async () => {
+ const testDate = dayjs("2024-02-20");
+ const { getByTestId } = render();
+
+ const selectedMonthLabel = getByTestId("selected-month-label");
+ await expect(selectedMonthLabel).toHaveAttribute("dateTime", testDate.format("YYYY-MM"));
+ });
+
+ test("Should render with the minimum date if browsingDate < minDate", async () => {
+ const testDate = dayjs("2024-02-20");
+ const minDate = dayjs("2025-02-10");
+ const { getByTestId } = render(
+
+ );
+
+ const selectedMonthLabel = getByTestId("selected-month-label");
+ await expect(selectedMonthLabel).toHaveAttribute("dateTime", minDate.format("YYYY-MM"));
+ });
+
+ test("Should render with the browsingDate date if browsingDate >= minDate", async () => {
+ const testDate = dayjs("2025-03-20");
+ const minDate = dayjs("2025-02-10");
+ const { getByTestId } = render(
+
+ );
+
+ const selectedMonthLabel = getByTestId("selected-month-label");
+ await expect(selectedMonthLabel).toHaveAttribute("dateTime", testDate.format("YYYY-MM"));
+ });
+});
diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx
index 3128016841..7173b0a369 100644
--- a/packages/features/calendars/DatePicker.tsx
+++ b/packages/features/calendars/DatePicker.tsx
@@ -10,9 +10,9 @@ import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
-import { SkeletonText } from "@calcom/ui/components/skeleton";
-import { Button } from "@calcom/ui/components/button";
import classNames from "@calcom/ui/classNames";
+import { Button } from "@calcom/ui/components/button";
+import { SkeletonText } from "@calcom/ui/components/skeleton";
export type DatePickerProps = {
/** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */
@@ -36,7 +36,7 @@ export type DatePickerProps = {
/** allows adding classes to the container */
className?: string;
/** Shows a small loading spinner next to the month name */
- isPending?: boolean;
+ 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 */
@@ -54,7 +54,7 @@ export type DatePickerProps = {
>;
};
-export const Day = ({
+const Day = ({
date,
active,
disabled,
@@ -256,7 +256,7 @@ const Days = ({
{day === null ? (
- ) : props.isPending ? (
+ ) : props.isLoading ? (