// Get router variables import { ArrowLeftIcon, ChevronDownIcon, ChevronUpIcon, ClipboardCheckIcon, ClockIcon, CreditCardIcon, GlobeIcon, InformationCircleIcon, LocationMarkerIcon, RefreshIcon, VideoCameraIcon, } from "@heroicons/react/solid"; import { EventType } from "@prisma/client"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useContracts } from "contexts/contractsContext"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; import utc from "dayjs/plugin/utc"; import { TFunction } from "next-i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations"; import { useEmbedNonStylesConfig, useEmbedStyles, useIsBackgroundTransparent, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; import classNames from "@calcom/lib/classNames"; import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { localStorage } from "@calcom/lib/webstorage"; import DatePicker, { Day } from "@calcom/ui/booker/DatePicker"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import useTheme from "@lib/hooks/useTheme"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { detectBrowserTimeFormat } from "@lib/timeFormat"; import { trpc } from "@lib/trpc"; import CustomBranding from "@components/CustomBranding"; import AvailableTimes from "@components/booking/AvailableTimes"; import TimeOptions from "@components/booking/TimeOptions"; import { HeadSeo } from "@components/seo/head-seo"; import AvatarGroup from "@components/ui/AvatarGroup"; 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]"; dayjs.extend(utc); dayjs.extend(customParseFormat); type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; export const locationKeyToString = (location: LocationObject, t: TFunction) => { switch (location.type) { case LocationType.InPerson: return location.address || "In Person"; // If disabled address won't exist on the object case LocationType.Link: return location.link || "Link"; // If disabled link won't exist on the object case LocationType.Phone: return t("your_number"); case LocationType.UserPhone: return t("phone_call"); case LocationType.GoogleMeet: return "Google Meet"; case LocationType.Zoom: return "Zoom"; case LocationType.Daily: return "Cal Video"; case LocationType.Jitsi: return "Jitsi"; case LocationType.Huddle01: return "Huddle Video"; case LocationType.Tandem: return "Tandem"; case LocationType.Teams: return "Microsoft Teams"; default: return null; } }; const GoBackToPreviousPage = ({ slug }: { slug: string }) => { const router = useRouter(); const [previousPage, setPreviousPage] = useState(); useEffect(() => { setPreviousPage(document.referrer); }, []); return previousPage === `${WEBAPP_URL}/${slug}` ? (
router.back()} />

Go Back

) : ( <> ); }; const useSlots = ({ eventTypeId, startTime, endTime, }: { eventTypeId: number; startTime: Date; endTime: Date; }) => { const { data, isLoading } = trpc.useQuery( [ "viewer.public.slots.getSchedule", { eventTypeId, startTime: startTime.toISOString(), endTime: endTime.toISOString(), }, ], /** Prevents fetching past dates */ { enabled: dayjs(startTime).isAfter(dayjs().subtract(1, "day")) } ); return { slots: data?.slots || {}, isLoading }; }; const SlotPicker = ({ eventType, timezoneDropdown, timeFormat, timeZone, recurringEventCount, seatsPerTimeSlot, weekStart = 0, }: { eventType: Pick; timezoneDropdown: JSX.Element; timeFormat: string; timeZone?: string; seatsPerTimeSlot?: number; recurringEventCount?: number; weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6; }) => { const { selectedDate, setSelectedDate } = useDateSelected({ timeZone }); const { i18n, isLocaleReady } = useLocale(); const [startDate, setStartDate] = useState(new Date()); useEffect(() => { if (dayjs(selectedDate).startOf("month").isAfter(dayjs())) { setStartDate(dayjs(selectedDate).startOf("month").toDate()); } }, [selectedDate]); const { slots, isLoading } = useSlots({ eventTypeId: eventType.id, startTime: dayjs(startDate).startOf("day").toDate(), endTime: dayjs(startDate).endOf("month").toDate(), }); return ( <> slots[k].length > 0)} locale={isLocaleReady ? i18n.language : "en"} selected={selectedDate} onChange={setSelectedDate} onMonthChange={(startDate) => { // set the minimum day to today in the current month, not the beginning of the month setStartDate( dayjs(startDate).isBefore(dayjs().subtract(1, "day")) ? dayjs(new Date()).startOf("day").toDate() : startDate ); }} weekStart={weekStart} // DayComponent={(props) => } />
{timezoneDropdown}
{selectedDate && ( )} ); }; function TimezoneDropdown({ onChangeTimeFormat, onChangeTimeZone, }: { onChangeTimeFormat: (newTimeFormat: string) => void; onChangeTimeZone: (newTimeZone: string) => void; }) { const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); useEffect(() => { handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true"); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSelectTimeZone = (newTimeZone: string) => { timeZone(newTimeZone); onChangeTimeZone(newTimeZone); setIsTimeOptionsOpen(false); }; const handleToggle24hClock = (is24hClock: boolean) => { onChangeTimeFormat(is24hClock ? "HH:mm" : "h:mma"); }; return ( {timeZone()} {isTimeOptionsOpen ? ( ) : ( )} ); } const useDateSelected = ({ timeZone }: { timeZone?: string }) => { const router = useRouter(); const [selectedDate, _setSelectedDate] = useState(); useEffect(() => { /** TODO: router.query.date is comming as `null` even when set like this: * `/user/type?date=2022-06-22-0600` */ const dateString = asStringOrNull(router.query.date); if (dateString) { const offsetString = dateString.substr(11, 14); // hhmm const offsetSign = dateString.substr(10, 1); // + or - const offsetHour = offsetString.slice(0, -2); const offsetMinute = offsetString.slice(-2); const utcOffsetInMinutes = (offsetSign === "-" ? -1 : 1) * (60 * (offsetHour !== "" ? parseInt(offsetHour) : 0) + (offsetMinute !== "" ? parseInt(offsetMinute) : 0)); const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffsetInMinutes, true); console.log("date.isValid()", date.isValid()); if (date.isValid()) { setSelectedDate(date.toDate()); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.query.date]); const setSelectedDate = (newDate: Date) => { router.replace( { query: { ...router.query, date: dayjs(newDate).tz(timeZone, true).format("YYYY-MM-DDZZ"), }, }, undefined, { shallow: true } ); _setSelectedDate(newDate); }; return { selectedDate, setSelectedDate }; }; const AvailabilityPage = ({ profile, eventType }: Props) => { const router = useRouter(); const isEmbed = useIsEmbed(); const { rescheduleUid } = router.query; const { Theme } = useTheme(profile.theme); const { t } = useLocale(); const { contracts } = useContracts(); const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker"); const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const isBackgroundTransparent = useIsBackgroundTransparent(); const [timeZone, setTimeZone] = useState(); const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat); const [isAvailableTimesVisible, setIsAvailableTimesVisible] = useState(); useEffect(() => { setIsAvailableTimesVisible(!!router.query.date); }, [router.query.date]); // TODO: Improve this; useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO"); // TODO: this needs to be extracted elsewhere useEffect(() => { if (eventType.metadata.smartContractAddress) { const eventOwner = eventType.users[0]; if (!contracts[(eventType.metadata.smartContractAddress || null) as number]) router.replace(`/${eventOwner.username}`); } }, [contracts, eventType.metadata.smartContractAddress, eventType.users, router]); 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]); // Avoid embed styling flicker. Till embed status is confirmed, don't render. if (isEmbed === null) { return null; } // Recurring event sidebar requires more space const maxWidth = isAvailableTimesVisible ? recurringEventCount ? "max-w-6xl" : "max-w-5xl" : recurringEventCount ? "max-w-4xl" : "max-w-3xl"; const timezoneDropdown = ( ); return ( <>
{/* mobile: details */}
user.name !== profile.name) .map((user) => ({ title: user.name, image: `${CAL_URL}/${user.username}/avatar.png`, alt: user.name || undefined, })), ].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[] } size={9} truncateAfter={5} />

