2022-04-27 15:19:04 +00:00
import { zodResolver } from "@hookform/resolvers/zod" ;
2022-09-29 16:58:29 +00:00
import { useMutation } from "@tanstack/react-query" ;
2022-03-15 14:39:20 +00:00
import { useSession } from "next-auth/react" ;
2023-03-02 18:15:28 +00:00
import dynamic from "next/dynamic" ;
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-03-02 18:15:28 +00:00
import { useForm , useFormContext } from "react-hook-form" ;
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" ;
2023-03-02 18:15:28 +00:00
import { getEventLocationType , locationKeyToString } 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" ;
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" ;
2023-03-02 18:15:28 +00:00
import {
getBookingFieldsWithSystemFields ,
SystemField ,
} from "@calcom/features/bookings/lib/getBookingFields" ;
import getBookingResponsesSchema , {
getBookingResponsesPartialSchema ,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema" ;
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder" ;
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" ;
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" ;
2023-03-02 18:15:28 +00:00
import { Button , Form , Tooltip } from "@calcom/ui" ;
import { FiAlertTriangle , FiCalendar , FiRefreshCw , FiUser } 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" ;
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
2023-03-02 18:15:28 +00:00
/** These are like 40kb that not every user needs */
const BookingDescriptionPayment = dynamic (
( ) = > import ( "@components/booking/BookingDescriptionPayment" )
) as unknown as typeof import ( "@components/booking/BookingDescriptionPayment" ) . default ;
2022-08-26 00:48:50 +00:00
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps ;
2023-03-02 18:15:28 +00:00
const BookingFields = ( {
fields ,
locations ,
rescheduleUid ,
isDynamicGroupBooking ,
} : {
fields : BookingPageProps [ "eventType" ] [ "bookingFields" ] ;
locations : LocationObject [ ] ;
rescheduleUid? : string ;
isDynamicGroupBooking : boolean ;
} ) = > {
const { t } = useLocale ( ) ;
const { watch , setValue } = useFormContext ( ) ;
const locationResponse = watch ( "responses.location" ) ;
2021-09-22 18:36:13 +00:00
2023-03-02 18:15:28 +00:00
return (
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
< div >
{ fields . map ( ( field , index ) = > {
// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
let readOnly =
( field . editable === "system" || field . editable === "system-but-optional" ) && ! ! rescheduleUid ;
let noLabel = false ;
let hidden = ! ! field . hidden ;
if ( field . name === SystemField . Enum . rescheduleReason ) {
if ( ! rescheduleUid ) {
return null ;
}
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false ;
}
if ( field . name === SystemField . Enum . smsReminderNumber ) {
// `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself.
// I think we should have a way to connect 2 fields together and have them share the same value in Form Builder
if ( locationResponse ? . value === "phone" ) {
setValue ( ` responses. ${ SystemField . Enum . smsReminderNumber } ` , locationResponse ? . optionValue ) ;
// Just don't render the field now, as the value is already connected to attendee phone location
return null ;
}
// `smsReminderNumber` can be edited during reschedule even though it's a system field
readOnly = false ;
}
if ( field . name === SystemField . Enum . guests ) {
// No matter what user configured for Guests field, we don't show it for dynamic group booking as that doesn't support guests
hidden = isDynamicGroupBooking ? true : ! ! field . hidden ;
}
// We don't show `notes` field during reschedule
if ( field . name === SystemField . Enum . notes && ! ! rescheduleUid ) {
return null ;
}
// Dynamically populate location field options
if ( field . name === SystemField . Enum . location && field . type === "radioInput" ) {
if ( ! field . optionsInputs ) {
throw new Error ( "radioInput must have optionsInputs" ) ;
}
const optionsInputs = field . optionsInputs ;
const options = locations . map ( ( location ) = > {
const eventLocation = getEventLocationType ( location . type ) ;
const locationString = locationKeyToString ( location ) ;
if ( typeof locationString !== "string" || ! eventLocation ) {
// It's possible that location app got uninstalled
return null ;
}
const type = eventLocation . type ;
const optionInput = optionsInputs [ type as keyof typeof optionsInputs ] ;
if ( optionInput ) {
optionInput . placeholder = t ( eventLocation ? . attendeeInputPlaceholder || "" ) ;
}
return {
label : t ( locationString ) ,
value : type ,
} ;
} ) ;
field . options = options . filter (
( location ) : location is NonNullable < ( typeof options ) [ number ] > = > ! ! location
) ;
// If we have only one option and it has an input, we don't show the field label because Option name acts as label.
// e.g. If it's just Attendee Phone Number option then we don't show `Location` label
if ( field . options . length === 1 ) {
if ( field . optionsInputs [ field . options [ 0 ] . value ] ) {
noLabel = true ;
} else {
// If there's only one option and it doesn't have an input, we don't show the field at all because it's visible in the left side bar
hidden = true ;
}
}
}
const label = noLabel ? "" : field . label || t ( field . defaultLabel || "" ) ;
const placeholder = field . placeholder || t ( field . defaultPlaceholder || "" ) ;
return (
< FormBuilderField
className = "mb-4"
field = { { . . . field , label , placeholder , hidden } }
readOnly = { readOnly }
key = { index }
/ >
) ;
} ) }
< / div >
) ;
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 ,
} ) ,
{ }
) ;
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 ,
2023-03-02 18:15:28 +00:00
name : bookingForm.getValues ( "responses.name" ) ,
email : bookingForm.getValues ( "responses.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 ,
2023-03-02 18:15:28 +00:00
email : bookingForm.getValues ( "responses.email" ) ,
2022-11-15 19:00:02 +00:00
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 ,
2023-03-02 18:15:28 +00:00
email : bookingForm.getValues ( "responses.email" ) ,
2022-11-15 19:00:02 +00:00
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 ) ;
2023-03-02 18:15:28 +00:00
const querySchema = getBookingResponsesPartialSchema ( {
bookingFields : getBookingFieldsWithSystemFields ( eventType ) ,
} ) ;
const parsedQuery = querySchema . parse ( {
. . . router . query ,
// `guest` because we need to support legacy URL with `guest` query param support
// `guests` because the `name` of the corresponding bookingField is `guests`
guests : router.query.guests || router . query . guest ,
} ) ;
2021-09-14 08:45:28 +00:00
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
) ;
2023-03-02 18:15:28 +00:00
const [ isClientTimezoneAvailable , setIsClientTimezoneAvailable ] = useState ( false ) ;
2021-12-03 10:15:20 +00:00
useEffect ( ( ) = > {
2023-03-02 18:15:28 +00:00
// THis is to fix hydration error that comes because of different timezone on server and client
setIsClientTimezoneAvailable ( true ) ;
} , [ ] ) ;
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
2022-08-05 17:08:47 +00:00
// There should only exists one default userData variable for primaryAttendee.
const defaultUserValues = {
2023-03-02 18:15:28 +00:00
email : rescheduleUid ? booking ? . attendees [ 0 ] . email : parsedQuery [ "email" ] ,
name : rescheduleUid ? booking ? . attendees [ 0 ] . name : parsedQuery [ "name" ] ,
2022-08-05 17:08:47 +00:00
} ;
2022-01-10 23:25:06 +00:00
const defaultValues = ( ) = > {
if ( ! rescheduleUid ) {
2023-03-02 18:15:28 +00:00
const defaults = {
responses : { } as Partial < z.infer < typeof bookingFormSchema > [ "responses" ] > ,
} ;
const responses = eventType . bookingFields . reduce ( ( responses , field ) = > {
return {
. . . responses ,
[ field . name ] : parsedQuery [ field . name ] ,
} ;
} , { } ) ;
defaults . responses = {
. . . responses ,
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
} ;
2023-03-02 18:15:28 +00:00
return defaults ;
2022-01-10 23:25:06 +00:00
}
2023-03-02 18:15:28 +00:00
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
2023-03-02 18:15:28 +00:00
const defaults = {
responses : { } as Partial < z.infer < typeof bookingFormSchema > [ "responses" ] > ,
} ;
const responses = eventType . bookingFields . reduce ( ( responses , field ) = > {
return {
. . . responses ,
[ field . name ] : booking . responses [ field . name ] ,
} ;
} , { } ) ;
defaults . responses = {
. . . responses ,
name : defaultUserValues.name || ( ! loggedInIsOwner && session ? . user ? . name ) || "" ,
email : defaultUserValues.email || ( ! loggedInIsOwner && session ? . user ? . email ) || "" ,
2022-01-10 23:25:06 +00:00
} ;
2023-03-02 18:15:28 +00:00
return defaults ;
2022-01-10 23:25:06 +00:00
} ;
2022-04-27 21:21:18 +00:00
const bookingFormSchema = z
. object ( {
2023-03-02 18:15:28 +00:00
responses : getBookingResponsesSchema ( {
bookingFields : getBookingFieldsWithSystemFields ( eventType ) ,
} ) ,
2022-04-27 21:21:18 +00:00
} )
. passthrough ( ) ;
2022-04-27 15:19:04 +00:00
2023-03-02 18:15:28 +00:00
type BookingFormValues = {
locationType? : EventLocationType [ "type" ] ;
responses : z.infer < typeof bookingFormSchema > [ "responses" ] ;
} ;
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-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 ( ) ,
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 ,
metadata ,
hasHashedBookingLink ,
hashedLink ,
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 ( ) ,
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 ,
metadata ,
hasHashedBookingLink ,
hashedLink ,
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-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-03-02 18:15:28 +00:00
< BookingDescriptionPayment eventType = { eventType } / >
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" >
2023-03-02 18:15:28 +00:00
{ isClientTimezoneAvailable &&
( rescheduleUid || ! eventType . recurringEvent ? . freq ) &&
` ${ parseDate ( date , i18n ) } ` }
{ isClientTimezoneAvailable &&
! rescheduleUid &&
2022-12-13 07:23:26 +00:00
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]" / >
2023-03-02 18:15:28 +00:00
{ isClientTimezoneAvailable &&
typeof booking . startTime === "string" &&
parseDate ( dayjs ( booking . startTime ) , i18n ) }
2022-12-13 07:23:26 +00:00
< / 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 } { " " }
/ { 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 >
) }
< / BookingDescription >
< / div >
) }
< div className = { classNames ( "p-6" , showEventTypeDetails ? "sm:w-1/2" : "w-full" ) } >
2023-03-02 18:15:28 +00:00
< Form form = { bookingForm } noValidate handleSubmit = { bookEvent } >
< BookingFields
isDynamicGroupBooking = { isDynamicGroupBooking }
fields = { eventType . bookingFields }
locations = { locations }
rescheduleUid = { rescheduleUid }
/ >
< div
className = { classNames (
"flex justify-end space-x-2 rtl:space-x-reverse" ,
// HACK: If the last field is guests, we need to move Cancel, Submit buttons up because "Add Guests" being present on the left and the buttons on the right, spacing is not required
eventType . bookingFields [ eventType . bookingFields . length - 1 ] . name ===
SystemField . Enum . guests
? "-mt-4"
: ""
) } >
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 >
) ;
}