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",