diff --git a/.env.example b/.env.example
index 503dee3414..b471e34c2e 100644
--- a/.env.example
+++ b/.env.example
@@ -236,6 +236,8 @@ CSP_POLICY=
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
+NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
+
# Control time intervals on a user's Schedule availability
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index a0f864523c..935ed980b4 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2236,5 +2236,7 @@
"create_entry": "Create entry",
"time_range": "Time range",
"redirect_to": "Redirect to",
+ "having_trouble_finding_time": "Having trouble finding a time?",
+ "show_more": "Show more",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx
index 86b828fda0..13776c5dee 100644
--- a/packages/features/bookings/Booker/Booker.tsx
+++ b/packages/features/bookings/Booker/Booker.tsx
@@ -21,6 +21,7 @@ import { AvailableTimeSlots } from "./components/AvailableTimeSlots";
import { BookEventForm } from "./components/BookEventForm";
import { BookFormAsModal } from "./components/BookEventForm/BookFormAsModal";
import { EventMeta } from "./components/EventMeta";
+import { HavingTroubleFindingTime } from "./components/HavingTroubleFindingTime";
import { Header } from "./components/Header";
import { InstantBooking } from "./components/InstantBooking";
import { LargeCalendar } from "./components/LargeCalendar";
@@ -86,6 +87,9 @@ const BookerComponent = ({
isEmbed,
bookerLayouts,
} = useBookerLayout(event.data);
+
+ const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
+
const date = dayjs(selectedDate).format("YYYY-MM-DD");
const prefetchNextMonth =
@@ -102,6 +106,8 @@ const BookerComponent = ({
? 2
: undefined;
+ const searchParams = useSearchParams();
+
/**
* Prioritize dateSchedule load
* Component will render but use data already fetched from here, and no duplicate requests will be made
@@ -110,9 +116,11 @@ const BookerComponent = ({
prefetchNextMonth,
username,
monthCount,
+ dayCount,
eventSlug,
month,
duration,
+ selectedDate: searchParams?.get("date"),
});
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
@@ -130,6 +138,7 @@ const BookerComponent = ({
if (nonEmptyScheduleDays.length !== 0)
columnViewExtraDays.current =
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 2], "day")) + addonDays;
+
const nextSlots =
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 1], "day")) + addonDays;
@@ -144,7 +153,6 @@ const BookerComponent = ({
const { t } = useLocale();
- const searchParams = useSearchParams();
const isRedirect = searchParams?.get("redirected") === "true" || false;
const fromUserNameRedirected = searchParams?.get("username") || "";
@@ -427,7 +435,16 @@ const BookerComponent = ({
{...fadeInLeft}
initial="visible"
className="md:border-subtle ml-[-1px] h-full flex-shrink px-5 py-3 md:border-l lg:w-[var(--booker-main-width)]">
-
+
+ {
+ setDayCount(null);
+ }}
+ />
+
+
{
const { i18n } = useLocale();
const [month, selectedDate] = useBookerStore((state) => [state.month, state.selectedDate], shallow);
- const [setSelectedDate, setMonth] = useBookerStore(
- (state) => [state.setSelectedDate, state.setMonth],
+ const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
+ (state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
shallow
);
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
@@ -34,6 +34,7 @@ export const DatePicker = ({
onMonthChange={(date: Dayjs) => {
setMonth(date.format("YYYY-MM"));
setSelectedDate(date.format("YYYY-MM-DD"));
+ setDayCount(null); // Whenever the month is changed, we nullify getting X days
}}
includedDates={nonEmptyScheduleDays}
locale={i18n.language}
diff --git a/packages/features/bookings/Booker/components/HavingTroubleFindingTime.tsx b/packages/features/bookings/Booker/components/HavingTroubleFindingTime.tsx
new file mode 100644
index 0000000000..4f1f46ce24
--- /dev/null
+++ b/packages/features/bookings/Booker/components/HavingTroubleFindingTime.tsx
@@ -0,0 +1,45 @@
+import { ArrowRight, InfoIcon } from "lucide-react";
+import { useState } from "react";
+
+import { BOOKER_NUMBER_OF_DAYS_TO_LOAD } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
+type Props = {
+ onButtonClick: () => void;
+ dayCount: number | null;
+ isScheduleLoading: boolean;
+};
+export function HavingTroubleFindingTime(props: Props) {
+ const { t } = useLocale();
+ const [internalClick, setInternalClick] = useState(false);
+
+ // Easiest way to detect if its not enabled
+ if (
+ (process.env.NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD == "0" && BOOKER_NUMBER_OF_DAYS_TO_LOAD == 0) ||
+ !process.env.NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD
+ )
+ return null;
+
+ // If we have clicked this internally - and the schedule above is not loading - hide this banner as there is no use of being able to go backwards
+ if (internalClick && !props.isScheduleLoading) return null;
+ if (props.isScheduleLoading || !props.dayCount) return null;
+
+ return (
+
+
+
+
{t("having_trouble_finding_time")}
+
+ {/* TODO: we should give this more of a touch target on mobile */}
+
+
+ );
+}
diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts
index da950c23fa..eb050d1166 100644
--- a/packages/features/bookings/Booker/store.ts
+++ b/packages/features/bookings/Booker/store.ts
@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { create } from "zustand";
import dayjs from "@calcom/dayjs";
+import { BOOKER_NUMBER_OF_DAYS_TO_LOAD } from "@calcom/lib/constants";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "../lib/get-booking";
@@ -104,6 +105,11 @@ export type BookerStore = {
*/
occurenceCount: number | null;
setOccurenceCount(count: number | null): void;
+ /**
+ * The number of days worth of schedules to load.
+ */
+ dayCount: number | null;
+ setDayCount: (dayCount: number | null) => void;
/**
* If booking is being rescheduled or it has seats, it receives a rescheduleUid or bookingUid
* the current booking details are passed in. The `bookingData`
@@ -205,6 +211,10 @@ export const useBookerStore = create((set, get) => ({
updateQueryParam("month", month ?? "");
get().setSelectedDate(null);
},
+ dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
+ setDayCount: (dayCount: number | null) => {
+ set({ dayCount });
+ },
isTeamEvent: false,
seatedEventData: {
seatsPerTimeSlot: undefined,
diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts
index b88645fdbd..e963b2eebd 100644
--- a/packages/features/bookings/Booker/utils/event.ts
+++ b/packages/features/bookings/Booker/utils/event.ts
@@ -50,6 +50,8 @@ export const useScheduleForEvent = ({
month,
duration,
monthCount,
+ dayCount,
+ selectedDate,
}: {
prefetchNextMonth?: boolean;
username?: string | null;
@@ -58,6 +60,8 @@ export const useScheduleForEvent = ({
month?: string | null;
duration?: number | null;
monthCount?: number;
+ dayCount?: number | null;
+ selectedDate?: string | null;
} = {}) => {
const { timezone } = useTimePreferences();
const event = useEvent();
@@ -77,8 +81,10 @@ export const useScheduleForEvent = ({
eventSlug: eventSlugFromStore ?? eventSlug,
eventId: event.data?.id ?? eventId,
timezone,
+ selectedDate,
prefetchNextMonth,
monthCount,
+ dayCount,
rescheduleUid,
month: monthFromStore ?? month,
duration: durationFromStore ?? duration,
diff --git a/packages/features/schedules/lib/use-schedule/useSchedule.ts b/packages/features/schedules/lib/use-schedule/useSchedule.ts
index 8d6d28327c..6f8f502f44 100644
--- a/packages/features/schedules/lib/use-schedule/useSchedule.ts
+++ b/packages/features/schedules/lib/use-schedule/useSchedule.ts
@@ -8,9 +8,11 @@ type UseScheduleWithCacheArgs = {
eventId?: number | null;
month?: string | null;
timezone?: string | null;
+ selectedDate?: string | null;
prefetchNextMonth?: boolean;
duration?: number | null;
monthCount?: number | null;
+ dayCount?: number | null;
rescheduleUid?: string | null;
isTeamEvent?: boolean;
};
@@ -21,17 +23,39 @@ export const useSchedule = ({
username,
eventSlug,
eventId,
+ selectedDate,
prefetchNextMonth,
duration,
monthCount,
+ dayCount,
rescheduleUid,
isTeamEvent,
}: UseScheduleWithCacheArgs) => {
- const monthDayjs = month ? dayjs(month) : dayjs();
+ const now = dayjs();
+ const monthDayjs = month ? dayjs(month) : now;
const nextMonthDayjs = monthDayjs.add(monthCount ? monthCount : 1, "month");
// Why the non-null assertions? All of these arguments are checked in the enabled condition,
// and the query will not run if they are null. However, the check in `enabled` does
// no satisfy typescript.
+ let startTime;
+ let endTime;
+
+ if (!!dayCount && dayCount > 0) {
+ if (selectedDate) {
+ startTime = dayjs(selectedDate).toISOString();
+ endTime = dayjs(selectedDate).add(dayCount, "day").toISOString();
+ } else if (monthDayjs.month() === now.month()) {
+ startTime = now.startOf("day").toISOString();
+ endTime = now.startOf("day").add(dayCount, "day").toISOString();
+ } else {
+ startTime = monthDayjs.startOf("month").toISOString();
+ endTime = monthDayjs.startOf("month").add(dayCount, "day").toISOString();
+ }
+ } else {
+ startTime = monthDayjs.startOf("month").toISOString();
+ endTime = (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString();
+ }
+
return trpc.viewer.public.slots.getSchedule.useQuery(
{
isTeamEvent,
@@ -42,9 +66,9 @@ export const useSchedule = ({
...(eventSlug ? { eventTypeSlug: eventSlug } : { eventTypeId: eventId ?? 0 }),
// @TODO: Old code fetched 2 days ago if we were fetching the current month.
// Do we want / need to keep that behavior?
- startTime: monthDayjs.startOf("month").toISOString(),
+ startTime,
// if `prefetchNextMonth` is true, two months are fetched at once.
- endTime: (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString(),
+ endTime,
timeZone: timezone!,
duration: duration ? `${duration}` : undefined,
rescheduleUid,
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index 4e99d20fd0..e6087bcc33 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -128,3 +128,8 @@ export const MAX_NB_INVITES = 100;
export const URL_PROTOCOL_REGEX = /(^\w+:|^)\/\//;
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
+
+export const BOOKER_NUMBER_OF_DAYS_TO_LOAD = parseInt(
+ process.env.NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD ?? "0",
+ 0
+);
diff --git a/turbo.json b/turbo.json
index 01a0388131..5cdcb2c360 100644
--- a/turbo.json
+++ b/turbo.json
@@ -26,6 +26,7 @@
"outputs": [".next/**"],
"env": [
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL",
+ "NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD",
"NEXT_PUBLIC_IS_E2E",
"NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY",