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-08-15 19:52:01 +00:00
import { SchedulingType } 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-09-05 21:10:58 +00:00
import { useEffect , useMemo , useState , useReducer } from "react" ;
2021-12-03 10:15:20 +00:00
import { Controller , useForm , useWatch } from "react-hook-form" ;
2021-09-22 19:52:38 +00:00
import { FormattedNumber , IntlProvider } from "react-intl" ;
2021-09-14 08:45:28 +00:00
import { ReactMultiEmail } from "react-multi-email" ;
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" ;
2022-08-26 00:48:50 +00:00
import {
locationKeyToString ,
getEventLocationValue ,
getEventLocationType ,
EventLocationType ,
} 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" ;
2022-08-26 00:48:50 +00:00
import { LocationObject , 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 ,
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-10-14 16:24:43 +00:00
import getStripeAppData from "@calcom/lib/getStripeAppData" ;
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" ;
2022-07-28 19:58:26 +00:00
import { collectPageParameters , telemetryEventTypes , useTelemetry } from "@calcom/lib/telemetry" ;
2022-07-27 02:24:00 +00:00
import { Icon } from "@calcom/ui/Icon" ;
2022-05-05 21:16:25 +00:00
import { Tooltip } from "@calcom/ui/Tooltip" ;
2022-07-28 19:58:26 +00:00
import PhoneInput from "@calcom/ui/form/PhoneInputLazy" ;
2022-03-16 23:36:43 +00:00
import { EmailInput , Form } from "@calcom/ui/form/fields" ;
2022-09-16 14:29:40 +00:00
import { Button } from "@calcom/ui/v2" ;
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" ;
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-12-03 10:15:20 +00:00
import slugify from "@lib/slugify" ;
2021-09-22 19:52:38 +00:00
2022-09-05 21:10:58 +00:00
import Gates , { Gate , GateState } from "@components/Gates" ;
2022-10-12 08:39:14 +00:00
import BookingDescription from "@components/booking/BookingDescription" ;
2022-08-15 19:52:01 +00:00
import { UserAvatars } from "@components/booking/UserAvatars" ;
2022-08-29 16:01:45 +00:00
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML" ;
2021-09-22 19:52:38 +00:00
2021-09-22 18:36:13 +00:00
import { BookPageProps } from "../../../pages/[user]/book" ;
2022-04-28 15:44:26 +00:00
import { HashLinkPageProps } from "../../../pages/d/[link]/book" ;
2021-09-22 18:36:13 +00:00
import { 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" ] ;
2022-02-01 21:48:40 +00:00
guests? : string [ ] ;
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-04-06 17:20:30 +00:00
} : BookingPageProps ) = > {
2021-10-25 13:05:21 +00:00
const { t , i18n } = useLocale ( ) ;
2022-04-08 05:33:24 +00:00
const isEmbed = useIsEmbed ( ) ;
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 ,
} ) ,
{ }
) ;
2022-10-14 16:24:43 +00:00
const stripeAppData = getStripeAppData ( eventType ) ;
2022-04-08 05:33:24 +00:00
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-05-06 14:15:05 +00:00
const { id , attendees , paymentUid } = responseData ;
2021-12-03 10:15:20 +00:00
if ( paymentUid ) {
return await router . push (
createPaymentLink ( {
paymentUid ,
date ,
name : attendees [ 0 ] . name ,
2022-05-25 01:29:29 +00:00
email : attendees [ 0 ] . email ,
2021-12-03 10:15:20 +00:00
absolute : false ,
} )
) ;
}
return router . push ( {
pathname : "/success" ,
query : {
date ,
2022-03-15 14:39:20 +00:00
type : eventType . id ,
2022-04-06 17:20:30 +00:00
eventSlug : eventType.slug ,
2022-10-16 01:38:39 +00:00
username : profile.slug ,
2021-12-03 10:15:20 +00:00
reschedule : ! ! rescheduleUid ,
name : attendees [ 0 ] . name ,
email : attendees [ 0 ] . email ,
2022-08-26 00:48:50 +00:00
location : responseData.location ,
2022-04-08 16:50:10 +00:00
eventName : profile.eventName || "" ,
2022-05-06 14:15:05 +00:00
bookingId : id ,
2022-06-08 14:02:09 +00:00
isSuccessBookingPage : true ,
2021-12-03 10:15:20 +00:00
} ,
} ) ;
} ,
} ) ;
2022-05-05 21:16:25 +00:00
const recurringMutation = useMutation ( createRecurringBooking , {
onSuccess : async ( responseData = [ ] ) = > {
2022-05-11 07:59:49 +00:00
const { attendees = [ ] , id , recurringEventId } = responseData [ 0 ] || { } ;
2022-05-05 21:16:25 +00:00
const location = ( function humanReadableLocation ( location ) {
if ( ! location ) {
return ;
}
if ( location . includes ( "integration" ) ) {
return t ( "web_conferencing_details_to_follow" ) ;
}
return location ;
} ) ( responseData [ 0 ] . location ) ;
return router . push ( {
pathname : "/success" ,
query : {
date ,
type : eventType . id ,
eventSlug : eventType.slug ,
recur : recurringEventId ,
2022-10-16 01:38:39 +00:00
username : profile.slug ,
2022-05-05 21:16:25 +00:00
reschedule : ! ! rescheduleUid ,
name : attendees [ 0 ] . name ,
email : attendees [ 0 ] . email ,
location ,
eventName : profile.eventName || "" ,
2022-05-11 07:59:49 +00:00
bookingId : id ,
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
? booking ? . attendees . slice ( 1 ) . map ( ( attendee ) = > attendee . email )
: [ ] ;
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 ) || "" ,
guests : ensureArray ( router . query . 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 ) ,
email : z.string ( ) . email ( ) ,
2022-07-06 19:01:16 +00:00
phone : z
. string ( )
. refine ( ( val ) = > isValidPhoneNumber ( val ) )
. optional ( ) ,
2022-07-14 00:10:45 +00:00
smsReminderNumber : z
. string ( )
. refine ( ( val ) = > isValidPhoneNumber ( val ) )
. optional ( ) ,
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
} ) ;
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 =
selectedLocation ? . attendeeInputType === "text"
? "input"
: selectedLocation ? . attendeeInputType === "phone"
? PhoneInput
: 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 ) = > {
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-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 ( ) ,
end : dayjs ( recurringDate ) . add ( eventType . length , "minute" ) . format ( ) ,
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-05-05 21:16:25 +00:00
metadata ,
customInputs : Object.keys ( booking . customInputs || { } ) . map ( ( inputId ) = > ( {
2022-05-17 20:43:27 +00:00
label : eventType.customInputs.find ( ( input ) = > input . id === parseInt ( inputId ) ) ? . label || "" ,
value : booking.customInputs && inputId in booking . customInputs ? booking . customInputs [ inputId ] : "" ,
2022-05-05 21:16:25 +00:00
} ) ) ,
hasHashedBookingLink ,
hashedLink ,
2022-07-14 00:10:45 +00:00
smsReminderNumber :
2022-08-26 00:48:50 +00:00
selectedLocationType === LocationType . Phone ? booking.phone : booking.smsReminderNumber ,
2022-09-05 21:10:58 +00:00
ethSignature : gateState.rainbowToken ,
2022-05-05 21:16:25 +00:00
} ) ) ;
recurringMutation . mutate ( recurringBookings ) ;
} else {
mutation . mutate ( {
. . . booking ,
start : dayjs ( date ) . format ( ) ,
end : dayjs ( date ) . add ( eventType . length , "minute" ) . format ( ) ,
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-05-05 21:16:25 +00:00
metadata ,
customInputs : Object.keys ( booking . customInputs || { } ) . map ( ( inputId ) = > ( {
2022-05-17 20:43:27 +00:00
label : eventType.customInputs.find ( ( input ) = > input . id === parseInt ( inputId ) ) ? . label || "" ,
value : booking.customInputs && inputId in booking . customInputs ? booking . customInputs [ inputId ] : "" ,
2022-05-05 21:16:25 +00:00
} ) ) ,
hasHashedBookingLink ,
hashedLink ,
2022-07-14 00:10:45 +00:00
smsReminderNumber :
2022-08-26 00:48:50 +00:00
selectedLocationType === LocationType . Phone ? booking.phone : booking.smsReminderNumber ,
2022-09-05 21:10:58 +00:00
ethSignature : gateState.rainbowToken ,
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 ;
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 ;
return ;
}
} ) ;
}
} ) ;
}
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
} ) } { " " }
| Cal . com
2021-09-24 22:11:30 +00:00
< / title >
< link rel = "icon" href = "/favicon.ico" / >
< / 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" ,
"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" ,
2022-08-30 08:56:30 +00:00
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white" ,
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-09-02 21:16:36 +00:00
< 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" >
2022-10-12 08:39:14 +00:00
< BookingDescription isBookingPage profile = { profile } eventType = { eventType } >
2022-10-14 16:24:43 +00:00
{ stripeAppData . price > 0 && (
2022-08-29 16:01:45 +00:00
< p className = "text-bookinglight -ml-2 px-2 text-sm " >
< Icon.FiCreditCard className = "mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" / >
< IntlProvider locale = "en" >
< FormattedNumber
2022-10-14 16:24:43 +00:00
value = { stripeAppData . price / 100.0 }
2022-08-29 16:01:45 +00:00
style = "currency"
2022-10-14 16:24:43 +00:00
currency = { stripeAppData . currency . toUpperCase ( ) }
2022-08-29 16:01:45 +00:00
/ >
< / IntlProvider >
2022-07-26 08:27:57 +00:00
< / p >
2022-08-29 16:01:45 +00:00
) }
{ ! rescheduleUid && eventType . recurringEvent ? . freq && recurringEventCount && (
< div className = "items-start text-sm font-medium text-gray-600 dark:text-white" >
< Icon.FiRefreshCw className = "mr-[10px] ml-[2px] inline-block h-4 w-4" / >
< p className = "-ml-2 inline-block items-center px-2" >
{ getEveryFreqFor ( {
t ,
recurringEvent : eventType.recurringEvent ,
recurringCount : recurringEventCount ,
} ) }
< / p >
< / div >
) }
< div className = "text-bookinghighlight flex items-start text-sm" >
< Icon.FiCalendar className = "mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" / >
< div className = "text-sm font-medium" >
{ ( rescheduleUid || ! eventType . recurringEvent ? . freq ) &&
parseDate ( dayjs ( date ) . tz ( timeZone ( ) ) , i18n ) }
{ ! rescheduleUid &&
eventType . recurringEvent ? . freq &&
recurringStrings . slice ( 0 , 5 ) . map ( ( aDate , key ) = > < p key = { key } > { aDate } < / p > ) }
{ ! rescheduleUid && eventType . recurringEvent ? . freq && recurringStrings . length > 5 && (
< div className = "flex" >
< Tooltip
content = { recurringStrings . slice ( 5 ) . map ( ( aDate , key ) = > (
< p key = { key } > { aDate } < / p >
) ) } >
2022-09-09 19:01:37 +00:00
< p className = "dark:text-darkgray-600 text-sm" >
2022-08-29 16:01:45 +00:00
{ t ( "plus_more" , { count : recurringStrings.length - 5 } ) }
< / p >
< / Tooltip >
< / div >
) }
< / div >
2022-07-26 08:27:57 +00:00
< / div >
2022-08-29 16:01:45 +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 " >
< Icon.FiCalendar className = "mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" / >
{ typeof booking . startTime === "string" && parseDate ( dayjs ( booking . startTime ) , i18n ) }
< / p >
< / div >
) }
2022-09-02 21:16:36 +00:00
{ ! ! eventType . seatsPerTimeSlot && (
< div className = "text-bookinghighlight flex items-start text-sm" >
< Icon.FiUser className = "mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" / >
< 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 } { " " }
/ { e v e n t T y p e . s e a t s P e r T i m e S l o t } { t ( " s e a t s _ a v a i l a b l e " ) }
< / p >
< / div >
) }
2022-10-12 08:39:14 +00:00
< / BookingDescription >
2022-07-26 08:27:57 +00:00
< / div >
2022-08-05 08:46:44 +00:00
< div className = "p-6 sm:w-1/2" >
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 " >
2022-08-03 16:01:29 +00:00
< Icon.FiInfo className = "mr-2 h-3 w-3" / >
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-07-26 08:27:57 +00:00
{ locations . length > 1 && (
2021-09-14 08:45:28 +00:00
< div className = "mb-4" >
2022-07-26 08:27:57 +00:00
< span className = "block text-sm font-medium text-gray-700 dark:text-white" >
{ t ( "location" ) }
< / span >
2022-08-26 00:48:50 +00:00
{ locations . map ( ( location , i ) = > {
const locationString = locationKeyToString ( location ) ;
// TODO: Right now selectedLocationType isn't send by getSSP. Once that's available defaultChecked should work and show the location in the original booking
const defaultChecked = rescheduleUid ? selectedLocationType === location.type : i === 0 ;
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 }
2022-09-02 21:16:36 +00:00
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"
2022-08-26 00:48:50 +00:00
{ . . . bookingForm . register ( "locationType" , { required : true } ) }
value = { location . type }
defaultChecked = { defaultChecked }
/ >
2022-09-02 21:16:36 +00:00
< span className = "text-sm ltr:ml-2 rtl:mr-2 dark:text-white" >
2022-08-26 00:48:50 +00:00
{ locationKeyToString ( location ) }
< / span >
< / label >
) ;
} ) }
2021-09-14 08:45:28 +00:00
< / div >
2022-07-26 08:27:57 +00:00
) }
2022-08-26 00:48:50 +00:00
{ /* TODO: Change name and id ="phone" to something generic */ }
{ AttendeeInput && (
2021-09-14 08:45:28 +00:00
< div className = "mb-4" >
< label
2022-07-26 08:27:57 +00:00
htmlFor = "phone"
2021-10-12 08:29:12 +00:00
className = "block text-sm font-medium text-gray-700 dark:text-white" >
2022-07-26 08:27:57 +00:00
{ t ( "phone_number" ) }
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 }
name = "phone"
2022-08-26 00:48:50 +00:00
placeholder = { t ( selectedLocation ? . attendeeInputPlaceholder || "" ) }
2022-07-26 08:27:57 +00:00
id = "phone"
2021-09-14 08:45:28 +00:00
required
2022-04-14 21:25:24 +00:00
disabled = { disableInput }
2021-09-14 08:45:28 +00:00
/ >
< / 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 " >
2022-08-03 16:01:29 +00:00
< Icon.FiInfo className = "mr-2 h-3 w-3" / >
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 }
className = "mb-1 block text-sm font-medium text-gray-700 dark:text-white" >
{ input . label }
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 >
) }
< / div >
2022-07-26 08:27:57 +00:00
) ) }
2022-09-20 12:37:34 +00:00
{ ! eventType . disableGuests && guestToggle && (
2022-07-26 08:27:57 +00:00
< div className = "mb-4" >
2022-09-20 12:37:34 +00:00
< div >
< label
htmlFor = "guests"
className = "mb-1 block text-sm font-medium text-gray-700 dark:text-white" >
{ t ( "guests" ) }
< / label >
{ ! disableInput && (
< Controller
control = { bookingForm . control }
name = "guests"
render = { ( { field : { onChange , value } } ) = > (
< ReactMultiEmail
className = "relative"
placeholder = { < span className = "dark:text-darkgray-600" > guest @example . com < / span > }
emails = { value }
onChange = { onChange }
getLabel = { (
email : string ,
index : number ,
removeEmail : ( index : number ) = > void
) = > {
return (
< div data - tag key = { index } className = "cursor-pointer" >
{ email }
{ ! disableInput && (
< span data - tag - handle onClick = { ( ) = > removeEmail ( index ) } >
×
< / span >
) }
< / div >
) ;
} }
/ >
) }
/ >
) }
{ /* Custom code when guest emails should not be editable */ }
{ disableInput && guestListEmails && guestListEmails . length > 0 && (
< div data - tag className = "react-multi-email" >
{ /* // @TODO: user owners are appearing as guest here when should be only user input */ }
{ guestListEmails . map ( ( email , index ) = > {
return (
< div key = { index } className = "cursor-pointer" >
< span data - tag > { email } < / span >
< / div >
) ;
} ) }
< / div >
) }
< / div >
2022-07-26 08:27:57 +00:00
< / div >
) }
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" >
{ t ( "number_for_sms_reminders" ) }
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"
required
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 " >
2022-08-03 16:01:29 +00:00
< Icon.FiInfo className = "mr-2 h-3 w-3" / >
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" ) }
id = "notes"
name = "notes"
rows = { 3 }
className = { inputClassName }
placeholder = { t ( "share_additional_notes" ) }
disabled = { disabledExceptForOwner }
/ >
) }
< / div >
2022-09-02 21:16:36 +00:00
< div className = "flex justify-end space-x-2 rtl:space-x-reverse" >
2022-09-20 12:37:34 +00:00
{ ! eventType . disableGuests && ! guestToggle && (
2022-09-14 17:56:54 +00:00
< Button
2022-09-14 21:31:47 +00:00
type = "button"
2022-09-21 05:52:14 +00:00
color = "minimalSecondary"
size = "icon"
tooltip = { t ( "additional_guests" ) }
StartIcon = { Icon . FiUserPlus }
2022-09-14 17:56:54 +00:00
onClick = { ( ) = > setGuestToggle ( ! guestToggle ) }
2022-09-21 05:52:14 +00:00
className = "mr-auto"
/ >
2022-09-14 17:56:54 +00:00
) }
2022-09-02 21:16:36 +00:00
< Button
2022-09-14 17:56:54 +00:00
color = "minimal"
2022-09-02 21:16:36 +00:00
type = "button"
onClick = { ( ) = > router . back ( ) }
// We override this for this component only for now - as we don't support darkmode everywhere in the app
2022-09-16 14:29:40 +00:00
className = "dark:hover:bg-darkgray-200 dark:border-none dark:text-white" >
2022-09-02 21:16:36 +00:00
{ t ( "cancel" ) }
< / Button >
2022-07-26 08:27:57 +00:00
< Button
type = "submit"
2022-09-16 14:29:40 +00:00
className = "dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast mr-auto dark:border-transparent"
2022-07-26 08:27:57 +00:00
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" >
2022-08-03 16:01:29 +00:00
< Icon.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" ) } { " " }
{ error instanceof HttpError || error instanceof Error ? error . message : "Unknown error" }
< / p >
< / div >
< / div >
< / div >
) ;
}