feat: Limit the amount of days to load for a schedule (#13282)
* feat: Disallow next month prefetching in booker * renamed to include NEXT_PUBLIC * fixed boolean expression * Now using a variable length of days to load * Type fixes * Now opening all dates to be clickable * Added turbo.json var and updated example env * Fixed issue with selectedDate being null when first loading the page * Updated description of variable * Made constant for the booking days * Type fix * Setting the days to load for weekly view to 7 * feat: add ui and ability to fetch more on click * feat: add ui and ability to fetch more on click * fix: loader * fix: banner detection * Update packages/features/bookings/Booker/Booker.tsx * fix: types * Not showing HavingTroubleFindingTime component when the schedule is loading * Only enabled on monthly view * Correctly load initial date * Type fix * Fixed bugs around triggering multiple requests * Added case for when there is no selectedDate but the month has been specified * Types * More types * Check for undefined * More typing * More typing --------- Co-authored-by: sean-brydon <sean@cal.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
co-authored by
sean-brydon
sean-brydon
parent
ebe6a60366
commit
e4037be79a
@@ -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=
|
||||
|
||||
|
||||
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -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)]">
|
||||
<DatePicker event={event} schedule={schedule} />
|
||||
<div className="relative">
|
||||
<HavingTroubleFindingTime
|
||||
dayCount={dayCount}
|
||||
isScheduleLoading={schedule.isLoading}
|
||||
onButtonClick={() => {
|
||||
setDayCount(null);
|
||||
}}
|
||||
/>
|
||||
<DatePicker event={event} schedule={schedule} />
|
||||
</div>
|
||||
</BookerSection>
|
||||
|
||||
<BookerSection
|
||||
|
||||
@@ -19,8 +19,8 @@ export const DatePicker = ({
|
||||
}) => {
|
||||
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}
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-default border-subtle absolute bottom-[-120px] flex w-full min-w-0 items-center justify-between rounded-[32px] border p-3 text-sm leading-none shadow-sm">
|
||||
<div className="flex items-center gap-2 overflow-x-hidden">
|
||||
<InfoIcon className="text-default h-4 w-4" />
|
||||
<p className="w-full leading-none">{t("having_trouble_finding_time")}</p>
|
||||
</div>
|
||||
{/* TODO: we should give this more of a touch target on mobile */}
|
||||
<button
|
||||
className="inline-flex items-center gap-2 font-medium"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onButtonClick();
|
||||
setInternalClick(true);
|
||||
}}>
|
||||
{t("show_more")} <ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<BookerStore>((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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user