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:
Keith Williams
2024-01-25 14:48:46 -03:00
committed by GitHub
co-authored by sean-brydon sean-brydon
parent ebe6a60366
commit e4037be79a
10 changed files with 120 additions and 7 deletions
+2
View File
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+19 -2
View File
@@ -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,
+5
View File
@@ -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
);
+1
View File
@@ -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",