diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index 857d06f099..f73239f6ff 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -18,7 +18,7 @@ import { useFlags } from "@calcom/features/flags/hooks"; import { trpc } from "@calcom/trpc/react"; import { MetaProvider } from "@calcom/ui"; -import usePublicPage from "@lib/hooks/usePublicPage"; +import useIsBookingPage from "@lib/hooks/useIsBookingPage"; import type { WithNonceProps } from "@lib/withNonce"; import { useViewerI18n } from "@components/I18nLanguageHandler"; @@ -247,7 +247,7 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) { const AppProviders = (props: AppPropsWithChildren) => { // No need to have intercom on public pages - Good for Page Performance - const isPublicPage = usePublicPage(); + const isBookingPage = useIsBookingPage(); const { pageProps, ...rest } = props; const { _nonce, ...restPageProps } = pageProps; const propsWithoutNonce = { @@ -267,7 +267,7 @@ const AppProviders = (props: AppPropsWithChildren) => { themeBasis={props.pageProps.themeBasis} nonce={props.pageProps.nonce} isThemeSupported={props.Component.isThemeSupported} - isBookingPage={props.Component.isBookingPage} + isBookingPage={props.Component.isBookingPage || isBookingPage} router={props.router}> @@ -281,7 +281,7 @@ const AppProviders = (props: AppPropsWithChildren) => { ); - if (isPublicPage) { + if (isBookingPage) { return RemainingProviders; } diff --git a/apps/web/lib/hooks/useIsBookingPage.ts b/apps/web/lib/hooks/useIsBookingPage.ts new file mode 100644 index 0000000000..9e975171e4 --- /dev/null +++ b/apps/web/lib/hooks/useIsBookingPage.ts @@ -0,0 +1,12 @@ +import { usePathname, useSearchParams } from "next/navigation"; + +export default function useIsBookingPage() { + const pathname = usePathname(); + const isBookingPage = ["/booking", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route)); + + const searchParams = useSearchParams(); + const userParam = searchParams.get("user"); + const teamParam = searchParams.get("team"); + + return !!(isBookingPage || userParam || teamParam); +} diff --git a/apps/web/lib/hooks/usePublicPage.ts b/apps/web/lib/hooks/usePublicPage.ts deleted file mode 100644 index b8b0d6e7ee..0000000000 --- a/apps/web/lib/hooks/usePublicPage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { usePathname } from "next/navigation"; - -export default function usePublicPage() { - const pathname = usePathname(); - const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) => - pathname?.startsWith(route) - ); - return isPublicPage; -} diff --git a/apps/web/playwright/manage-booking-questions.e2e.ts b/apps/web/playwright/manage-booking-questions.e2e.ts index b05a321fb5..0d06a2e9d2 100644 --- a/apps/web/playwright/manage-booking-questions.e2e.ts +++ b/apps/web/playwright/manage-booking-questions.e2e.ts @@ -316,8 +316,6 @@ async function runTestStepsCommonForTeamAndUserEventType( await test.step("Do a reschedule and notice that we can't book without giving a value for rescheduleReason", async () => { const page = previewTabPage; await rescheduleFromTheLinkOnPage({ page }); - // eslint-disable-next-line playwright/no-page-pause - await page.pause(); await expectErrorToBeThereFor({ page, name: "rescheduleReason" }); }); } diff --git a/apps/web/server/lib/ssr.ts b/apps/web/server/lib/ssr.ts index 1afc0c5f62..343d625d38 100644 --- a/apps/web/server/lib/ssr.ts +++ b/apps/web/server/lib/ssr.ts @@ -31,5 +31,7 @@ export async function ssrInit(context: GetServerSidePropsContext) { // Provides a better UX to the users who have already upgraded. await ssr.viewer.teams.hasTeamPlan.prefetch(); + await ssr.viewer.public.session.prefetch(); + return ssr; } diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 11a8855f3f..8c637b36d0 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -21,7 +21,7 @@ import { Away, NotFound } from "./components/Unavailable"; import { extraDaysConfig, fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config"; import { useBookerStore, useInitializeBookerStore } from "./store"; import type { BookerLayout, BookerProps } from "./types"; -import { useEvent } from "./utils/event"; +import { useEvent, useScheduleForEvent } from "./utils/event"; import { validateLayout } from "./utils/layout"; import { getQueryParam } from "./utils/query-param"; import { useBrandColors } from "./utils/use-brand-colors"; @@ -41,6 +41,17 @@ const BookerComponent = ({ isTeamEvent, entity, }: BookerProps) => { + /** + * Prioritize dateSchedule load + * Component will render but use data already fetched from here, and no duplicate requests will be made + * */ + useScheduleForEvent({ + prefetchNextMonth: false, + username, + eventSlug, + month, + duration: undefined, + }); const isMobile = useMediaQuery("(max-width: 768px)"); const isTablet = useMediaQuery("(max-width: 1024px)"); const timeslotsRef = useRef(null); diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts index df8532aa3e..9a266a1e5b 100644 --- a/packages/features/bookings/Booker/utils/event.ts +++ b/packages/features/bookings/Booker/utils/event.ts @@ -27,7 +27,8 @@ export const useEvent = () => { /** * Gets schedule for the current event and current month. - * Gets all values from the booker store. + * Gets all values right away and not the store because it increases network timing, only for the first render. + * We can read from the store if we want to get the latest values. * * Using this hook means you only need to use one hook, instead * of combining multiple conditional hooks. @@ -36,21 +37,35 @@ export const useEvent = () => { * useful when the user is viewing dates near the end of the month, * this way the multi day view will show data of both months. */ -export const useScheduleForEvent = ({ prefetchNextMonth }: { prefetchNextMonth?: boolean } = {}) => { +export const useScheduleForEvent = ({ + prefetchNextMonth, + username, + eventSlug, + eventId, + month, + duration, +}: { + prefetchNextMonth?: boolean; + username?: string | null; + eventSlug?: string | null; + eventId?: number | null; + month?: string | null; + duration?: number | null; +} = {}) => { const { timezone } = useTimePreferences(); const event = useEvent(); - const [username, eventSlug, month, duration] = useBookerStore( + const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore( (state) => [state.username, state.eventSlug, state.month, state.selectedDuration], shallow ); return useSchedule({ - username, - eventSlug, - eventId: event.data?.id, - month, + username: usernameFromStore ?? username, + eventSlug: eventSlugFromStore ?? eventSlug, + eventId: event.data?.id ?? eventId, timezone, prefetchNextMonth, - duration, + 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 6ea3f433bc..898fce6e66 100644 --- a/packages/features/schedules/lib/use-schedule/useSchedule.ts +++ b/packages/features/schedules/lib/use-schedule/useSchedule.ts @@ -29,24 +29,31 @@ export const useSchedule = ({ return trpc.viewer.public.slots.getSchedule.useQuery( { usernameList: getUsernameList(username ?? ""), - eventTypeSlug: eventSlug!, + // Prioritize slug over id, since slug is the first value we get available. + // If we have a slug, we don't need to fetch the id. + // TODO: are queries using eventTypeId faster? Even tho we lost time fetching the id with the slug. + ...(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(), // if `prefetchNextMonth` is true, two months are fetched at once. endTime: (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString(), timeZone: timezone!, - eventTypeId: eventId!, duration: duration ? `${duration}` : undefined, }, { + trpc: { + context: { + skipBatch: true, + }, + }, refetchOnWindowFocus: false, enabled: Boolean(username) && - Boolean(eventSlug) && - (Boolean(eventId) || eventId === 0) && Boolean(month) && - Boolean(timezone), + Boolean(timezone) && + // Should only wait for one or the other, not both. + (Boolean(eventSlug) || Boolean(eventId) || eventId === 0), } ); }; diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 0526413d8f..ea013879c5 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -70,9 +70,46 @@ export const checkIfIsAvailable = ({ }; export async function getEventType(input: TGetScheduleInputSchema) { + const { eventTypeSlug, usernameList } = input; + let eventTypeId = input.eventTypeId; + + if (eventTypeId === undefined && eventTypeSlug && usernameList && usernameList.length === 1) { + // If we only have the slug and usernameList, we need to get the id first + const username = usernameList[0]; + const userId = await getUserIdFromUsername(username); + let teamId; + + if (!userId) { + teamId = await getTeamIdFromSlug(username); + if (!teamId) { + throw new TRPCError({ + message: "User or team not found", + code: "NOT_FOUND", + }); + } + } + + const eventType = await prisma.eventType.findFirst({ + where: { + slug: eventTypeSlug, + ...(teamId ? { teamId } : {}), + ...(userId ? { userId } : {}), + }, + select: { + id: true, + }, + }); + + if (!eventType) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + eventTypeId = eventType.id; + } + const eventType = await prisma.eventType.findUnique({ where: { - id: input.eventTypeId, + id: eventTypeId, }, select: { id: true, @@ -175,7 +212,7 @@ export async function getDynamicEventType(input: TGetScheduleInputSchema) { } export function getRegularOrDynamicEventType(input: TGetScheduleInputSchema) { - const isDynamicBooking = !input.eventTypeId; + const isDynamicBooking = input.usernameList && input.usernameList.length > 1; return isDynamicBooking ? getDynamicEventType(input) : getEventType(input); } @@ -237,7 +274,7 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) { username: currentUser.username || "", dateFrom: startTime.format(), dateTo: endTime.format(), - eventTypeId: input.eventTypeId, + eventTypeId: eventType.id, afterEventBuffer: eventType.afterEventBuffer, beforeEventBuffer: eventType.beforeEventBuffer, duration: input.duration || 0, @@ -446,3 +483,27 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) { slots: computedAvailableSlots, }; } + +async function getUserIdFromUsername(username: string) { + const user = await prisma.user.findFirst({ + where: { + username, + }, + select: { + id: true, + }, + }); + return user?.id; +} + +async function getTeamIdFromSlug(slug: string) { + const team = await prisma.team.findFirst({ + where: { + slug, + }, + select: { + id: true, + }, + }); + return team?.id; +}