diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 1432820ba6..17fbba2e8d 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -6,9 +6,8 @@ import { useSession } from "next-auth/react"; import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect, useMemo, useReducer, useState } from "react"; -import { Controller, useForm, useWatch } from "react-hook-form"; +import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; -import { ReactMultiEmail } from "react-multi-email"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; @@ -40,15 +39,26 @@ import { HttpError } from "@calcom/lib/http-error"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import slugify from "@calcom/lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; -import { AddressInput, Button, EmailInput, Form, PhoneInput, Tooltip, Group, RadioField } from "@calcom/ui"; import { - FiAlertTriangle, + AddressInput, + Button, + EmailField, + EmailInput, + Form, + Group, + PhoneInput, + RadioField, + Tooltip, +} from "@calcom/ui"; +import { + FiUserPlus, FiCalendar, - FiCreditCard, + FiX, FiInfo, + FiCreditCard, FiRefreshCw, FiUser, - FiUserPlus, + FiAlertTriangle, } from "@calcom/ui/components/icon"; import { asStringOrNull } from "@lib/asStringOrNull"; @@ -73,7 +83,7 @@ type BookingFormValues = { email: string; notes?: string; locationType?: EventLocationType["type"]; - guests?: string[]; + guests?: { email: string }[]; address?: string; attendeeAddress?: string; phone?: string; @@ -192,7 +202,9 @@ const BookingPage = ({ const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id; const guestListEmails = !isDynamicGroupBooking - ? booking?.attendees.slice(1).map((attendee) => attendee.email) + ? booking?.attendees.slice(1).map((attendee) => { + return { email: attendee.email }; + }) : []; // There should only exists one default userData variable for primaryAttendee. @@ -211,7 +223,9 @@ const BookingPage = ({ name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "", email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "", notes: (router.query.notes as string) || "", - guests: ensureArray(router.query.guest) as string[], + guests: ensureArray(router.query.guest).map((guest) => { + return { email: guest as string }; + }), customInputs: eventType.customInputs.reduce( (customInputs, input) => ({ ...customInputs, @@ -253,6 +267,7 @@ const BookingPage = ({ .object({ name: z.string().min(1), email: z.string().trim().email(), + guests: z.array(z.object({ email: z.string().email() })).optional(), phone: z .string() .refine((val) => isValidPhoneNumber(val)) @@ -271,6 +286,10 @@ const BookingPage = ({ defaultValues: defaultValues(), resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema }); + const guestsField = useFieldArray({ + name: "guests", + control: bookingForm.control, + }); const selectedLocationType = useWatch({ control: bookingForm.control, @@ -309,6 +328,7 @@ const BookingPage = ({ } const bookEvent = (booking: BookingFormValues) => { + bookingForm.clearErrors(); 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] : "", @@ -376,6 +396,29 @@ const BookingPage = ({ } } + // 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; + if (recurringDates.length) { // Identify set of bookings to one intance of recurring event to support batch changes const recurringEventId = uuidv4(); @@ -406,6 +449,7 @@ const BookingPage = ({ ? booking.phone : booking.smsReminderNumber || undefined, ethSignature: gateState.rainbowToken, + guests: booking.guests?.map((guest) => guest.email), })); recurringMutation.mutate(recurringBookings); } else { @@ -434,6 +478,7 @@ const BookingPage = ({ ? booking.phone : booking.smsReminderNumber || undefined, ethSignature: gateState.rainbowToken, + guests: booking.guests?.map((guest) => guest.email), }); } }; @@ -850,60 +895,7 @@ const BookingPage = ({ )} ))} - {!eventType.disableGuests && guestToggle && ( -
+ {bookingForm.formState.errors.guests?.[index]?.message} +
+