2022-04-27 15:19:04 +00:00
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
2022-07-14 00:10:45 +00:00
|
|
|
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
|
2022-09-29 16:58:29 +00:00
|
|
|
import { useMutation } from "@tanstack/react-query";
|
2022-07-06 19:01:16 +00:00
|
|
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
2022-03-15 14:39:20 +00:00
|
|
|
import { useSession } from "next-auth/react";
|
2021-09-22 19:52:38 +00:00
|
|
|
import Head from "next/head";
|
|
|
|
import { useRouter } from "next/router";
|
2022-11-23 02:55:25 +00:00
|
|
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
2023-02-14 23:26:29 +00:00
|
|
|
import { useFieldArray, useForm, useWatch } from "react-hook-form";
|
2021-09-22 19:52:38 +00:00
|
|
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
2022-05-05 21:16:25 +00:00
|
|
|
import { v4 as uuidv4 } from "uuid";
|
2022-04-27 15:19:04 +00:00
|
|
|
import { z } from "zod";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2022-10-14 16:24:43 +00:00
|
|
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
2022-08-26 00:48:50 +00:00
|
|
|
import {
|
2022-11-23 02:55:25 +00:00
|
|
|
getEventLocationType,
|
|
|
|
getEventLocationValue,
|
2022-10-17 13:47:11 +00:00
|
|
|
getHumanReadableLocationValue,
|
2022-11-23 02:55:25 +00:00
|
|
|
locationKeyToString,
|
2022-08-26 00:48:50 +00:00
|
|
|
} from "@calcom/app-store/locations";
|
2022-07-28 19:58:26 +00:00
|
|
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
2022-10-14 16:24:43 +00:00
|
|
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { LocationObject } from "@calcom/core/location";
|
|
|
|
import { LocationType } from "@calcom/core/location";
|
2022-06-28 20:40:58 +00:00
|
|
|
import dayjs from "@calcom/dayjs";
|
2022-05-27 15:37:02 +00:00
|
|
|
import {
|
|
|
|
useEmbedNonStylesConfig,
|
2022-12-13 07:23:26 +00:00
|
|
|
useEmbedUiConfig,
|
2022-05-27 15:37:02 +00:00
|
|
|
useIsBackgroundTransparent,
|
|
|
|
useIsEmbed,
|
|
|
|
} from "@calcom/embed-core/embed-iframe";
|
2022-07-23 00:39:50 +00:00
|
|
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
2022-04-08 05:33:24 +00:00
|
|
|
import classNames from "@calcom/lib/classNames";
|
2022-11-30 21:52:56 +00:00
|
|
|
import { APP_NAME } from "@calcom/lib/constants";
|
2023-02-08 20:36:22 +00:00
|
|
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
2022-04-06 17:20:30 +00:00
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
2022-07-28 19:58:26 +00:00
|
|
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
2022-03-24 02:27:35 +00:00
|
|
|
import { HttpError } from "@calcom/lib/http-error";
|
2022-06-10 20:38:06 +00:00
|
|
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
2023-01-26 22:51:03 +00:00
|
|
|
import slugify from "@calcom/lib/slugify";
|
2022-07-28 19:58:26 +00:00
|
|
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
2023-01-23 23:08:01 +00:00
|
|
|
import {
|
2023-01-31 17:36:38 +00:00
|
|
|
AddressInput,
|
|
|
|
Button,
|
|
|
|
EmailField,
|
|
|
|
EmailInput,
|
|
|
|
Form,
|
|
|
|
Group,
|
|
|
|
PhoneInput,
|
|
|
|
RadioField,
|
|
|
|
Tooltip,
|
|
|
|
} from "@calcom/ui";
|
|
|
|
import {
|
|
|
|
FiUserPlus,
|
2023-01-23 23:08:01 +00:00
|
|
|
FiCalendar,
|
2023-01-31 17:36:38 +00:00
|
|
|
FiX,
|
2023-01-23 23:08:01 +00:00
|
|
|
FiInfo,
|
2023-01-31 17:36:38 +00:00
|
|
|
FiCreditCard,
|
2023-01-23 23:08:01 +00:00
|
|
|
FiRefreshCw,
|
|
|
|
FiUser,
|
2023-01-31 17:36:38 +00:00
|
|
|
FiAlertTriangle,
|
2023-01-23 23:08:01 +00:00
|
|
|
} from "@calcom/ui/components/icon";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
|
|
|
import { timeZone } from "@lib/clock";
|
2021-12-03 16:18:31 +00:00
|
|
|
import { ensureArray } from "@lib/ensureArray";
|
2022-11-28 18:14:01 +00:00
|
|
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
2021-09-22 19:52:38 +00:00
|
|
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
2022-05-05 21:16:25 +00:00
|
|
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
|
|
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { Gate, GateState } from "@components/Gates";
|
|
|
|
import Gates from "@components/Gates";
|
2022-10-12 08:39:14 +00:00
|
|
|
import BookingDescription from "@components/booking/BookingDescription";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { BookPageProps } from "../../../pages/[user]/book";
|
|
|
|
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
|
|
|
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
2021-09-14 08:45:28 +00:00
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
2021-09-22 18:36:13 +00:00
|
|
|
|
2022-02-01 21:48:40 +00:00
|
|
|
type BookingFormValues = {
|
|
|
|
name: string;
|
|
|
|
email: string;
|
|
|
|
notes?: string;
|
2022-08-26 00:48:50 +00:00
|
|
|
locationType?: EventLocationType["type"];
|
2023-01-31 17:36:38 +00:00
|
|
|
guests?: { email: string }[];
|
2022-11-05 20:10:10 +00:00
|
|
|
address?: string;
|
|
|
|
attendeeAddress?: string;
|
2022-02-01 21:48:40 +00:00
|
|
|
phone?: string;
|
2022-05-16 15:50:12 +00:00
|
|
|
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
|
2022-02-01 21:48:40 +00:00
|
|
|
customInputs?: {
|
2022-05-18 21:05:49 +00:00
|
|
|
[key: string]: string | boolean;
|
2022-02-01 21:48:40 +00:00
|
|
|
};
|
2022-05-30 19:40:29 +00:00
|
|
|
rescheduleReason?: string;
|
2022-07-14 00:10:45 +00:00
|
|
|
smsReminderNumber?: string;
|
2022-02-01 21:48:40 +00:00
|
|
|
};
|
|
|
|
|
2022-04-06 17:20:30 +00:00
|
|
|
const BookingPage = ({
|
|
|
|
eventType,
|
|
|
|
booking,
|
|
|
|
profile,
|
|
|
|
isDynamicGroupBooking,
|
2022-05-05 21:16:25 +00:00
|
|
|
recurringEventCount,
|
2022-04-28 15:44:26 +00:00
|
|
|
hasHashedBookingLink,
|
|
|
|
hashedLink,
|
2022-10-19 21:25:03 +00:00
|
|
|
...restProps
|
2022-04-06 17:20:30 +00:00
|
|
|
}: BookingPageProps) => {
|
2021-10-25 13:05:21 +00:00
|
|
|
const { t, i18n } = useLocale();
|
2022-11-28 18:14:01 +00:00
|
|
|
const { duration: queryDuration } = useRouterQuery("duration");
|
2022-10-19 21:25:03 +00:00
|
|
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
2022-12-13 07:23:26 +00:00
|
|
|
const embedUiConfig = useEmbedUiConfig();
|
2022-04-25 04:33:00 +00:00
|
|
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
|
|
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
2021-09-14 08:45:28 +00:00
|
|
|
const router = useRouter();
|
2022-03-15 14:39:20 +00:00
|
|
|
const { data: session } = useSession();
|
2022-04-08 05:33:24 +00:00
|
|
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
2022-05-14 13:49:39 +00:00
|
|
|
const telemetry = useTelemetry();
|
2022-09-05 21:10:58 +00:00
|
|
|
const [gateState, gateDispatcher] = useReducer(
|
|
|
|
(state: GateState, newState: Partial<GateState>) => ({
|
|
|
|
...state,
|
|
|
|
...newState,
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
);
|
2023-02-08 20:36:22 +00:00
|
|
|
const paymentAppData = getPaymentAppData(eventType);
|
2022-11-28 18:14:01 +00:00
|
|
|
// Define duration now that we support multiple duration eventTypes
|
|
|
|
let duration = eventType.length;
|
2023-02-01 22:19:37 +00:00
|
|
|
if (
|
|
|
|
queryDuration &&
|
|
|
|
!isNaN(Number(queryDuration)) &&
|
|
|
|
eventType.metadata?.multipleDuration &&
|
|
|
|
eventType.metadata?.multipleDuration.includes(Number(queryDuration))
|
|
|
|
) {
|
2022-11-28 18:14:01 +00:00
|
|
|
duration = Number(queryDuration);
|
|
|
|
}
|
|
|
|
|
2022-05-11 05:14:08 +00:00
|
|
|
useEffect(() => {
|
2022-06-02 16:19:01 +00:00
|
|
|
if (top !== window) {
|
|
|
|
//page_view will be collected automatically by _middleware.ts
|
|
|
|
telemetry.event(
|
|
|
|
telemetryEventTypes.embedView,
|
2022-05-11 05:14:08 +00:00
|
|
|
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
2022-06-02 16:19:01 +00:00
|
|
|
);
|
|
|
|
}
|
2022-05-14 13:49:39 +00:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2022-05-11 05:14:08 +00:00
|
|
|
}, []);
|
|
|
|
|
2021-12-03 10:15:20 +00:00
|
|
|
const mutation = useMutation(createBooking, {
|
2022-02-18 16:53:45 +00:00
|
|
|
onSuccess: async (responseData) => {
|
2022-11-15 19:00:02 +00:00
|
|
|
const { uid, paymentUid } = responseData;
|
2023-02-08 20:36:22 +00:00
|
|
|
|
2021-12-03 10:15:20 +00:00
|
|
|
if (paymentUid) {
|
|
|
|
return await router.push(
|
|
|
|
createPaymentLink({
|
|
|
|
paymentUid,
|
|
|
|
date,
|
2022-10-18 19:41:50 +00:00
|
|
|
name: bookingForm.getValues("name"),
|
|
|
|
email: bookingForm.getValues("email"),
|
2021-12-03 10:15:20 +00:00
|
|
|
absolute: false,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return router.push({
|
2022-11-29 20:27:29 +00:00
|
|
|
pathname: `/booking/${uid}`,
|
2021-12-03 10:15:20 +00:00
|
|
|
query: {
|
2022-06-08 14:02:09 +00:00
|
|
|
isSuccessBookingPage: true,
|
2022-11-15 19:00:02 +00:00
|
|
|
email: bookingForm.getValues("email"),
|
|
|
|
eventTypeSlug: eventType.slug,
|
2023-02-08 21:13:10 +00:00
|
|
|
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
2021-12-03 10:15:20 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2022-05-05 21:16:25 +00:00
|
|
|
const recurringMutation = useMutation(createRecurringBooking, {
|
|
|
|
onSuccess: async (responseData = []) => {
|
2022-11-15 19:00:02 +00:00
|
|
|
const { uid } = responseData[0] || {};
|
2022-05-05 21:16:25 +00:00
|
|
|
|
|
|
|
return router.push({
|
2022-11-29 20:27:29 +00:00
|
|
|
pathname: `/booking/${uid}`,
|
2022-05-05 21:16:25 +00:00
|
|
|
query: {
|
2022-11-16 19:48:17 +00:00
|
|
|
allRemainingBookings: true,
|
2022-11-15 19:00:02 +00:00
|
|
|
email: bookingForm.getValues("email"),
|
|
|
|
eventTypeSlug: eventType.slug,
|
2022-12-20 22:46:28 +00:00
|
|
|
formerTime: booking?.startTime.toString(),
|
2022-05-05 21:16:25 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2021-12-03 10:15:20 +00:00
|
|
|
const rescheduleUid = router.query.rescheduleUid as string;
|
2022-07-26 08:27:57 +00:00
|
|
|
useTheme(profile.theme);
|
2021-09-14 08:45:28 +00:00
|
|
|
const date = asStringOrNull(router.query.date);
|
|
|
|
|
2022-03-15 14:39:20 +00:00
|
|
|
const [guestToggle, setGuestToggle] = useState(booking && booking.attendees.length > 1);
|
2021-12-03 10:15:20 +00:00
|
|
|
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
|
2022-05-25 20:34:08 +00:00
|
|
|
const locations: LocationObject[] = useMemo(
|
|
|
|
() => (eventType.locations as LocationObject[]) || [],
|
2022-03-15 14:39:20 +00:00
|
|
|
[eventType.locations]
|
2021-09-14 08:45:28 +00:00
|
|
|
);
|
|
|
|
|
2021-12-03 10:15:20 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (router.query.guest) {
|
|
|
|
setGuestToggle(true);
|
|
|
|
}
|
|
|
|
}, [router.query.guest]);
|
2021-09-14 08:45:28 +00:00
|
|
|
|
2022-05-18 21:05:49 +00:00
|
|
|
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
|
2022-04-14 21:25:24 +00:00
|
|
|
const guestListEmails = !isDynamicGroupBooking
|
2023-01-31 17:36:38 +00:00
|
|
|
? booking?.attendees.slice(1).map((attendee) => {
|
|
|
|
return { email: attendee.email };
|
|
|
|
})
|
2022-04-14 21:25:24 +00:00
|
|
|
: [];
|
|
|
|
|
2022-08-05 17:08:47 +00:00
|
|
|
// There should only exists one default userData variable for primaryAttendee.
|
|
|
|
const defaultUserValues = {
|
2022-09-17 20:44:29 +00:00
|
|
|
email: rescheduleUid
|
|
|
|
? booking?.attendees[0].email
|
2022-08-05 17:08:47 +00:00
|
|
|
: router.query.email
|
|
|
|
? (router.query.email as string)
|
|
|
|
: "",
|
2022-09-17 20:44:29 +00:00
|
|
|
name: rescheduleUid ? booking?.attendees[0].name : router.query.name ? (router.query.name as string) : "",
|
2022-08-05 17:08:47 +00:00
|
|
|
};
|
|
|
|
|
2022-01-10 23:25:06 +00:00
|
|
|
const defaultValues = () => {
|
|
|
|
if (!rescheduleUid) {
|
|
|
|
return {
|
2022-08-05 17:08:47 +00:00
|
|
|
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
|
|
|
|
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
|
2022-01-10 23:25:06 +00:00
|
|
|
notes: (router.query.notes as string) || "",
|
2023-01-31 17:36:38 +00:00
|
|
|
guests: ensureArray(router.query.guest).map((guest) => {
|
|
|
|
return { email: guest as string };
|
|
|
|
}),
|
2022-03-15 14:39:20 +00:00
|
|
|
customInputs: eventType.customInputs.reduce(
|
2022-01-10 23:25:06 +00:00
|
|
|
(customInputs, input) => ({
|
|
|
|
...customInputs,
|
|
|
|
[input.id]: router.query[slugify(input.label)],
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
2022-03-15 14:39:20 +00:00
|
|
|
if (!booking || !booking.attendees.length) {
|
2022-01-10 23:25:06 +00:00
|
|
|
return {};
|
|
|
|
}
|
2022-03-15 14:39:20 +00:00
|
|
|
const primaryAttendee = booking.attendees[0];
|
2022-01-10 23:25:06 +00:00
|
|
|
if (!primaryAttendee) {
|
|
|
|
return {};
|
|
|
|
}
|
2022-05-18 21:05:49 +00:00
|
|
|
|
|
|
|
const customInputType = booking.customInputs;
|
2022-01-10 23:25:06 +00:00
|
|
|
return {
|
2022-08-05 17:08:47 +00:00
|
|
|
name: defaultUserValues.name,
|
|
|
|
email: defaultUserValues.email || "",
|
2022-04-14 21:25:24 +00:00
|
|
|
guests: guestListEmails,
|
|
|
|
notes: booking.description || "",
|
2022-05-30 19:40:29 +00:00
|
|
|
rescheduleReason: "",
|
2022-09-09 19:01:37 +00:00
|
|
|
smsReminderNumber: booking.smsReminderNumber || undefined,
|
2022-05-18 21:05:49 +00:00
|
|
|
customInputs: eventType.customInputs.reduce(
|
|
|
|
(customInputs, input) => ({
|
|
|
|
...customInputs,
|
|
|
|
[input.id]: booking.customInputs
|
|
|
|
? booking.customInputs[input.label as keyof typeof customInputType]
|
|
|
|
: "",
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
),
|
2022-01-10 23:25:06 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2022-04-27 21:21:18 +00:00
|
|
|
const bookingFormSchema = z
|
|
|
|
.object({
|
|
|
|
name: z.string().min(1),
|
2022-12-07 20:12:53 +00:00
|
|
|
email: z.string().trim().email(),
|
2023-01-31 17:36:38 +00:00
|
|
|
guests: z.array(z.object({ email: z.string().email() })).optional(),
|
2022-07-06 19:01:16 +00:00
|
|
|
phone: z
|
|
|
|
.string()
|
|
|
|
.refine((val) => isValidPhoneNumber(val))
|
2022-10-20 18:21:27 +00:00
|
|
|
.optional()
|
|
|
|
.nullable(),
|
2022-11-05 20:10:10 +00:00
|
|
|
attendeeAddress: z.string().optional().nullable(),
|
2022-07-14 00:10:45 +00:00
|
|
|
smsReminderNumber: z
|
|
|
|
.string()
|
|
|
|
.refine((val) => isValidPhoneNumber(val))
|
2022-10-18 12:47:15 +00:00
|
|
|
.optional()
|
|
|
|
.nullable(),
|
2022-04-27 21:21:18 +00:00
|
|
|
})
|
|
|
|
.passthrough();
|
2022-04-27 15:19:04 +00:00
|
|
|
|
2021-12-03 10:15:20 +00:00
|
|
|
const bookingForm = useForm<BookingFormValues>({
|
2022-01-10 23:25:06 +00:00
|
|
|
defaultValues: defaultValues(),
|
2022-04-27 21:21:18 +00:00
|
|
|
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
|
2021-12-03 10:15:20 +00:00
|
|
|
});
|
2023-01-31 17:36:38 +00:00
|
|
|
const guestsField = useFieldArray({
|
|
|
|
name: "guests",
|
|
|
|
control: bookingForm.control,
|
|
|
|
});
|
2021-09-14 08:45:28 +00:00
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
const selectedLocationType = useWatch({
|
2021-12-03 10:15:20 +00:00
|
|
|
control: bookingForm.control,
|
|
|
|
name: "locationType",
|
2022-08-26 00:48:50 +00:00
|
|
|
defaultValue: ((): EventLocationType["type"] | undefined => {
|
2021-12-03 10:15:20 +00:00
|
|
|
if (router.query.location) {
|
2022-08-26 00:48:50 +00:00
|
|
|
return router.query.location as EventLocationType["type"];
|
2021-09-14 08:45:28 +00:00
|
|
|
}
|
2021-12-03 10:15:20 +00:00
|
|
|
if (locations.length === 1) {
|
|
|
|
return locations[0]?.type;
|
|
|
|
}
|
|
|
|
})(),
|
|
|
|
});
|
2021-09-14 08:45:28 +00:00
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
const selectedLocation = getEventLocationType(selectedLocationType);
|
|
|
|
const AttendeeInput =
|
2022-11-05 20:10:10 +00:00
|
|
|
selectedLocation?.attendeeInputType === "phone"
|
2022-08-26 00:48:50 +00:00
|
|
|
? PhoneInput
|
2022-11-05 20:10:10 +00:00
|
|
|
: selectedLocation?.attendeeInputType === "attendeeAddress"
|
|
|
|
? AddressInput
|
2022-08-26 00:48:50 +00:00
|
|
|
: null;
|
2021-09-22 18:36:13 +00:00
|
|
|
|
2022-05-05 21:16:25 +00:00
|
|
|
// Calculate the booking date(s)
|
|
|
|
let recurringStrings: string[] = [],
|
|
|
|
recurringDates: Date[] = [];
|
|
|
|
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
|
|
|
[recurringStrings, recurringDates] = parseRecurringDates(
|
|
|
|
{
|
|
|
|
startDate: date,
|
2022-07-07 01:08:38 +00:00
|
|
|
timeZone: timeZone(),
|
2022-05-05 21:16:25 +00:00
|
|
|
recurringEvent: eventType.recurringEvent,
|
|
|
|
recurringCount: parseInt(recurringEventCount.toString()),
|
|
|
|
},
|
|
|
|
i18n
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-03 10:15:20 +00:00
|
|
|
const bookEvent = (booking: BookingFormValues) => {
|
2023-01-31 17:36:38 +00:00
|
|
|
bookingForm.clearErrors();
|
2022-12-16 19:39:41 +00:00
|
|
|
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
|
|
|
|
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
|
|
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
|
|
|
|
}));
|
|
|
|
|
|
|
|
// Checking if custom inputs of type Phone number are valid to display error message on UI
|
|
|
|
if (eventType.customInputs.length) {
|
|
|
|
let isErrorFound = false;
|
|
|
|
eventType.customInputs.forEach((customInput) => {
|
|
|
|
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
|
|
|
|
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
|
|
|
|
try {
|
|
|
|
z.string({
|
|
|
|
errorMap: () => ({
|
|
|
|
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
.refine((val) => isValidPhoneNumber(val), {
|
|
|
|
message: "Phone number is invalid",
|
|
|
|
})
|
|
|
|
.parse(input?.value);
|
|
|
|
} catch (err) {
|
|
|
|
isErrorFound = true;
|
|
|
|
bookingForm.setError(`customInputs.${customInput.id}`, {
|
|
|
|
type: "custom",
|
|
|
|
message: "Invalid Phone number",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (isErrorFound) return;
|
|
|
|
}
|
|
|
|
|
2022-06-02 16:19:01 +00:00
|
|
|
telemetry.event(
|
|
|
|
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
|
|
|
{ isTeamBooking: document.URL.includes("team/") }
|
2021-12-03 10:15:20 +00:00
|
|
|
);
|
|
|
|
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
|
|
|
|
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
|
2022-02-01 21:48:40 +00:00
|
|
|
|
|
|
|
// @TODO: move to metadata
|
2021-12-03 10:15:20 +00:00
|
|
|
const metadata = Object.keys(router.query)
|
|
|
|
.filter((key) => key.startsWith("metadata"))
|
|
|
|
.reduce(
|
|
|
|
(metadata, key) => ({
|
|
|
|
...metadata,
|
|
|
|
[key.substring("metadata[".length, key.length - 1)]: router.query[key],
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
);
|
2021-09-14 08:45:28 +00:00
|
|
|
|
2022-12-12 13:18:04 +00:00
|
|
|
if (eventType.customInputs.length > 0) {
|
|
|
|
// find all required custom inputs and ensure they are filled out in the booking form
|
|
|
|
const requiredCustomInputs = eventType.customInputs.filter((input) => input.required);
|
|
|
|
const missingRequiredCustomInputs = requiredCustomInputs.filter(
|
|
|
|
(input) => !booking?.customInputs?.[input.id]
|
|
|
|
);
|
|
|
|
if (missingRequiredCustomInputs.length > 0) {
|
|
|
|
missingRequiredCustomInputs.forEach((input) => {
|
|
|
|
bookingForm.setError(`customInputs.${input.id}`, {
|
|
|
|
type: "required",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-31 17:36:38 +00:00
|
|
|
// Validate that guests are unique
|
|
|
|
let alreadyInvited = false;
|
|
|
|
booking.guests?.forEach((guest, index) => {
|
|
|
|
if (guest.email === booking.email) {
|
|
|
|
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
|
|
|
|
alreadyInvited = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (booking.guests) {
|
|
|
|
let guestCount = 0;
|
|
|
|
for (const checkGuest of booking.guests) {
|
|
|
|
if (checkGuest.email === guest.email) guestCount++;
|
|
|
|
if (guestCount > 1) {
|
|
|
|
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
|
|
|
|
alreadyInvited = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (alreadyInvited) return;
|
|
|
|
|
2022-05-05 21:16:25 +00:00
|
|
|
if (recurringDates.length) {
|
|
|
|
// Identify set of bookings to one intance of recurring event to support batch changes
|
|
|
|
const recurringEventId = uuidv4();
|
|
|
|
const recurringBookings = recurringDates.map((recurringDate) => ({
|
|
|
|
...booking,
|
|
|
|
start: dayjs(recurringDate).format(),
|
2022-11-28 18:14:01 +00:00
|
|
|
end: dayjs(recurringDate).add(duration, "minute").format(),
|
2022-05-05 21:16:25 +00:00
|
|
|
eventTypeId: eventType.id,
|
|
|
|
eventTypeSlug: eventType.slug,
|
|
|
|
recurringEventId,
|
|
|
|
// Added to track down the number of actual occurrences selected by the user
|
|
|
|
recurringCount: recurringDates.length,
|
|
|
|
timeZone: timeZone(),
|
|
|
|
language: i18n.language,
|
|
|
|
rescheduleUid,
|
|
|
|
user: router.query.user,
|
2022-08-26 00:48:50 +00:00
|
|
|
location: getEventLocationValue(locations, {
|
|
|
|
type: booking.locationType ? booking.locationType : selectedLocationType || "",
|
|
|
|
phone: booking.phone,
|
2022-11-05 20:10:10 +00:00
|
|
|
attendeeAddress: booking.attendeeAddress,
|
2022-08-26 00:48:50 +00:00
|
|
|
}),
|
2022-05-05 21:16:25 +00:00
|
|
|
metadata,
|
2022-12-16 19:39:41 +00:00
|
|
|
customInputs: bookingCustomInputs,
|
2022-05-05 21:16:25 +00:00
|
|
|
hasHashedBookingLink,
|
|
|
|
hashedLink,
|
2022-07-14 00:10:45 +00:00
|
|
|
smsReminderNumber:
|
2022-10-18 12:47:15 +00:00
|
|
|
selectedLocationType === LocationType.Phone
|
|
|
|
? booking.phone
|
|
|
|
: booking.smsReminderNumber || undefined,
|
2022-09-05 21:10:58 +00:00
|
|
|
ethSignature: gateState.rainbowToken,
|
2023-01-31 17:36:38 +00:00
|
|
|
guests: booking.guests?.map((guest) => guest.email),
|
2022-05-05 21:16:25 +00:00
|
|
|
}));
|
|
|
|
recurringMutation.mutate(recurringBookings);
|
|
|
|
} else {
|
|
|
|
mutation.mutate({
|
|
|
|
...booking,
|
|
|
|
start: dayjs(date).format(),
|
2022-11-28 18:14:01 +00:00
|
|
|
end: dayjs(date).add(duration, "minute").format(),
|
2022-05-05 21:16:25 +00:00
|
|
|
eventTypeId: eventType.id,
|
|
|
|
eventTypeSlug: eventType.slug,
|
|
|
|
timeZone: timeZone(),
|
|
|
|
language: i18n.language,
|
|
|
|
rescheduleUid,
|
2022-05-24 13:19:12 +00:00
|
|
|
bookingUid: router.query.bookingUid as string,
|
2022-05-05 21:16:25 +00:00
|
|
|
user: router.query.user,
|
2022-08-26 00:48:50 +00:00
|
|
|
location: getEventLocationValue(locations, {
|
|
|
|
type: (booking.locationType ? booking.locationType : selectedLocationType) || "",
|
|
|
|
phone: booking.phone,
|
2022-11-05 20:10:10 +00:00
|
|
|
attendeeAddress: booking.attendeeAddress,
|
2022-08-26 00:48:50 +00:00
|
|
|
}),
|
2022-05-05 21:16:25 +00:00
|
|
|
metadata,
|
2022-12-16 19:39:41 +00:00
|
|
|
customInputs: bookingCustomInputs,
|
2022-05-05 21:16:25 +00:00
|
|
|
hasHashedBookingLink,
|
|
|
|
hashedLink,
|
2022-07-14 00:10:45 +00:00
|
|
|
smsReminderNumber:
|
2022-10-18 12:47:15 +00:00
|
|
|
selectedLocationType === LocationType.Phone
|
|
|
|
? booking.phone
|
|
|
|
: booking.smsReminderNumber || undefined,
|
2022-09-05 21:10:58 +00:00
|
|
|
ethSignature: gateState.rainbowToken,
|
2023-01-31 17:36:38 +00:00
|
|
|
guests: booking.guests?.map((guest) => guest.email),
|
2022-05-05 21:16:25 +00:00
|
|
|
});
|
|
|
|
}
|
2021-09-14 08:45:28 +00:00
|
|
|
};
|
|
|
|
|
2022-08-05 17:08:47 +00:00
|
|
|
// Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields.
|
|
|
|
const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name;
|
2022-08-26 00:48:50 +00:00
|
|
|
const disableLocations = !!rescheduleUid;
|
2022-05-18 21:05:49 +00:00
|
|
|
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
|
|
|
|
const inputClassName =
|
2022-08-24 20:18:42 +00:00
|
|
|
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
|
2022-04-14 21:25:24 +00:00
|
|
|
|
2022-07-14 00:10:45 +00:00
|
|
|
let isSmsReminderNumberNeeded = false;
|
2022-10-18 12:47:15 +00:00
|
|
|
let isSmsReminderNumberRequired = false;
|
2022-07-14 00:10:45 +00:00
|
|
|
|
|
|
|
if (eventType.workflows.length > 0) {
|
|
|
|
eventType.workflows.forEach((workflowReference) => {
|
|
|
|
if (workflowReference.workflow.steps.length > 0) {
|
|
|
|
workflowReference.workflow.steps.forEach((step) => {
|
|
|
|
if (step.action === WorkflowActions.SMS_ATTENDEE) {
|
|
|
|
isSmsReminderNumberNeeded = true;
|
2022-10-18 12:47:15 +00:00
|
|
|
isSmsReminderNumberRequired = step.numberRequired || false;
|
2022-07-14 00:10:45 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-12-13 07:23:26 +00:00
|
|
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
2022-10-14 16:24:43 +00:00
|
|
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
2022-07-14 00:10:45 +00:00
|
|
|
|
2022-09-05 21:10:58 +00:00
|
|
|
// Define conditional gates here
|
|
|
|
const gates = [
|
|
|
|
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
2022-10-14 16:24:43 +00:00
|
|
|
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
2022-09-05 21:10:58 +00:00
|
|
|
? ("rainbow" as Gate)
|
|
|
|
: undefined,
|
|
|
|
];
|
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
return (
|
2022-10-14 16:24:43 +00:00
|
|
|
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
2021-09-24 22:11:30 +00:00
|
|
|
<Head>
|
|
|
|
<title>
|
2021-10-08 11:43:48 +00:00
|
|
|
{rescheduleUid
|
|
|
|
? t("booking_reschedule_confirmation", {
|
2022-03-15 14:39:20 +00:00
|
|
|
eventTypeTitle: eventType.title,
|
|
|
|
profileName: profile.name,
|
2021-10-08 11:43:48 +00:00
|
|
|
})
|
|
|
|
: t("booking_confirmation", {
|
2022-03-15 14:39:20 +00:00
|
|
|
eventTypeTitle: eventType.title,
|
|
|
|
profileName: profile.name,
|
2021-10-08 11:43:48 +00:00
|
|
|
})}{" "}
|
2022-11-30 21:52:56 +00:00
|
|
|
| {APP_NAME}
|
2021-09-24 22:11:30 +00:00
|
|
|
</title>
|
2023-01-23 23:08:01 +00:00
|
|
|
<link rel="icon" href="/favico.ico" />
|
2021-09-24 22:11:30 +00:00
|
|
|
</Head>
|
2022-10-14 16:24:43 +00:00
|
|
|
<BookingPageTagManager eventType={eventType} />
|
2022-03-15 14:39:20 +00:00
|
|
|
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
2022-04-08 05:33:24 +00:00
|
|
|
<main
|
2022-04-14 02:47:34 +00:00
|
|
|
className={classNames(
|
2022-04-25 04:33:00 +00:00
|
|
|
shouldAlignCentrally ? "mx-auto" : "",
|
|
|
|
isEmbed ? "" : "sm:my-24",
|
2022-12-13 07:23:26 +00:00
|
|
|
"my-0 max-w-3xl"
|
2022-04-14 02:47:34 +00:00
|
|
|
)}>
|
2022-07-26 08:27:57 +00:00
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
"main overflow-hidden",
|
2023-01-20 22:04:58 +00:00
|
|
|
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
|
2022-08-24 20:18:42 +00:00
|
|
|
"dark:border-darkgray-300 rounded-md sm:border"
|
2022-07-26 08:27:57 +00:00
|
|
|
)}>
|
2022-08-05 08:46:44 +00:00
|
|
|
<div className="sm:flex">
|
2022-12-13 07:23:26 +00:00
|
|
|
{showEventTypeDetails && (
|
|
|
|
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
|
|
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
2023-02-08 20:36:22 +00:00
|
|
|
{paymentAppData.price > 0 && (
|
2022-12-13 07:23:26 +00:00
|
|
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
2022-12-13 07:23:26 +00:00
|
|
|
<IntlProvider locale="en">
|
|
|
|
<FormattedNumber
|
2023-02-08 20:36:22 +00:00
|
|
|
value={paymentAppData.price / 100.0}
|
2022-12-13 07:23:26 +00:00
|
|
|
style="currency"
|
2023-02-08 20:36:22 +00:00
|
|
|
currency={paymentAppData?.currency?.toUpperCase()}
|
2022-12-13 07:23:26 +00:00
|
|
|
/>
|
|
|
|
</IntlProvider>
|
2022-08-29 16:01:45 +00:00
|
|
|
</p>
|
2022-12-13 07:23:26 +00:00
|
|
|
)}
|
|
|
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
|
|
|
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
2022-12-13 07:23:26 +00:00
|
|
|
<p className="-ml-2 inline-block items-center px-2">
|
|
|
|
{getEveryFreqFor({
|
|
|
|
t,
|
|
|
|
recurringEvent: eventType.recurringEvent,
|
|
|
|
recurringCount: recurringEventCount,
|
|
|
|
})}
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
2022-09-02 21:16:36 +00:00
|
|
|
<div className="text-bookinghighlight flex items-start text-sm">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
2022-12-13 07:23:26 +00:00
|
|
|
<div className="text-sm font-medium">
|
|
|
|
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
|
|
|
{!rescheduleUid &&
|
|
|
|
eventType.recurringEvent?.freq &&
|
|
|
|
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
|
|
|
return <p key={key}>{timeFormatted}</p>;
|
|
|
|
})}
|
|
|
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
|
|
|
<div className="flex">
|
|
|
|
<Tooltip
|
|
|
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
|
|
|
<p key={key}>{timeFormatted}</p>
|
|
|
|
))}>
|
|
|
|
<p className="dark:text-darkgray-600 text-sm">
|
|
|
|
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
|
|
|
</p>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
2022-09-02 21:16:36 +00:00
|
|
|
</div>
|
2022-12-13 07:23:26 +00:00
|
|
|
{booking?.startTime && rescheduleUid && (
|
|
|
|
<div>
|
|
|
|
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
|
|
|
{t("former_time")}
|
|
|
|
</p>
|
|
|
|
<p className="line-through ">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
2022-12-13 07:23:26 +00:00
|
|
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{!!eventType.seatsPerTimeSlot && (
|
|
|
|
<div className="text-bookinghighlight flex items-start text-sm">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiUser
|
2023-01-04 07:38:45 +00:00
|
|
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
2022-12-13 07:23:26 +00:00
|
|
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
|
|
|
? "text-rose-600"
|
|
|
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
|
|
|
? "text-yellow-500"
|
|
|
|
: "text-bookinghighlight"
|
|
|
|
}`}
|
|
|
|
/>
|
|
|
|
<p
|
|
|
|
className={`${
|
|
|
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
|
|
|
? "text-rose-600"
|
|
|
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
|
|
|
? "text-yellow-500"
|
|
|
|
: "text-bookinghighlight"
|
|
|
|
} mb-2 font-medium`}>
|
|
|
|
{booking
|
|
|
|
? eventType.seatsPerTimeSlot - booking.attendees.length
|
|
|
|
: eventType.seatsPerTimeSlot}{" "}
|
|
|
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</BookingDescription>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
|
2022-07-26 08:27:57 +00:00
|
|
|
<Form form={bookingForm} handleSubmit={bookEvent}>
|
|
|
|
<div className="mb-4">
|
|
|
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{t("your_name")}
|
|
|
|
</label>
|
|
|
|
<div className="mt-1">
|
|
|
|
<input
|
|
|
|
{...bookingForm.register("name", { required: true })}
|
|
|
|
type="text"
|
|
|
|
name="name"
|
|
|
|
id="name"
|
|
|
|
required
|
|
|
|
className={inputClassName}
|
|
|
|
placeholder={t("example_name")}
|
|
|
|
disabled={disableInput}
|
|
|
|
/>
|
2022-05-05 21:16:25 +00:00
|
|
|
</div>
|
2022-07-26 08:27:57 +00:00
|
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{t("email_address")}
|
|
|
|
</label>
|
|
|
|
<div className="mt-1">
|
|
|
|
<EmailInput
|
|
|
|
{...bookingForm.register("email")}
|
|
|
|
required
|
|
|
|
className={classNames(
|
|
|
|
inputClassName,
|
2022-09-16 14:29:40 +00:00
|
|
|
bookingForm.formState.errors.email && "!focus:ring-red-700 !border-red-700"
|
2022-07-26 08:27:57 +00:00
|
|
|
)}
|
|
|
|
placeholder="you@example.com"
|
|
|
|
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
|
|
|
disabled={disableInput}
|
|
|
|
/>
|
|
|
|
{bookingForm.formState.errors.email && (
|
|
|
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
2022-07-26 08:27:57 +00:00
|
|
|
<p>{t("email_validation_error")}</p>
|
2022-05-05 21:16:25 +00:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-10-18 14:41:14 +00:00
|
|
|
<>
|
|
|
|
{rescheduleUid ? (
|
|
|
|
<div className="mb-4">
|
|
|
|
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{t("location")}
|
|
|
|
</span>
|
2022-10-17 13:47:11 +00:00
|
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
|
|
{getHumanReadableLocationValue(booking?.location, t)}
|
|
|
|
</p>
|
2022-10-18 14:41:14 +00:00
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
locations.length > 1 && (
|
|
|
|
<div className="mb-4">
|
|
|
|
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{t("location")}
|
|
|
|
</span>
|
|
|
|
{locations.map((location, i) => {
|
|
|
|
const locationString = locationKeyToString(location);
|
|
|
|
if (!selectedLocationType) {
|
|
|
|
bookingForm.setValue("locationType", locations[0].type);
|
|
|
|
}
|
|
|
|
if (typeof locationString !== "string") {
|
|
|
|
// It's possible that location app got uninstalled
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<label key={i} className="block">
|
|
|
|
<input
|
|
|
|
type="radio"
|
|
|
|
disabled={!!disableLocations}
|
|
|
|
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
|
|
|
{...bookingForm.register("locationType", { required: true })}
|
|
|
|
value={location.type}
|
|
|
|
defaultChecked={i === 0}
|
|
|
|
/>
|
2023-01-04 07:38:45 +00:00
|
|
|
<span className="text-sm ltr:ml-2 ltr:mr-2 rtl:ml-2 dark:text-white">
|
2023-01-04 15:31:50 +00:00
|
|
|
{t(locationKeyToString(location) ?? "")}
|
2022-10-18 14:41:14 +00:00
|
|
|
</span>
|
|
|
|
</label>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
</>
|
2022-08-26 00:48:50 +00:00
|
|
|
{/* TODO: Change name and id ="phone" to something generic */}
|
2022-11-23 22:30:54 +00:00
|
|
|
{AttendeeInput && !disableInput && (
|
2021-09-14 08:45:28 +00:00
|
|
|
<div className="mb-4">
|
|
|
|
<label
|
2022-11-05 20:10:10 +00:00
|
|
|
htmlFor={
|
|
|
|
selectedLocationType === LocationType.Phone
|
|
|
|
? "phone"
|
|
|
|
: selectedLocationType === LocationType.AttendeeInPerson
|
|
|
|
? "attendeeAddress"
|
|
|
|
: ""
|
|
|
|
}
|
2021-10-12 08:29:12 +00:00
|
|
|
className="block text-sm font-medium text-gray-700 dark:text-white">
|
2022-11-05 20:10:10 +00:00
|
|
|
{selectedLocationType === LocationType.Phone
|
|
|
|
? t("phone_number")
|
|
|
|
: selectedLocationType === LocationType.AttendeeInPerson
|
2023-01-04 15:31:50 +00:00
|
|
|
? t("address")
|
2022-11-05 20:10:10 +00:00
|
|
|
: ""}
|
2021-09-14 08:45:28 +00:00
|
|
|
</label>
|
|
|
|
<div className="mt-1">
|
2022-08-26 00:48:50 +00:00
|
|
|
<AttendeeInput<BookingFormValues>
|
2022-07-26 08:27:57 +00:00
|
|
|
control={bookingForm.control}
|
2022-11-05 20:10:10 +00:00
|
|
|
bookingForm={bookingForm}
|
|
|
|
name={
|
|
|
|
selectedLocationType === LocationType.Phone
|
|
|
|
? "phone"
|
|
|
|
: selectedLocationType === LocationType.AttendeeInPerson
|
|
|
|
? "attendeeAddress"
|
|
|
|
: ""
|
|
|
|
}
|
2022-08-26 00:48:50 +00:00
|
|
|
placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")}
|
2022-11-05 20:10:10 +00:00
|
|
|
id={
|
|
|
|
selectedLocationType === LocationType.Phone
|
|
|
|
? "phone"
|
|
|
|
: selectedLocationType === LocationType.AttendeeInPerson
|
|
|
|
? "attendeeAddress"
|
|
|
|
: ""
|
|
|
|
}
|
2021-09-14 08:45:28 +00:00
|
|
|
required
|
|
|
|
/>
|
|
|
|
</div>
|
2022-07-26 08:27:57 +00:00
|
|
|
{bookingForm.formState.errors.phone && (
|
|
|
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
2022-07-26 08:27:57 +00:00
|
|
|
<p>{t("invalid_number")}</p>
|
|
|
|
</div>
|
|
|
|
)}
|
2021-09-14 08:45:28 +00:00
|
|
|
</div>
|
2022-07-26 08:27:57 +00:00
|
|
|
)}
|
|
|
|
{eventType.customInputs
|
|
|
|
.sort((a, b) => a.id - b.id)
|
|
|
|
.map((input) => (
|
|
|
|
<div className="mb-4" key={input.id}>
|
|
|
|
{input.type !== EventTypeCustomInputType.BOOL && (
|
|
|
|
<label
|
|
|
|
htmlFor={"custom_" + input.id}
|
2022-12-12 13:18:04 +00:00
|
|
|
className={classNames(
|
|
|
|
"mb-1 block text-sm font-medium text-gray-700 transition-colors dark:text-white",
|
|
|
|
bookingForm.formState.errors.customInputs?.[input.id] && "!text-red-700"
|
|
|
|
)}>
|
|
|
|
{input.label} {input.required && <span className="text-red-700">*</span>}
|
2021-09-14 08:45:28 +00:00
|
|
|
</label>
|
2022-07-26 08:27:57 +00:00
|
|
|
)}
|
|
|
|
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
|
|
|
<textarea
|
|
|
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
|
|
|
required: input.required,
|
|
|
|
})}
|
2022-08-29 13:04:22 +00:00
|
|
|
required={input.required}
|
2022-07-26 08:27:57 +00:00
|
|
|
id={"custom_" + input.id}
|
|
|
|
rows={3}
|
|
|
|
className={inputClassName}
|
|
|
|
placeholder={input.placeholder}
|
|
|
|
disabled={disabledExceptForOwner}
|
2022-03-03 09:57:59 +00:00
|
|
|
/>
|
2022-07-06 19:01:16 +00:00
|
|
|
)}
|
2022-07-26 08:27:57 +00:00
|
|
|
{input.type === EventTypeCustomInputType.TEXT && (
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
|
|
|
required: input.required,
|
|
|
|
})}
|
2022-08-29 13:04:22 +00:00
|
|
|
required={input.required}
|
2022-07-26 08:27:57 +00:00
|
|
|
id={"custom_" + input.id}
|
|
|
|
className={inputClassName}
|
|
|
|
placeholder={input.placeholder}
|
|
|
|
disabled={disabledExceptForOwner}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{input.type === EventTypeCustomInputType.NUMBER && (
|
|
|
|
<input
|
|
|
|
type="number"
|
|
|
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
|
|
|
required: input.required,
|
|
|
|
})}
|
2022-08-29 13:04:22 +00:00
|
|
|
required={input.required}
|
2022-07-26 08:27:57 +00:00
|
|
|
id={"custom_" + input.id}
|
|
|
|
className={inputClassName}
|
|
|
|
placeholder=""
|
|
|
|
disabled={disabledExceptForOwner}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{input.type === EventTypeCustomInputType.BOOL && (
|
2022-10-06 14:33:57 +00:00
|
|
|
<div className="my-6">
|
|
|
|
<div className="flex">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
|
|
|
required: input.required,
|
|
|
|
})}
|
|
|
|
required={input.required}
|
|
|
|
id={"custom_" + input.id}
|
|
|
|
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
|
|
|
|
placeholder=""
|
|
|
|
disabled={disabledExceptForOwner}
|
|
|
|
/>
|
|
|
|
<label
|
|
|
|
htmlFor={"custom_" + input.id}
|
|
|
|
className="-mt-px block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{input.label}
|
|
|
|
</label>
|
|
|
|
</div>
|
2021-09-22 11:04:32 +00:00
|
|
|
</div>
|
|
|
|
)}
|
2022-12-01 21:53:52 +00:00
|
|
|
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
2022-12-12 13:18:04 +00:00
|
|
|
<div className="flex">
|
|
|
|
<Group
|
2022-12-28 19:43:18 +00:00
|
|
|
name={`customInputs.${input.id}`}
|
2022-12-12 13:18:04 +00:00
|
|
|
required={input.required}
|
|
|
|
onValueChange={(e) => {
|
|
|
|
bookingForm.setValue(`customInputs.${input.id}`, e);
|
|
|
|
}}>
|
|
|
|
<>
|
|
|
|
{input.options.map((option, i) => (
|
|
|
|
<RadioField
|
|
|
|
label={option.label}
|
2022-12-30 17:52:03 +00:00
|
|
|
key={`option.${input.id}.${i}.radio`}
|
2022-12-12 13:18:04 +00:00
|
|
|
value={option.label}
|
2022-12-30 17:52:03 +00:00
|
|
|
id={`option.${input.id}.${i}.radio`}
|
2022-12-12 13:18:04 +00:00
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
{bookingForm.formState.errors.customInputs?.[input.id] && (
|
|
|
|
<div className="mt-px flex items-center text-xs text-red-700 ">
|
|
|
|
<p>{t("required")}</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Group>
|
2022-12-01 21:53:52 +00:00
|
|
|
</div>
|
|
|
|
)}
|
2022-12-16 19:39:41 +00:00
|
|
|
{input.type === EventTypeCustomInputType.PHONE && (
|
|
|
|
<div>
|
|
|
|
<PhoneInput<BookingFormValues>
|
|
|
|
name={`customInputs.${input.id}`}
|
|
|
|
control={bookingForm.control}
|
|
|
|
placeholder={t("enter_phone_number")}
|
|
|
|
id={`customInputs.${input.id}`}
|
|
|
|
required={input.required}
|
|
|
|
/>
|
|
|
|
{bookingForm.formState.errors?.customInputs?.[input.id] && (
|
|
|
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
2022-12-16 19:39:41 +00:00
|
|
|
<p>{t("invalid_number")}</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
2021-09-22 11:04:32 +00:00
|
|
|
</div>
|
2022-07-26 08:27:57 +00:00
|
|
|
))}
|
2023-01-31 17:36:38 +00:00
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
|
2021-09-14 08:45:28 +00:00
|
|
|
<div className="mb-4">
|
|
|
|
<label
|
2022-07-26 08:27:57 +00:00
|
|
|
htmlFor="smsReminderNumber"
|
|
|
|
className="block text-sm font-medium text-gray-700 dark:text-white">
|
2022-10-27 09:53:13 +00:00
|
|
|
{t("number_sms_notifications")}
|
2021-09-14 08:45:28 +00:00
|
|
|
</label>
|
2022-07-26 08:27:57 +00:00
|
|
|
<div className="mt-1">
|
|
|
|
<PhoneInput<BookingFormValues>
|
|
|
|
control={bookingForm.control}
|
|
|
|
name="smsReminderNumber"
|
|
|
|
placeholder={t("enter_phone_number")}
|
|
|
|
id="smsReminderNumber"
|
2022-10-18 12:47:15 +00:00
|
|
|
required={isSmsReminderNumberRequired}
|
2022-05-30 19:40:29 +00:00
|
|
|
/>
|
2022-07-26 08:27:57 +00:00
|
|
|
</div>
|
|
|
|
{bookingForm.formState.errors.smsReminderNumber && (
|
|
|
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
2022-07-26 08:27:57 +00:00
|
|
|
<p>{t("invalid_number")}</p>
|
|
|
|
</div>
|
2022-05-30 19:40:29 +00:00
|
|
|
)}
|
2021-09-14 08:45:28 +00:00
|
|
|
</div>
|
|
|
|
)}
|
2022-07-26 08:27:57 +00:00
|
|
|
<div className="mb-4">
|
|
|
|
<label
|
|
|
|
htmlFor="notes"
|
|
|
|
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
|
|
|
|
</label>
|
|
|
|
{rescheduleUid ? (
|
|
|
|
<textarea
|
|
|
|
{...bookingForm.register("rescheduleReason")}
|
|
|
|
id="rescheduleReason"
|
|
|
|
name="rescheduleReason"
|
|
|
|
rows={3}
|
|
|
|
className={inputClassName}
|
|
|
|
placeholder={t("reschedule_placeholder")}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<textarea
|
|
|
|
{...bookingForm.register("notes")}
|
2022-11-28 18:14:01 +00:00
|
|
|
required={!!eventType.metadata?.additionalNotesRequired}
|
2022-07-26 08:27:57 +00:00
|
|
|
id="notes"
|
|
|
|
name="notes"
|
|
|
|
rows={3}
|
|
|
|
className={inputClassName}
|
|
|
|
placeholder={t("share_additional_notes")}
|
|
|
|
disabled={disabledExceptForOwner}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-01-31 17:36:38 +00:00
|
|
|
{!eventType.disableGuests && guestsField.fields.length ? (
|
|
|
|
<div className="mb-4">
|
|
|
|
<div>
|
|
|
|
<label
|
|
|
|
htmlFor="guests"
|
|
|
|
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
|
|
|
{t("guests")}
|
|
|
|
</label>
|
|
|
|
<ul>
|
|
|
|
{guestsField.fields.map((field, index) => (
|
|
|
|
<li key={field.id}>
|
|
|
|
<EmailField
|
|
|
|
{...bookingForm.register(`guests.${index}.email` as const)}
|
|
|
|
className={classNames(
|
|
|
|
inputClassName,
|
|
|
|
bookingForm.formState.errors.guests?.[index] &&
|
|
|
|
"!focus:ring-red-700 !border-red-700",
|
|
|
|
"border-r-0"
|
|
|
|
)}
|
|
|
|
addOnClassname={classNames(
|
|
|
|
"border-gray-300 border block border-l-0 disabled:bg-gray-200 disabled:hover:cursor-not-allowed bg-transparent disabled:text-gray-500 dark:border-darkgray-300 ",
|
|
|
|
bookingForm.formState.errors.guests?.[index] &&
|
|
|
|
"!focus:ring-red-700 !border-red-700"
|
|
|
|
)}
|
|
|
|
placeholder="guest@example.com"
|
|
|
|
label={<></>}
|
|
|
|
required
|
|
|
|
addOnSuffix={
|
|
|
|
<Tooltip content="Remove guest">
|
|
|
|
<button
|
|
|
|
className="m-1 disabled:hover:cursor-not-allowed"
|
|
|
|
type="button"
|
|
|
|
onClick={() => guestsField.remove(index)}>
|
|
|
|
<FiX className="text-gray-600" />
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
{bookingForm.formState.errors.guests?.[index] && (
|
|
|
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
|
|
|
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
|
|
|
<p className="text-red-700">
|
|
|
|
{bookingForm.formState.errors.guests?.[index]?.message}
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
<Button
|
|
|
|
type="button"
|
|
|
|
color="minimal"
|
|
|
|
StartIcon={FiUserPlus}
|
|
|
|
className="my-2.5"
|
|
|
|
// className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"
|
|
|
|
onClick={() => guestsField.append({ email: "" })}>
|
|
|
|
{t("add_another")}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<></>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!eventType.disableGuests && !guestsField.fields.length && (
|
|
|
|
<Button
|
|
|
|
color="minimal"
|
|
|
|
variant="button"
|
|
|
|
StartIcon={FiUserPlus}
|
|
|
|
onClick={() => {
|
|
|
|
guestsField.append({ email: "" });
|
|
|
|
}}
|
|
|
|
className="mr-auto">
|
|
|
|
{t("additional_guests")}
|
|
|
|
</Button>
|
|
|
|
)}
|
2022-07-26 08:27:57 +00:00
|
|
|
|
2022-09-02 21:16:36 +00:00
|
|
|
<div className="flex justify-end space-x-2 rtl:space-x-reverse">
|
2023-01-17 12:21:05 +00:00
|
|
|
<Button color="minimal" type="button" onClick={() => router.back()}>
|
2022-09-02 21:16:36 +00:00
|
|
|
{t("cancel")}
|
|
|
|
</Button>
|
2022-07-26 08:27:57 +00:00
|
|
|
<Button
|
|
|
|
type="submit"
|
|
|
|
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
|
|
|
loading={mutation.isLoading || recurringMutation.isLoading}>
|
|
|
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</Form>
|
|
|
|
{(mutation.isError || recurringMutation.isError) && (
|
|
|
|
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
|
|
|
)}
|
2021-09-14 08:45:28 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-07-26 08:27:57 +00:00
|
|
|
</div>
|
2021-09-24 22:11:30 +00:00
|
|
|
</main>
|
2022-09-05 21:10:58 +00:00
|
|
|
</Gates>
|
2021-09-14 08:45:28 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default BookingPage;
|
2022-06-10 18:38:46 +00:00
|
|
|
|
|
|
|
function ErrorMessage({ error }: { error: unknown }) {
|
|
|
|
const { t } = useLocale();
|
|
|
|
const { query: { rescheduleUid } = {} } = useRouter();
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
|
|
|
<div className="flex">
|
|
|
|
<div className="flex-shrink-0">
|
2023-01-23 23:08:01 +00:00
|
|
|
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
2022-06-10 18:38:46 +00:00
|
|
|
</div>
|
|
|
|
<div className="ltr:ml-3 rtl:mr-3">
|
|
|
|
<p className="text-sm text-yellow-700">
|
|
|
|
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
2022-11-22 03:17:54 +00:00
|
|
|
{error instanceof HttpError || error instanceof Error ? t(error.message) : "Unknown error"}
|
2022-06-10 18:38:46 +00:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|