import { CalendarIcon, ClockIcon, CreditCardIcon, ExclamationIcon, LocationMarkerIcon, } from "@heroicons/react/solid"; import { EventTypeCustomInputType } from "@prisma/client"; import dayjs from "dayjs"; import Head from "next/head"; import { useRouter } from "next/router"; import { stringify } from "querystring"; import { useCallback, useEffect, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { createPaymentLink } from "@ee/lib/stripe/client"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; import { parseZone } from "@lib/parseZone"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { BookingCreateBody } from "@lib/types/booking"; import CustomBranding from "@components/CustomBranding"; import AvatarGroup from "@components/ui/AvatarGroup"; import { Button } from "@components/ui/Button"; import PhoneInput from "@components/ui/form/PhoneInput"; import { BookPageProps } from "../../../pages/[user]/book"; import { TeamBookingPageProps } from "../../../pages/team/[slug]/book"; type BookingPageProps = BookPageProps | TeamBookingPageProps; const BookingPage = (props: BookingPageProps) => { const { t, i18n } = useLocale(); const router = useRouter(); const { rescheduleUid } = router.query; const { isReady } = useTheme(props.profile.theme); const date = asStringOrNull(router.query.date); const timeFormat = asStringOrNull(router.query.clock) === "24h" ? "H:mm" : "h:mma"; const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const [guestToggle, setGuestToggle] = useState(false); const [guestEmails, setGuestEmails] = useState([]); const locations = props.eventType.locations || []; const [selectedLocation, setSelectedLocation] = useState( locations.length === 1 ? locations[0].type : "" ); const telemetry = useTelemetry(); useEffect(() => { telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters())); }, []); function toggleGuestEmailInput() { setGuestToggle(!guestToggle); } const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); // TODO: Move to translations const locationLabels = { [LocationType.InPerson]: t("in_person_meeting"), [LocationType.Phone]: t("phone_call"), [LocationType.GoogleMeet]: "Google Meet", [LocationType.Zoom]: "Zoom Video", [LocationType.Daily]: "Daily.co Video", }; const _bookingHandler = (event) => { const book = async () => { setLoading(true); setError(false); let notes = ""; if (props.eventType.customInputs) { notes = props.eventType.customInputs .map((input) => { const data = event.target["custom_" + input.id]; if (data) { if (input.type === EventTypeCustomInputType.BOOL) { return input.label + "\n" + (data.checked ? t("yes") : t("no")); } else { return input.label + "\n" + data.value; } } }) .join("\n\n"); } if (!!notes && !!event.target.notes.value) { notes += `\n\n${t("additional_notes")}:\n` + event.target.notes.value; } else { notes += event.target.notes.value; } const payload: BookingCreateBody = { start: dayjs(date).format(), end: dayjs(date).add(props.eventType.length, "minute").format(), name: event.target.name.value, email: event.target.email.value, notes: notes, guests: guestEmails, eventTypeId: props.eventType.id, timeZone: timeZone(), language: i18n.language, }; if (typeof rescheduleUid === "string") payload.rescheduleUid = rescheduleUid; if (typeof router.query.user === "string") payload.user = router.query.user; if (selectedLocation) { switch (selectedLocation) { case LocationType.Phone: payload["location"] = event.target.phone.value; break; case LocationType.InPerson: payload["location"] = locationInfo(selectedLocation).address; break; // Catches all other location types, such as Google Meet, Zoom etc. default: payload["location"] = selectedLocation; } } telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters()) ); const content = await createBooking(payload).catch((e) => { console.error(e.message); setLoading(false); setError(true); }); if (content?.id) { const params: { [k: string]: any } = { date, type: props.eventType.id, user: props.profile.slug, reschedule: !!rescheduleUid, name: payload.name, email: payload.email, }; if (payload["location"]) { if (payload["location"].includes("integration")) { params.location = t("web_conferencing_details_to_follow"); } else { params.location = payload["location"]; } } const query = stringify(params); let successUrl = `/success?${query}`; if (content?.paymentUid) { successUrl = createPaymentLink({ paymentUid: content?.paymentUid, name: payload.name, date, absolute: false, }); } await router.push(successUrl); } else { setLoading(false); setError(true); } }; event.preventDefault(); book(); }; const bookingHandler = useCallback(_bookingHandler, [guestEmails]); return (
{rescheduleUid ? t("booking_reschedule_confirmation", { eventTypeTitle: props.eventType.title, profileName: props.profile.name, }) : t("booking_confirmation", { eventTypeTitle: props.eventType.title, profileName: props.profile.name, })}{" "} | Cal.com
{isReady && (
user.name !== props.profile.name) .map((user) => ({ image: user.avatar, title: user.name, })) )} />

{props.profile.name}

{props.eventType.title}

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

{props.eventType.price > 0 && (

)} {selectedLocation === LocationType.InPerson && (

{locationInfo(selectedLocation).address}

)}

{parseZone(date).format(timeFormat + ", dddd DD MMMM YYYY")}

{props.eventType.description}

{locations.length > 1 && (
{t("location")} {locations.map((location) => ( ))}
)} {selectedLocation === LocationType.Phone && (
)} {props.eventType.customInputs && props.eventType.customInputs .sort((a, b) => a.id - b.id) .map((input) => (
{input.type !== EventTypeCustomInputType.BOOL && ( )} {input.type === EventTypeCustomInputType.TEXTLONG && (