// Get router variables
import {
ArrowLeftIcon,
CalendarIcon,
ChevronDownIcon,
ChevronUpIcon,
ClockIcon,
CreditCardIcon,
GlobeIcon,
InformationCircleIcon,
LocationMarkerIcon,
RefreshIcon,
VideoCameraIcon,
} from "@heroicons/react/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useContracts } from "contexts/contractsContext";
import dayjs, { 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 { useCallback, useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
import {
useEmbedStyles,
useIsEmbed,
useIsBackgroundTransparent,
sdkActionManager,
useEmbedNonStylesConfig,
} from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
import { Frequency } from "@calcom/prisma/zod-utils";
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 { parseDate } from "@lib/parseDate";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import CustomBranding from "@components/CustomBranding";
import AvailableTimes from "@components/booking/AvailableTimes";
import DatePicker from "@components/booking/DatePicker";
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 { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
dayjs.extend(utc);
dayjs.extend(customParseFormat);
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
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 AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage, booking }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed();
const { rescheduleUid } = router.query;
const { isReady, Theme } = useTheme(profile.theme);
const { t, i18n } = useLocale();
const { contracts } = useContracts();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const isBackgroundTransparent = useIsBackgroundTransparent();
useExposePlanGlobally(plan);
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 selectedDate = useMemo(() => {
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);
return date.isValid() ? date : null;
}
return null;
}, [router.query.date]);
if (selectedDate) {
// Let iframe take the width available due to increase in max-width
sdkActionManager?.fire("__refreshWidth", {});
}
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
const telemetry = useTelemetry();
useEffect(() => {
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
);
}
}, [telemetry]);
const changeDate = useCallback(
(newDate: Dayjs) => {
router.replace(
{
query: {
...router.query,
date: newDate.tz(timeZone(), true).format("YYYY-MM-DDZZ"),
},
},
undefined,
{ shallow: true }
);
},
[router]
);
useEffect(() => {
if (
selectedDate != null &&
selectedDate?.utcOffset() !== selectedDate.clone().utcOffset(0).tz(timeZone()).utcOffset()
) {
changeDate(selectedDate.tz(timeZone(), true));
}
}, [selectedDate, changeDate]);
const handleSelectTimeZone = (selectedTimeZone: string): void => {
timeZone(selectedTimeZone);
if (selectedDate) {
changeDate(selectedDate.tz(selectedTimeZone, true));
}
setIsTimeOptionsOpen(false);
};
const handleToggle24hClock = (is24hClock: boolean) => {
setTimeFormat(is24hClock ? "HH:mm" : "h:mma");
};
// Recurring event sidebar requires more space
const maxWidth = selectedDate
? recurringEventCount
? "max-w-6xl"
: "max-w-5xl"
: recurringEventCount
? "max-w-4xl"
: "max-w-3xl";
return (
<>
{isReady && (
{/* 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.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?.count && eventType.recurringEvent?.freq && (
{t("every_for_freq", {
freq: t(
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
),
})}
{
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
)}
{eventType.price > 0 && (
)}
{!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && (
{t("every_for_freq", {
freq: t(
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
),
})}
{
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
)}
{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.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?.count && eventType.recurringEvent?.freq && (
{t("every_for_freq", {
freq: t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
})}
{
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
)}
{eventType.price > 0 && (
)}
{previousPage === `${WEBAPP_URL}/${profile.slug}` && (
router.back()}
/>
Go Back
)}
{booking?.startTime && rescheduleUid && (
{t("former_time")}
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
)}
{selectedDate && (
)}
)}
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && }
>
);
function TimezoneDropdown() {
return (
{timeZone()}
{isTimeOptionsOpen ? (
) : (
)}
);
}
};
export default AvailabilityPage;