{profile.name}

{eventType.title}

{eventType?.description && (

{eventType.description}

)} {eventType?.requiresConfirmation && (

{t("requires_confirmation")}

)} {eventType.locations.length === 1 && (

{Object.values(AppStoreLocationType).includes( eventType.locations[0].type as unknown as AppStoreLocationType ) ? ( ) : ( )} {locationKeyToString(eventType.locations[0], t)}

)} {eventType.locations.length > 1 && (

{eventType.locations.map((el, i, arr) => { return ( {locationKeyToString(el, t)}{" "} {arr.length - 1 !== i && ( {t("or_lowercase")} )} ); })}

)}

{eventType.length} {t("minutes")}

{eventType.price > 0 && (
)} {!rescheduleUid && eventType.recurringEvent && (

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

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

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

)} {timezoneDropdown}
{/* Temp disabled booking?.startTime && rescheduleUid && (

{t("former_time")}

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

)*/}
user.name !== profile.name) .map((user) => ({ title: user.name, alt: user.name, image: `${CAL_URL}/${user.username}/avatar.png`, })), ].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[] } size={10} truncateAfter={3} />

{profile.name}

{eventType.title}

{eventType?.description && (

{eventType.description}

)} {eventType?.requiresConfirmation && (
{t("requires_confirmation")}
)} {eventType.locations.length === 1 && (

{Object.values(AppStoreLocationType).includes( eventType.locations[0].type as unknown as AppStoreLocationType ) ? ( ) : ( )} {locationKeyToString(eventType.locations[0], t)}

)} {eventType.locations.length > 1 && (

{eventType.locations.map((el, i, arr) => { return ( {locationKeyToString(el, t)}{" "} {arr.length - 1 !== i && ( {t("or_lowercase")} )} ); })}

)}

{eventType.length} {t("minutes")}

{!rescheduleUid && eventType.recurringEvent && (

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

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

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

)} {eventType.price > 0 && (

)} {timezoneDropdown}
{/* 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 && }
); }; const DayContainer = (props: React.ComponentProps & { eventTypeId: number }) => { const { eventTypeId, ...rest } = props; /** : * Fetch each individual day here. All these are batched with tRPC anyways. **/ const { slots } = useSlots({ eventTypeId, startTime: dayjs(props.date).startOf("day").toDate(), endTime: dayjs(props.date).endOf("day").toDate(), }); const includedDates = Object.keys(slots).filter((k) => slots[k].length > 0); const disabled = includedDates.length > 0 ? !includedDates.includes(yyyymmdd(props.date)) : props.disabled; return ; }; const AvailableTimesContainer = (props: React.ComponentProps) => { const { date, eventTypeId } = props; const { slots } = useSlots({ eventTypeId, startTime: dayjs(date).startOf("day").toDate(), endTime: dayjs(date).endOf("day").toDate(), }); return ; }; export default AvailabilityPage;