// Get router variables import autoAnimate from "@formkit/auto-animate"; import { EventType } from "@prisma/client"; import * as Popover from "@radix-ui/react-popover"; import { TFunction } from "next-i18next"; import { useRouter } from "next/router"; import { useReducer, useEffect, useMemo, useState, useRef } from "react"; import { Toaster } from "react-hot-toast"; import { FormattedNumber, IntlProvider } from "react-intl"; import { z } from "zod"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import dayjs, { Dayjs } from "@calcom/dayjs"; import { useEmbedNonStylesConfig, useEmbedStyles, useIsBackgroundTransparent, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; import CustomBranding from "@calcom/lib/CustomBranding"; import classNames from "@calcom/lib/classNames"; import { WEBSITE_URL } from "@calcom/lib/constants"; import getStripeAppData from "@calcom/lib/getStripeAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import notEmpty from "@calcom/lib/notEmpty"; import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { detectBrowserTimeFormat, getIs24hClockFromLocalStorage } from "@calcom/lib/timeFormat"; import { trpc } from "@calcom/trpc/react"; import { Icon } from "@calcom/ui/Icon"; import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker"; import { timeZone as localStorageTimeZone } from "@lib/clock"; // import { timeZone } from "@lib/clock"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import Gates, { Gate, GateState } from "@components/Gates"; import AvailableTimes from "@components/booking/AvailableTimes"; import BookingDescription from "@components/booking/BookingDescription"; import TimeOptions from "@components/booking/TimeOptions"; import { HeadSeo } from "@components/seo/head-seo"; import PoweredByCal from "@components/ui/PoweredByCal"; import type { AvailabilityPageProps } from "../../../pages/[user]/[type]"; import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]"; import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]"; const GoBackToPreviousPage = ({ t }: { t: TFunction }) => { const router = useRouter(); const path = router.asPath.split("/"); path.pop(); // Remove the last item (where we currently are) path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"] const slug = path.join("/"); return (
); }; const useSlots = ({ eventTypeId, eventTypeSlug, startTime, endTime, usernameList, timeZone, }: { eventTypeId: number; eventTypeSlug: string; startTime?: Dayjs; endTime?: Dayjs; usernameList: string[]; timeZone?: string; }) => { const { data, isLoading, isPaused } = trpc.useQuery( [ "viewer.public.slots.getSchedule", { eventTypeId, eventTypeSlug, usernameList, startTime: startTime?.toISOString() || "", endTime: endTime?.toISOString() || "", timeZone, }, ], { enabled: !!startTime && !!endTime } ); const [cachedSlots, setCachedSlots] = useState["slots"]>({}); useEffect(() => { if (data?.slots) { setCachedSlots((c) => ({ ...c, ...data?.slots })); } }, [data]); // The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state. return { slots: cachedSlots, isLoading: isLoading || isPaused }; }; const SlotPicker = ({ eventType, timeFormat, timeZone, recurringEventCount, users, seatsPerTimeSlot, weekStart = 0, ethSignature, }: { eventType: Pick; timeFormat: string; timeZone?: string; seatsPerTimeSlot?: number; recurringEventCount?: number; users: string[]; weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6; ethSignature?: string; }) => { const [selectedDate, setSelectedDate] = useState(); const [browsingDate, setBrowsingDate] = useState(); const { date, setQuery: setDate } = useRouterQuery("date"); const { month, setQuery: setMonth } = useRouterQuery("month"); const router = useRouter(); const slotPickerRef = useRef(null); useEffect(() => { slotPickerRef.current && autoAnimate(slotPickerRef.current); }, [slotPickerRef]); useEffect(() => { if (!router.isReady) return; // Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash. if (timeZone === "Etc/GMT") { setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)); if (date) { setSelectedDate(dayjs.utc(date)); } } else { // Set the start of the month without shifting time like startOf() may do. setBrowsingDate( dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0) ); if (date) { // It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime. setSelectedDate(dayjs.tz(date, timeZone)); } } }, [router.isReady, month, date, timeZone]); const { i18n, isLocaleReady } = useLocale(); const { slots: _1 } = useSlots({ eventTypeId: eventType.id, eventTypeSlug: eventType.slug, usernameList: users, startTime: selectedDate?.startOf("day"), endTime: selectedDate?.endOf("day"), timeZone, }); const { slots: _2, isLoading } = useSlots({ eventTypeId: eventType.id, eventTypeSlug: eventType.slug, usernameList: users, startTime: browsingDate?.startOf("month"), endTime: browsingDate?.endOf("month"), timeZone, }); const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]); return ( <> slots[k].length > 0)} locale={isLocaleReady ? i18n.language : "en"} selected={selectedDate} onChange={(newDate) => { setDate(newDate.format("YYYY-MM-DD")); }} onMonthChange={(newMonth) => { setMonth(newMonth.format("YYYY-MM")); }} browsingDate={browsingDate} weekStart={weekStart} />
); }; function TimezoneDropdown({ onChangeTimeFormat, onChangeTimeZone, timeZone, timeFormat, hideTimeFormatToggle, }: { onChangeTimeFormat: (newTimeFormat: string) => void; onChangeTimeZone: (newTimeZone: string) => void; timeZone?: string; timeFormat: string; hideTimeFormatToggle?: boolean; }) { const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); useEffect(() => { handleToggle24hClock(!!getIs24hClockFromLocalStorage()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSelectTimeZone = (newTimeZone: string) => { onChangeTimeZone(newTimeZone); localStorageTimeZone(newTimeZone); setIsTimeOptionsOpen(false); }; const handleToggle24hClock = (is24hClock: boolean) => { onChangeTimeFormat(is24hClock ? "HH:mm" : "h:mma"); }; return (

{timeZone} {isTimeOptionsOpen ? ( ) : ( )}

); } const dateQuerySchema = z.object({ rescheduleUid: z.string().optional().default(""), date: z.string().optional().default(""), timeZone: z.string().optional().default(""), }); const useRouterQuery = (name: T) => { const router = useRouter(); const existingQueryParams = router.asPath.split("?")[1]; const urlParams = new URLSearchParams(existingQueryParams); const query = Object.fromEntries(urlParams); const setQuery = (newValue: string | number | null | undefined) => { router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true }); router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true }); }; return { [name]: query[name], setQuery } as { [K in T]: string | undefined; } & { setQuery: typeof setQuery }; }; export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; const timeFormatTotimeFormatString = (timeFormat?: number | null) => { if (!timeFormat) return null; return timeFormat === 24 ? "HH:mm" : "h:mma"; }; const AvailabilityPage = ({ profile, eventType }: Props) => { const { data: user } = trpc.useQuery(["viewer.me"]); const timeFormatFromProfile = timeFormatTotimeFormatString(user?.timeFormat); const router = useRouter(); const isEmbed = useIsEmbed(); const query = dateQuerySchema.parse(router.query); const { rescheduleUid } = query; useTheme(profile.theme); const { t } = useLocale(); const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker"); const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const isBackgroundTransparent = useIsBackgroundTransparent(); const [timeZone, setTimeZone] = useState(); const [timeFormat, setTimeFormat] = useState("HH:mm"); const [gateState, gateDispatcher] = useReducer( (state: GateState, newState: Partial) => ({ ...state, ...newState, }), {} ); useEffect(() => { setTimeZone(localStorageTimeZone() || dayjs.tz.guess()); setTimeFormat(timeFormatFromProfile || detectBrowserTimeFormat); }, [timeFormatFromProfile]); // TODO: Improve this; useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO"); const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count); const telemetry = useTelemetry(); useEffect(() => { if (top !== window) { //page_view will be collected automatically by _middleware.ts telemetry.event( telemetryEventTypes.embedView, collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") }) ); } }, [telemetry]); // get dynamic user list here const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : []; const timezoneDropdown = useMemo( () => ( ), [timeZone, timeFormat, timeFormatFromProfile] ); const stripeAppData = getStripeAppData(eventType); const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {}; const rawSlug = profile.slug ? profile.slug.split("/") : []; if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug. // Define conditional gates here const gates = [ // Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress` rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress ? ("rainbow" as Gate) : undefined, ]; return ( ({ name: `${user.name}`, username: `${user.username}`, })), ], }} nextSeoProps={{ nofollow: eventType.hidden, noindex: eventType.hidden, }} />
{!rescheduleUid && eventType.recurringEvent && (

{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}

{ setRecurringEventCount(parseInt(event?.target.value)); }} />

{t("occurrence", { count: recurringEventCount, })}

)} {stripeAppData.price > 0 && (

)} {timezoneDropdown}
{!isEmbed && (
)} {/* Temporarily disabled - booking?.startTime && rescheduleUid && (

{t("former_time")}

{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}

)*/}
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && }
); }; export default AvailabilityPage;