import { LazyMotion, m, AnimatePresence } from "framer-motion"; import dynamic from "next/dynamic"; import { useEffect, useRef } from "react"; import StickyBox from "react-sticky-box"; import { shallow } from "zustand/shallow"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import dayjs from "@calcom/dayjs"; import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import classNames from "@calcom/lib/classNames"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils"; import { AvailableTimeSlots } from "./components/AvailableTimeSlots"; import { BookEventForm } from "./components/BookEventForm"; import { BookFormAsModal } from "./components/BookEventForm/BookFormAsModal"; import { EventMeta } from "./components/EventMeta"; import { Header } from "./components/Header"; import { LargeCalendar } from "./components/LargeCalendar"; import { BookerSection } from "./components/Section"; 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, useScheduleForEvent } from "./utils/event"; import { validateLayout } from "./utils/layout"; import { getQueryParam } from "./utils/query-param"; import { useBrandColors } from "./utils/use-brand-colors"; const loadFramerFeatures = () => import("./framer-features").then((res) => res.default); const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy")); const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity)); const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), { ssr: false, }); const BookerComponent = ({ username, eventSlug, month, bookingData, hideBranding = false, isTeamEvent, entity, duration, hashedLink, }: BookerProps) => { /** * Prioritize dateSchedule load * Component will render but use data already fetched from here, and no duplicate requests will be made * */ const schedule = useScheduleForEvent({ prefetchNextMonth: false, username, eventSlug, month, duration, }); const isMobile = useMediaQuery("(max-width: 768px)"); const isTablet = useMediaQuery("(max-width: 1024px)"); const timeslotsRef = useRef(null); const StickyOnDesktop = isMobile ? "div" : StickyBox; const rescheduleUid = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null; const bookingUid = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null; const event = useEvent(); const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const isEmbed = useIsEmbed(); const embedType = useEmbedType(); // Floating Button and Element Click both are modal and thus have dark background const hasDarkBackground = isEmbed && embedType !== "inline"; const embedUiConfig = useEmbedUiConfig(); // In Embed we give preference to embed configuration for the layout.If that's not set, we use the App configuration for the event layout // But if it's mobile view, there is only one layout supported which is 'mobile' const layout = isEmbed ? (isMobile ? "mobile" : validateLayout(embedUiConfig.layout) || _layout) : _layout; const columnViewExtraDays = useRef( isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop ); const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); const selectedDate = useBookerStore((state) => state.selectedDate); const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( (state) => [state.selectedTimeslot, state.setSelectedTimeslot], shallow ); // const seatedEventData = useBookerStore((state) => state.seatedEventData); const [seatedEventData, setSeatedEventData] = useBookerStore( (state) => [state.seatedEventData, state.setSeatedEventData], shallow ); const date = dayjs(selectedDate).format("YYYY-MM-DD"); const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter( (slot) => dayjs(selectedDate).diff(slot, "day") <= 0 ); const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop; const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings; const animationScope = useBookerResizeAnimation(layout, bookerState); const totalWeekDays = 7; const addonDays = nonEmptyScheduleDays.length < extraDays ? (extraDays - nonEmptyScheduleDays.length + 1) * totalWeekDays : nonEmptyScheduleDays.length === extraDays ? totalWeekDays : 0; // Taking one more available slot(extraDays + 1) to calculate the no of days in between, that next and prev button need to shift. const availableSlots = nonEmptyScheduleDays.slice(0, extraDays + 1); if (nonEmptyScheduleDays.length !== 0) columnViewExtraDays.current = Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 2], "day")) + addonDays; const prefetchNextMonth = layout === BookerLayouts.COLUMN_VIEW && dayjs(date).month() !== dayjs(date).add(columnViewExtraDays.current, "day").month(); const monthCount = dayjs(date).add(1, "month").month() !== dayjs(date).add(columnViewExtraDays.current, "day").month() ? 2 : undefined; const nextSlots = Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 1], "day")) + addonDays; // I would expect isEmbed to be not needed here as it's handled in derived variable layout, but somehow removing it breaks the views. const defaultLayout = isEmbed ? validateLayout(embedUiConfig.layout) || bookerLayouts.defaultLayout : bookerLayouts.defaultLayout; useBrandColors({ brandColor: event.data?.profile.brandColor, darkBrandColor: event.data?.profile.darkBrandColor, theme: event.data?.profile.theme, }); useInitializeBookerStore({ username, eventSlug, month, eventId: event?.data?.id, rescheduleUid, bookingUid, bookingData, layout: defaultLayout, isTeamEvent, org: entity.orgSlug, durationConfig: event?.data?.metadata?.multipleDuration, }); useEffect(() => { if (isMobile && layout !== "mobile") { setLayout("mobile"); } else if (!isMobile && layout === "mobile") { setLayout(defaultLayout); } }, [isMobile, setLayout, layout, defaultLayout]); //setting layout from query param useEffect(() => { const layout = getQueryParam("layout") as BookerLayouts; if ( !isMobile && !isEmbed && validateLayout(layout) && bookerLayouts?.enabledLayouts?.length && layout !== _layout ) { const validLayout = bookerLayouts.enabledLayouts.find((userLayout) => userLayout === layout); validLayout && setLayout(validLayout); } }, [bookerLayouts, validateLayout, setLayout, _layout]); useEffect(() => { if (event.isLoading) return setBookerState("loading"); if (!selectedDate) return setBookerState("selecting_date"); if (!selectedTimeslot) return setBookerState("selecting_time"); return setBookerState("booking"); }, [event, selectedDate, selectedTimeslot, setBookerState]); useEffect(() => { if (layout === "mobile") { timeslotsRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [layout]); const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false; if (entity.isUnpublished) { return ; } if (event.isSuccess && !event.data) { return ; } // In Embed, a Dialog doesn't look good, we disable it intentionally for the layouts that support showing Form without Dialog(i.e. no-dialog Form) const shouldShowFormInDialogMap: Record = { // mobile supports showing the Form without Dialog mobile: !isEmbed, // We don't show Dialog in month_view currently. Can be easily toggled though as it supports no-dialog Form month_view: false, // week_view doesn't support no-dialog Form // When it's supported, disable it for embed week_view: true, // column_view doesn't support no-dialog Form // When it's supported, disable it for embed column_view: true, }; const shouldShowFormInDialog = shouldShowFormInDialogMap[layout]; if (bookerState === "loading") { return null; } return ( <> {event.data ? : null}
{layout !== BookerLayouts.MONTH_VIEW && !(layout === "mobile" && bookerState === "booking") && (
)}
{ setSelectedTimeslot(null); if (seatedEventData.bookingUid) { setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined }); } }} hashedLink={hashedLink} />
{!hideBranding ? : null}
setSelectedTimeslot(null)} /> ); }; export const Booker = (props: BookerProps) => { if (props.isAway) return ; return ( ); };