2022-09-02 13:12:58 +00:00
import { Collapsible , CollapsibleContent , CollapsibleTrigger } from "@radix-ui/react-collapsible" ;
import classNames from "classnames" ;
import { createEvent } from "ics" ;
2023-02-16 22:39:57 +00:00
import type { GetServerSidePropsContext } from "next" ;
2022-09-02 13:12:58 +00:00
import { useSession } from "next-auth/react" ;
import Link from "next/link" ;
2023-08-02 09:35:48 +00:00
import { usePathname , useRouter , useSearchParams } from "next/navigation" ;
2023-04-04 08:52:27 +00:00
import { useEffect , useState } from "react" ;
2022-09-02 13:12:58 +00:00
import { RRule } from "rrule" ;
import { z } from "zod" ;
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 { getEventLocationValue } from "@calcom/app-store/locations" ;
import { getSuccessPageLocationMessage , guessEventLocationType } from "@calcom/app-store/locations" ;
2022-10-14 16:24:43 +00:00
import { getEventTypeAppData } from "@calcom/app-store/utils" ;
2022-09-02 13:12:58 +00:00
import { getEventName } from "@calcom/core/event" ;
2023-02-16 22:39:57 +00:00
import type { ConfigType } from "@calcom/dayjs" ;
import dayjs from "@calcom/dayjs" ;
2022-09-02 13:12:58 +00:00
import {
sdkActionManager ,
useEmbedNonStylesConfig ,
useIsBackgroundTransparent ,
useIsEmbed ,
} from "@calcom/embed-core/embed-iframe" ;
2023-04-18 12:29:26 +00:00
import { getServerSession } from "@calcom/features/auth/lib/getServerSession" ;
2023-07-20 05:03:50 +00:00
import { SystemField } from "@calcom/features/bookings/lib/SystemField" ;
New Booker Component (preparations for booker atom) (#6792)
* Wip on booker atom
* Wip on booker atom
* Added correct icon imports
* Fixed build
* Responsive improvements
* Removed package lock
* Responsive tweaks
* Animation improvements and cleanup
* Animation improvements and event meta layout improvements.
* Tweaked margins.
* Added more event meta blocks
* Layout tweaks
* Converted booker layout to css grid and implemented multiple layout options
* cleanup
* Fixed build
* Fixed build
* Added temporary api route to enable/disable new booker
* Added sticky behavior
* Reverted yarn.lock and reinstalled new packages to see if this fixes build on vercel.
* Ensure divider lines always have 100% height.
* Improved animation config + initial load
* Ensure to pass eventid to getschedule, otherwise custom availability schedule wont work and wont return any availability
* Fixed divider line heights in booker
* Fixed timezone select positioning
* Added ability to view multiple days of timeslots
* Added icons to booker toggle
* Always show timeslots in timeslots view, also if no date is selected yet. In that case we show upcoming 5 days.
* Fixed timeslots in small calendar view
* Show selected day in calendar
* Fixed booker timeslots view
* Wip in making booking form work
* Moved most of the booker atom stuff to features, since it belongs there. Atom should be a rather small wrapper.
* Added create event functionality to booker form.
* Added guests toggle to booker form and styled input addons in dark mode.
* Added dynamic weekstart to booker
* Added seats limit feature to timeslots.
* Removed todo
* Added correct event avatars
* Added correct event name and icons
* Added correct translation for minutes text in multi duration
* Add rescheduling functionality to new booker.
* Added selected booking time to booking meta in sidebar.
* Abstracted away timeformat to custom hook
* Added correct key props to all components in booker.
* Fix build
* Create some new custom hooks to have a lot less repitition in code.
* Moved bookerform component inside booker directory since it is tied to it.
* Added error messages to booker form, plus fixed bug in recurring events.
* Added some comments <3
* Fixed todos in booker form.
* Added loading state for timeslot selector, and added prefetching of next month, in case of multi day view showing 2 months at the same time.
* Fixed import paths
* Added away view
* Validate uniqueness of event attendees.
* Tweaked comment
* #5798 added correct date format and style for selected date in booker.
* UI improvements
* Enable possibility to add booking values via query params.
* Added functionality to update query params when user selects date/duration etc in booker
* First steps in adding e2e test.
* Fixes after merge with main, and added new form builder.
* Implemented new form types and validation to booker, confirming new form builder. Validation still throwing wrong error keys though.
* Added search to timezone dropdown
* Added e2e test for booker (copy of current booker tests, only enabling cookie), plus fixed reschedule view.
* Updated yarn.lock
* Added new booker for team pages.
* Fixed input addon (hover) styles.
* Added dynamic booking.
* Hide timeformat select for multi day view for now.
* Cleanup and ui tweaks
* removed log
* Mobile improvements
* Cleanup
* Small design tweaks after talking to ciaran.
* Text color and weight tweaks in booker
* Added rainbow gates to new booker.
* Added in default values which fixes form vallidation (???).
* Added empty defaults for name and email
* Added metadata
* Reset yarn.lock
* Fixed booker zod validation after change in main.
* Icon tweak
* Fixed timezone select styles after new classnames have been merged.
* Updated seat availability styles.
* Update yarn.lock
* Added explanation for alchemy key to .env.example
* Added tooltip to booker month/week/multiday toggle
* Fixed timezoneselect styles in booker after select updates.
* Updates bookingfields component by taking changes from current booker component
* Removed remaining booker todos
* Fix bookeventform
* Fix for recurring event meta
* Type fixes
* Typefixes
* Team event fixes
* Avoid hydration errors by only rendering date picker client side. Remove web3 gates since we dont offer them anymore. Prevent timeslot select from staying open when switching to a different month.
* Don't show calendar on mobile booker during booking.
* Always align booker buttons to bottom
* Don't show backend messages in error, rather show a helpful text like the current booker does as well.
* Do invisible next rewrite based on cookie from next.config.js (#7949)
* Do invisible next rewrite based on cookie from next.config.js
* Name embed link instead of bookerPath
* Rewrites only dynamic user pages
---------
Co-authored-by: zomars <zomars@me.com>
* Don't allow change of timezone when bookerform is visible
* Don't add duration to query param if the event is not a multi duration event.
* Update next.config.js
* Added correct timezone formatting to event meta when timeslot is selected.
* removed .env variable that isn't needed anymore.
* Update Gates.tsx
* Type fixes
* Allows to run all tests with the new booker
* Fixed timezone select styles after merge.
* Don't throw error when event doesn't have hosts, rather return no users, which will result in no availability in UI.
* Make booker errors of severity info instead of warning.
* Ensure team avatars are shown, as well as filter on uniqueness of avatars.
* Added all booked today message to timeslots.
* Added cal.com logo to booker.
* Fixed fragment classname error, minor mobile animation tweaks plus make all booked today text smaller for multi day layout.
* Improved timezone select styles, and updated arguments of getbooking function after updates in main.
* Prevent infinite loop in rewriting new booker.
* Prevent infinite loop in rewriting new booker.
* Moved new-booker pages to their own directory to prevent regexes confusing next and thus nut running getserversideprops after rewrite. Also adding clearing of old date in booker store, that could stick around when user immediately navigates back to the same page after booking.
* Fixed cal logo color in darkmode for new booker.
* Implemented new color tokens and theme variables. Also small design tweaks after merge with main.
* Minor style tweaks
* Show multiple locations in tooltip on booker #8222
* Radio button style tweaks
* Fixed build
* Updated calendar imports to new lucide names
* Removed resetting of selected times logic, because otherwise url params wouldnt be taken into account which is actually what we want. So old values sticking around when navigating back is actually the desired behavior.
* Updated tests to instead of always run the new booker in tests, have a utility to run both the new and old booker for specified tests.
* Added comment and eslint disable for if statement in booker test.
* Update packages/features/bookings/components/event-meta/Details.tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Fix badge types
* Lazy loaded timezone select to save 85kb in bundle size.
* Upgraded framer to latest. Als moved framer and react sticky deps to features instead of atoms.
* Added new pagewrapper logic
* Simplified rescheduling ssr fetches, this now also supports multi seat rescheduling.
* Unset selected time when user is rescheduling directly after a new booking, otherwise it would show the form instead of new time selection.
* Updated form builder logic as per form builder in current booker.
* Updated form builder prefill logic as per logic in current booker.
* Updated getbooking function to fetch correct details when a reschedule uid is used
* Fixed booking questions test by NOT waiting for /book page because the new booker doesnt have this.
* Added former meeting time to reschedule view.
* Fixed types
* Undo playwright config update by mistake.
* Fixed event types test by only waiting for /book page in old booker
* Set new booker cookie to one year in the future instead of 2050
* added reset mockdate to test
* Temporary disabled test to see if this solves the out of memory error.
* Deleted test to see if that fixes the memory error
* Select first day when switching months in booker
---------
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-24 14:32:30 +00:00
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking" ;
2023-03-02 18:15:28 +00:00
import {
getBookingFieldsWithSystemFields ,
2023-05-02 16:58:39 +00:00
SMS_REMINDER_NUMBER_FIELD ,
2023-03-02 18:15:28 +00:00
} from "@calcom/features/bookings/lib/getBookingFields" ;
2022-09-02 13:12:58 +00:00
import { parseRecurringEvent } from "@calcom/lib" ;
2022-11-30 21:52:56 +00:00
import { APP_NAME } from "@calcom/lib/constants" ;
2023-02-16 17:16:22 +00:00
import {
formatToLocalizedDate ,
formatToLocalizedTime ,
formatToLocalizedTimezone ,
} from "@calcom/lib/date-fns" ;
2022-09-02 13:12:58 +00:00
import { getDefaultEvent } from "@calcom/lib/defaultEvents" ;
2023-04-05 18:14:46 +00:00
import useGetBrandingColours from "@calcom/lib/getBrandColours" ;
2022-09-02 13:12:58 +00:00
import { useLocale } from "@calcom/lib/hooks/useLocale" ;
2023-08-02 09:35:48 +00:00
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery" ;
2022-09-02 13:12:58 +00:00
import useTheme from "@calcom/lib/hooks/useTheme" ;
import { getEveryFreqFor } from "@calcom/lib/recurringStrings" ;
2023-03-14 04:19:05 +00:00
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat" ;
2022-09-30 12:45:28 +00:00
import { getIs24hClockFromLocalStorage , isBrowserLocale24h } from "@calcom/lib/timeFormat" ;
2022-09-02 13:12:58 +00:00
import { localStorage } from "@calcom/lib/webstorage" ;
2023-01-12 21:09:12 +00:00
import prisma from "@calcom/prisma" ;
2023-02-16 22:39:57 +00:00
import type { Prisma } from "@calcom/prisma/client" ;
2023-05-02 11:44:05 +00:00
import { BookingStatus } from "@calcom/prisma/enums" ;
2023-08-02 09:35:48 +00:00
import { bookingMetadataSchema , customInputSchema , EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils" ;
import { Alert , Badge , Button , EmailInput , HeadSeo , useCalcomTheme } from "@calcom/ui" ;
import { AlertCircle , Calendar , Check , ChevronLeft , ExternalLink , X } from "@calcom/ui/components/icon" ;
2022-09-02 13:12:58 +00:00
2022-11-04 16:43:02 +00:00
import { timeZone } from "@lib/clock" ;
2023-02-16 22:39:57 +00:00
import type { inferSSRProps } from "@lib/types/inferSSRProps" ;
2022-09-02 13:12:58 +00:00
2023-04-18 18:45:32 +00:00
import PageWrapper from "@components/PageWrapper" ;
2022-09-02 13:12:58 +00:00
import CancelBooking from "@components/booking/CancelBooking" ;
2022-12-05 21:35:44 +00:00
import EventReservationSchema from "@components/schemas/EventReservationSchema" ;
2022-09-02 13:12:58 +00:00
import { ssrInit } from "@server/lib/ssr" ;
2023-04-05 18:14:46 +00:00
const useBrandColors = ( {
brandColor ,
darkBrandColor ,
} : {
brandColor? : string | null ;
darkBrandColor? : string | null ;
} ) = > {
const brandTheme = useGetBrandingColours ( {
lightVal : brandColor ,
darkVal : darkBrandColor ,
} ) ;
useCalcomTheme ( brandTheme ) ;
} ;
2022-09-02 13:12:58 +00:00
type SuccessProps = inferSSRProps < typeof getServerSideProps > ;
2022-11-16 19:48:17 +00:00
const stringToBoolean = z
. string ( )
. optional ( )
. transform ( ( val ) = > val === "true" ) ;
const querySchema = z . object ( {
uid : z.string ( ) ,
2023-04-04 08:52:27 +00:00
email : z.string ( ) . optional ( ) ,
eventTypeSlug : z.string ( ) . optional ( ) ,
2022-11-16 19:48:17 +00:00
cancel : stringToBoolean ,
2023-04-04 08:52:27 +00:00
allRemainingBookings : stringToBoolean ,
2022-11-29 20:27:29 +00:00
changes : stringToBoolean ,
2022-11-16 19:48:17 +00:00
reschedule : stringToBoolean ,
2023-03-22 15:22:40 +00:00
isSuccessBookingPage : stringToBoolean ,
2022-12-20 22:46:28 +00:00
formerTime : z.string ( ) . optional ( ) ,
2023-03-14 04:19:05 +00:00
seatReferenceUid : z.string ( ) . optional ( ) ,
2022-11-16 19:48:17 +00:00
} ) ;
2022-09-02 13:12:58 +00:00
export default function Success ( props : SuccessProps ) {
const { t } = useLocale ( ) ;
const router = useRouter ( ) ;
2023-08-02 09:35:48 +00:00
const routerQuery = useRouterQuery ( ) ;
const pathname = usePathname ( ) ;
const searchParams = useSearchParams ( ) ;
2022-11-16 19:48:17 +00:00
const {
allRemainingBookings ,
isSuccessBookingPage ,
cancel : isCancellationMode ,
2022-12-20 22:46:28 +00:00
formerTime ,
2023-01-31 19:47:32 +00:00
email ,
2023-03-14 04:19:05 +00:00
seatReferenceUid ,
2023-08-02 09:35:48 +00:00
} = querySchema . parse ( routerQuery ) ;
2022-11-16 19:48:17 +00:00
2023-04-18 12:29:26 +00:00
const attendeeTimeZone = props ? . bookingInfo ? . attendees . find (
( attendee ) = > attendee . email === email
) ? . timeZone ;
const tz = isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : props.tz ? props.tz : timeZone ( ) ;
2022-12-01 15:20:01 +00:00
2023-03-02 18:15:28 +00:00
const location = props . bookingInfo . location as ReturnType < typeof getEventLocationValue > ;
2022-09-02 13:12:58 +00:00
2023-02-05 14:43:37 +00:00
const locationVideoCallUrl : string | undefined = bookingMetadataSchema . parse (
props ? . bookingInfo ? . metadata || { }
) ? . videoCallUrl ;
2022-11-15 19:00:02 +00:00
const status = props . bookingInfo ? . status ;
const reschedule = props . bookingInfo . status === BookingStatus . ACCEPTED ;
2023-02-13 12:26:28 +00:00
const cancellationReason = props . bookingInfo . cancellationReason || props . bookingInfo . rejectionReason ;
2022-11-15 19:00:02 +00:00
2022-12-05 17:41:40 +00:00
const attendeeName =
typeof props ? . bookingInfo ? . attendees ? . [ 0 ] ? . name === "string"
? props ? . bookingInfo ? . attendees ? . [ 0 ] ? . name
: "Nameless" ;
2023-06-23 17:13:16 +00:00
const attendees = props ? . bookingInfo ? . attendees ;
const isGmail = ! ! attendees . find ( ( attendee ) = > attendee . email . includes ( "gmail.com" ) ) ;
2022-09-02 13:12:58 +00:00
const [ is24h , setIs24h ] = useState ( isBrowserLocale24h ( ) ) ;
const { data : session } = useSession ( ) ;
2022-11-15 19:00:02 +00:00
const [ date , setDate ] = useState ( dayjs . utc ( props . bookingInfo . startTime ) ) ;
2022-09-02 13:12:58 +00:00
const { eventType , bookingInfo } = props ;
const isBackgroundTransparent = useIsBackgroundTransparent ( ) ;
const isEmbed = useIsEmbed ( ) ;
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig ( "align" ) !== "left" ;
const shouldAlignCentrally = ! isEmbed || shouldAlignCentrallyInEmbed ;
2022-11-28 18:14:01 +00:00
const [ calculatedDuration , setCalculatedDuration ] = useState < number | undefined > ( undefined ) ;
2022-11-16 19:48:17 +00:00
function setIsCancellationMode ( value : boolean ) {
2023-08-02 09:35:48 +00:00
const _searchParams = new URLSearchParams ( searchParams ) ;
2023-03-15 20:07:35 +00:00
if ( value ) {
2023-08-02 09:35:48 +00:00
_searchParams . set ( "cancel" , "true" ) ;
2023-03-15 20:07:35 +00:00
} else {
2023-08-02 09:35:48 +00:00
if ( _searchParams . get ( "cancel" ) ) {
_searchParams . delete ( "cancel" ) ;
2023-03-15 20:07:35 +00:00
}
}
2023-08-02 09:35:48 +00:00
router . replace ( ` ${ pathname } ? ${ _searchParams . toString ( ) } ` ) ;
2022-11-16 19:48:17 +00:00
}
2022-09-02 13:12:58 +00:00
2023-07-27 08:52:46 +00:00
let evtName = props . eventType . eventName ;
if ( eventType . isDynamic && bookingInfo . responses ? . title ) {
evtName = bookingInfo . responses . title as string ;
}
2022-09-02 13:12:58 +00:00
const eventNameObject = {
attendeeName ,
eventType : props.eventType.title ,
2023-07-27 08:52:46 +00:00
eventName : evtName ,
2022-09-02 13:12:58 +00:00
host : props.profile.name || "Nameless" ,
location : location ,
2023-03-09 15:11:16 +00:00
bookingFields : bookingInfo.responses ,
2022-09-02 13:12:58 +00:00
t ,
} ;
2022-10-14 16:24:43 +00:00
const giphyAppData = getEventTypeAppData ( eventType , "giphy" ) ;
const giphyImage = giphyAppData ? . thankYouPage ;
2022-09-02 13:12:58 +00:00
2023-02-03 10:20:10 +00:00
const eventName = getEventName ( eventNameObject , true ) ;
2023-03-23 18:50:39 +00:00
// Confirmation can be needed in two cases as of now
// - Event Type has require confirmation option enabled always
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
// - It's a paid event and payment is pending.
2023-04-14 21:56:16 +00:00
const needsConfirmation = bookingInfo . status === BookingStatus . PENDING && eventType . requiresConfirmation ;
2023-03-14 04:19:05 +00:00
const userIsOwner = ! ! ( session ? . user ? . id && eventType . owner ? . id === session . user . id ) ;
2023-07-07 15:50:44 +00:00
const isLoggedIn = session ? . user ;
2023-03-14 04:19:05 +00:00
const isCancelled =
status === "CANCELLED" ||
status === "REJECTED" ||
2023-03-16 13:29:16 +00:00
( ! ! seatReferenceUid &&
! bookingInfo . seatsReferences . some ( ( reference ) = > reference . referenceUid === seatReferenceUid ) ) ;
2023-03-14 04:19:05 +00:00
2023-04-16 21:58:47 +00:00
// const telemetry = useTelemetry();
/ * u s e E f f e c t ( ( ) = > {
2022-09-02 13:12:58 +00:00
if ( top !== window ) {
//page_view will be collected automatically by _middleware.ts
2022-11-29 20:27:29 +00:00
telemetry . event ( telemetryEventTypes . embedView , collectPageParameters ( "/booking" ) ) ;
2022-09-02 13:12:58 +00:00
}
2023-04-16 21:58:47 +00:00
} , [ telemetry ] ) ; * /
2022-09-02 13:12:58 +00:00
useEffect ( ( ) = > {
const users = eventType . users ;
if ( ! sdkActionManager ) return ;
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
sdkActionManager . fire ( "bookingSuccessful" , {
2023-01-27 05:32:33 +00:00
booking : bookingInfo ,
2022-09-02 13:12:58 +00:00
eventType ,
date : date.toString ( ) ,
2022-11-28 18:14:01 +00:00
duration : calculatedDuration ,
2022-09-02 13:12:58 +00:00
organizer : {
name : users [ 0 ] . name || "Nameless" ,
email : users [ 0 ] . email || "Email-less" ,
timeZone : users [ 0 ] . timeZone ,
} ,
confirmed : ! needsConfirmation ,
// TODO: Add payment details
} ) ;
setDate ( date . tz ( localStorage . getItem ( "timeOption.preferredTimeZone" ) || dayjs . tz . guess ( ) ) ) ;
2022-09-30 12:45:28 +00:00
setIs24h ( ! ! getIs24hClockFromLocalStorage ( ) ) ;
2022-09-02 13:12:58 +00:00
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ eventType , needsConfirmation ] ) ;
2022-11-28 18:14:01 +00:00
useEffect ( ( ) = > {
setCalculatedDuration (
dayjs ( props . bookingInfo . endTime ) . diff ( dayjs ( props . bookingInfo . startTime ) , "minutes" )
) ;
2023-06-06 11:59:57 +00:00
// eslint-disable-next-line react-hooks/exhaustive-deps
2022-11-28 18:14:01 +00:00
} , [ ] ) ;
2022-09-02 13:12:58 +00:00
function eventLink ( ) : string {
const optional : { location? : string } = { } ;
2023-02-05 14:43:37 +00:00
if ( locationVideoCallUrl ) {
optional [ "location" ] = locationVideoCallUrl ;
2022-09-02 13:12:58 +00:00
}
const event = createEvent ( {
start : [
date . toDate ( ) . getUTCFullYear ( ) ,
( date . toDate ( ) . getUTCMonth ( ) as number ) + 1 ,
date . toDate ( ) . getUTCDate ( ) ,
date . toDate ( ) . getUTCHours ( ) ,
date . toDate ( ) . getUTCMinutes ( ) ,
] ,
startInputType : "utc" ,
title : eventName ,
description : props.eventType.description ? props.eventType.description : undefined ,
/** formatted to required type of description ^ */
2022-11-28 18:14:01 +00:00
duration : {
minutes : calculatedDuration ,
} ,
2022-09-02 13:12:58 +00:00
. . . optional ,
} ) ;
if ( event . error ) {
throw event . error ;
}
return encodeURIComponent ( event . value ? event.value : false ) ;
}
function getTitle ( ) : string {
const titleSuffix = props . recurringBookings ? "_recurring" : "" ;
if ( isCancelled ) {
2022-11-16 19:48:17 +00:00
return "" ;
2022-09-02 13:12:58 +00:00
}
if ( needsConfirmation ) {
if ( props . profile . name !== null ) {
return t ( "user_needs_to_confirm_or_reject_booking" + titleSuffix , {
user : props.profile.name ,
} ) ;
}
return t ( "needs_to_be_confirmed_or_rejected" + titleSuffix ) ;
}
return t ( "emailed_you_and_attendees" + titleSuffix ) ;
}
2023-03-02 18:15:28 +00:00
2023-04-17 12:16:54 +00:00
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
// As Booking Page it has to support configured theme, but as booking detail page it should not do any change. Let Shell.tsx handle it.
2023-07-18 01:02:42 +00:00
useTheme ( isSuccessBookingPage ? props . profile . theme : "system" ) ;
2023-04-05 18:14:46 +00:00
useBrandColors ( {
brandColor : props.profile.brandColor ,
darkBrandColor : props.profile.darkBrandColor ,
} ) ;
2022-09-02 13:12:58 +00:00
const title = t (
2023-06-05 07:28:03 +00:00
` booking_ ${ needsConfirmation ? "submitted" : "confirmed" } ${ props . recurringBookings ? "_recurring" : "" } `
2022-09-02 13:12:58 +00:00
) ;
2023-02-05 14:43:37 +00:00
const locationToDisplay = getSuccessPageLocationMessage (
locationVideoCallUrl ? locationVideoCallUrl : location ,
t ,
bookingInfo . status
) ;
2022-09-02 13:12:58 +00:00
2023-02-08 12:45:09 +00:00
const providerName = guessEventLocationType ( location ) ? . label ;
2022-11-23 22:30:54 +00:00
2022-09-02 13:12:58 +00:00
return (
2022-09-07 12:49:30 +00:00
< div className = { isEmbed ? "" : "h-screen" } data - testid = "success-page" >
2022-12-05 21:35:44 +00:00
{ ! isEmbed && (
< EventReservationSchema
reservationId = { bookingInfo . uid }
eventName = { eventName }
startTime = { bookingInfo . startTime }
endTime = { bookingInfo . endTime }
organizer = { bookingInfo . user }
attendees = { bookingInfo . attendees }
location = { locationToDisplay }
description = { bookingInfo . description }
status = { status }
/ >
) }
2023-07-07 15:50:44 +00:00
{ isLoggedIn && ! isEmbed && (
2023-06-22 22:25:37 +00:00
< div className = "-mb-4 ml-4 mt-2" >
2023-01-06 12:13:56 +00:00
< Link
href = { allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming" }
2023-05-04 11:58:27 +00:00
className = "hover:bg-subtle text-subtle hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent" >
2023-04-12 15:26:31 +00:00
< ChevronLeft className = "h-5 w-5" / > { t ( "back_to_bookings" ) }
2022-09-02 13:12:58 +00:00
< / Link >
< / div >
) }
< HeadSeo title = { title } description = { title } / >
2022-10-14 16:24:43 +00:00
< BookingPageTagManager eventType = { eventType } / >
2022-09-02 13:12:58 +00:00
< main className = { classNames ( shouldAlignCentrally ? "mx-auto" : "" , isEmbed ? "" : "max-w-3xl" ) } >
< div className = { classNames ( "overflow-y-auto" , isEmbed ? "" : "z-50 " ) } >
< div
className = { classNames (
shouldAlignCentrally ? "text-center" : "" ,
2023-06-22 22:25:37 +00:00
"flex items-end justify-center px-4 pb-20 pt-4 sm:block sm:p-0"
2022-09-02 13:12:58 +00:00
) } >
< div
className = { classNames ( "my-4 transition-opacity sm:my-0" , isEmbed ? "" : " inset-0" ) }
aria - hidden = "true" >
< div
className = { classNames (
2023-01-18 19:29:36 +00:00
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl" ,
2023-04-13 18:26:31 +00:00
! isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width" ,
2023-06-22 22:25:37 +00:00
"px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
2022-09-02 13:12:58 +00:00
) }
role = "dialog"
aria - modal = "true"
aria - labelledby = "modal-headline" >
2023-01-18 19:29:36 +00:00
< div
className = { classNames (
"mx-auto flex items-center justify-center" ,
! giphyImage && ! isCancelled && ! needsConfirmation
2023-04-05 18:14:46 +00:00
? "bg-success h-12 w-12 rounded-full"
2023-01-18 19:29:36 +00:00
: "" ,
! giphyImage && ! isCancelled && needsConfirmation
2023-04-05 18:14:46 +00:00
? "bg-subtle h-12 w-12 rounded-full"
2023-01-18 19:29:36 +00:00
: "" ,
2023-04-05 18:14:46 +00:00
isCancelled ? "bg-error h-12 w-12 rounded-full" : ""
2023-01-18 19:29:36 +00:00
) } >
2023-03-20 07:48:22 +00:00
{ giphyImage && ! needsConfirmation && ! isCancelled && (
2023-01-18 19:29:36 +00:00
// eslint-disable-next-line @next/next/no-img-element
< img src = { giphyImage } alt = "Gif from Giphy" / >
) }
{ ! giphyImage && ! needsConfirmation && ! isCancelled && (
2023-04-12 15:26:31 +00:00
< Check className = "h-5 w-5 text-green-600" / >
2023-01-18 19:29:36 +00:00
) }
2023-04-12 15:26:31 +00:00
{ needsConfirmation && ! isCancelled && < Calendar className = "text-emphasis h-5 w-5" / > }
{ isCancelled && < X className = "h-5 w-5 text-red-600" / > }
2023-01-18 19:29:36 +00:00
< / div >
2023-06-22 22:25:37 +00:00
< div className = "mb-8 mt-6 text-center last:mb-0" >
2023-01-18 19:29:36 +00:00
< h3
2023-04-05 18:14:46 +00:00
className = "text-emphasis text-2xl font-semibold leading-6"
2023-01-18 19:29:36 +00:00
data - testid = { isCancelled ? "cancelled-headline" : "" }
id = "modal-headline" >
{ needsConfirmation && ! isCancelled
? props . recurringBookings
2023-06-08 13:37:54 +00:00
? t ( "booking_submitted_recurring" )
2023-05-31 16:33:29 +00:00
: t ( "booking_submitted" )
2023-01-18 19:29:36 +00:00
: isCancelled
2023-03-14 04:19:05 +00:00
? seatReferenceUid
? t ( "no_longer_attending" )
: t ( "event_cancelled" )
2023-01-18 19:29:36 +00:00
: props . recurringBookings
? t ( "meeting_is_scheduled_recurring" )
: t ( "meeting_is_scheduled" ) }
< / h3 >
< div className = "mt-3" >
2023-04-05 18:14:46 +00:00
< p className = "text-default" > { getTitle ( ) } < / p >
2022-09-02 13:12:58 +00:00
< / div >
2023-02-24 04:58:14 +00:00
{ props . paymentStatus &&
( bookingInfo . status === BookingStatus . CANCELLED ||
bookingInfo . status === BookingStatus . REJECTED ) && (
< h4 >
{ ! props . paymentStatus . success &&
! props . paymentStatus . refunded &&
t ( "booking_with_payment_cancelled" ) }
{ props . paymentStatus . success &&
! props . paymentStatus . refunded &&
t ( "booking_with_payment_cancelled_already_paid" ) }
{ props . paymentStatus . refunded && t ( "booking_with_payment_cancelled_refunded" ) }
< / h4 >
) }
2023-01-14 15:49:15 +00:00
2023-04-05 18:14:46 +00:00
< div className = "border-subtle text-default mt-8 grid grid-cols-3 border-t pt-8 text-left" >
2023-01-18 19:29:36 +00:00
{ ( isCancelled || reschedule ) && cancellationReason && (
< >
< div className = "font-medium" >
2023-06-20 08:39:46 +00:00
{ isCancelled ? t ( "reason" ) : t ( "reschedule_reason" ) }
2023-01-18 19:29:36 +00:00
< / div >
< div className = "col-span-2 mb-6 last:mb-0" > { cancellationReason } < / div >
< / >
) }
< div className = "font-medium" > { t ( "what" ) } < / div >
2023-07-27 11:48:08 +00:00
< div className = "col-span-2 mb-6 last:mb-0" data - testid = "booking-title" >
{ eventName }
< / div >
2023-01-18 19:29:36 +00:00
< div className = "font-medium" > { t ( "when" ) } < / div >
< div className = "col-span-2 mb-6 last:mb-0" >
{ reschedule && ! ! formerTime && (
< p className = "line-through" >
< RecurringBookings
eventType = { props . eventType }
duration = { calculatedDuration }
recurringBookings = { props . recurringBookings }
allRemainingBookings = { allRemainingBookings }
date = { dayjs ( formerTime ) }
is24h = { is24h }
isCancelled = { isCancelled }
2023-03-09 10:18:22 +00:00
tz = { tz }
2023-01-18 19:29:36 +00:00
/ >
< / p >
2023-01-18 10:40:06 +00:00
) }
2023-01-18 19:29:36 +00:00
< RecurringBookings
eventType = { props . eventType }
duration = { calculatedDuration }
recurringBookings = { props . recurringBookings }
allRemainingBookings = { allRemainingBookings }
date = { date }
is24h = { is24h }
isCancelled = { isCancelled }
2023-03-09 10:18:22 +00:00
tz = { tz }
2023-01-18 19:29:36 +00:00
/ >
2023-01-18 10:40:06 +00:00
< / div >
2023-01-18 19:29:36 +00:00
{ ( bookingInfo ? . user || bookingInfo ? . attendees ) && (
2022-09-02 13:12:58 +00:00
< >
2023-01-18 19:29:36 +00:00
< div className = "font-medium" > { t ( "who" ) } < / div >
< div className = "col-span-2 last:mb-0" >
2023-03-15 20:07:35 +00:00
{ bookingInfo ? . user && (
< div className = "mb-3" >
< div >
2023-07-28 12:51:15 +00:00
< span data - testid = "booking-host-name" className = "mr-2" >
2023-07-21 11:58:52 +00:00
{ bookingInfo . user . name }
< / span >
2023-03-15 20:07:35 +00:00
< Badge variant = "blue" > { t ( "Host" ) } < / Badge >
2023-01-18 19:29:36 +00:00
< / div >
2023-04-05 18:14:46 +00:00
< p className = "text-default" > { bookingInfo . user . email } < / p >
2023-03-15 20:07:35 +00:00
< / div >
) }
{ bookingInfo ? . attendees . map ( ( attendee ) = > (
2023-03-23 10:22:30 +00:00
< div key = { attendee . name + attendee . email } className = "mb-3 last:mb-0" >
2023-05-22 23:15:06 +00:00
{ attendee . name && (
< p data - testid = { ` attendee-name- ${ attendee . name } ` } > { attendee . name } < / p >
) }
< p data - testid = { ` attendee-email- ${ attendee . email } ` } > { attendee . email } < / p >
2023-03-15 20:07:35 +00:00
< / div >
) ) }
2022-09-02 13:12:58 +00:00
< / div >
< / >
2023-01-18 19:29:36 +00:00
) }
2023-02-21 09:25:57 +00:00
{ locationToDisplay && ! isCancelled && (
2022-09-02 13:12:58 +00:00
< >
2023-01-18 19:29:36 +00:00
< div className = "mt-3 font-medium" > { t ( "where" ) } < / div >
< div className = "col-span-2 mt-3" >
{ locationToDisplay . startsWith ( "http" ) ? (
2023-02-08 12:45:09 +00:00
< a
href = { locationToDisplay }
target = "_blank"
title = { locationToDisplay }
2023-04-05 18:14:46 +00:00
className = "text-default flex items-center gap-2 underline"
2023-02-08 12:45:09 +00:00
rel = "noreferrer" >
{ providerName || "Link" }
2023-04-12 15:26:31 +00:00
< ExternalLink className = "text-default inline h-4 w-4" / >
2023-01-18 19:29:36 +00:00
< / a >
) : (
locationToDisplay
) }
< / div >
2022-09-02 13:12:58 +00:00
< / >
2023-01-18 19:29:36 +00:00
) }
{ bookingInfo ? . description && (
2022-10-27 09:53:13 +00:00
< >
2023-01-18 19:29:36 +00:00
< div className = "mt-9 font-medium" > { t ( "additional_notes" ) } < / div >
< div className = "col-span-2 mb-2 mt-9" >
< p className = "break-words" > { bookingInfo . description } < / p >
2022-10-27 09:53:13 +00:00
< / div >
< / >
) }
2023-03-31 17:15:09 +00:00
< / div >
< div className = "text-bookingdark dark:border-darkgray-200 mt-8 text-left dark:text-gray-300" >
2023-03-02 18:15:28 +00:00
{ Object . entries ( bookingInfo . responses ) . map ( ( [ name , response ] ) = > {
const field = eventType . bookingFields . find ( ( field ) = > field . name === name ) ;
// We show location in the "where" section
// We show Booker Name, Emails and guests in Who section
// We show notes in additional notes section
// We show rescheduleReason at the top
if ( ! field ) return null ;
const isSystemField = SystemField . safeParse ( field . name ) ;
2023-05-02 16:58:39 +00:00
// SMS_REMINDER_NUMBER_FIELD is a system field but doesn't have a dedicated place in the UI. So, it would be shown through the following responses list
if ( isSystemField . success && field . name !== SMS_REMINDER_NUMBER_FIELD ) return null ;
2023-03-02 18:15:28 +00:00
const label = field . label || t ( field . defaultLabel || "" ) ;
return (
< >
2023-04-05 18:14:46 +00:00
< div className = "text-emphasis mt-4 font-medium" > { label } < / div >
< p
className = "text-default break-words"
data - testid = "field-response"
data - fob - field = { field . name } >
2023-03-31 17:15:09 +00:00
{ response . toString ( ) }
< / p >
2023-03-02 18:15:28 +00:00
< / >
) ;
} ) }
2023-01-18 19:29:36 +00:00
< / div >
< / div >
{ ( ! needsConfirmation || ! userIsOwner ) &&
! isCancelled &&
( ! isCancellationMode ? (
< >
2023-04-05 18:14:46 +00:00
< hr className = "border-subtle mb-8" / >
2023-01-18 19:29:36 +00:00
< div className = "text-center last:pb-0" >
2023-04-05 18:14:46 +00:00
< span className = "text-emphasis ltr:mr-2 rtl:ml-2" > { t ( "need_to_make_a_change" ) } < / span >
2023-01-18 19:29:36 +00:00
{ ! props . recurringBookings && (
2023-04-05 18:14:46 +00:00
< span className = "text-default inline" >
2023-03-16 05:10:20 +00:00
< span className = "underline" data - testid = "reschedule-link" >
2023-03-14 04:19:05 +00:00
< Link
href = { ` /reschedule/ ${ seatReferenceUid || bookingInfo ? . uid } ` }
legacyBehavior >
2023-01-18 19:29:36 +00:00
{ t ( "reschedule" ) }
< / Link >
< / span >
< span className = "mx-2" > { t ( "or_lowercase" ) } < / span >
< / span >
) }
< button
data - testid = "cancel"
className = { classNames (
2023-04-05 18:14:46 +00:00
"text-default underline" ,
2023-01-18 19:29:36 +00:00
props . recurringBookings && "ltr:mr-2 rtl:ml-2"
) }
onClick = { ( ) = > setIsCancellationMode ( true ) } >
{ t ( "cancel" ) }
< / button >
< / div >
< / >
) : (
< >
2023-04-05 18:14:46 +00:00
< hr className = "border-subtle" / >
2023-01-18 19:29:36 +00:00
< CancelBooking
booking = { { uid : bookingInfo?.uid , title : bookingInfo?.title , id : bookingInfo?.id } }
profile = { { name : props.profile.name , slug : props.profile.slug } }
recurringEvent = { eventType . recurringEvent }
team = { eventType ? . team ? . name }
setIsCancellationMode = { setIsCancellationMode }
theme = { isSuccessBookingPage ? props . profile . theme : "light" }
allRemainingBookings = { allRemainingBookings }
2023-03-14 04:19:05 +00:00
seatReferenceUid = { seatReferenceUid }
2023-01-18 19:29:36 +00:00
/ >
< / >
) ) }
{ userIsOwner &&
! needsConfirmation &&
! isCancellationMode &&
! isCancelled &&
2023-03-14 04:19:05 +00:00
! ! calculatedDuration && (
2022-11-28 18:14:01 +00:00
< >
2023-04-05 18:14:46 +00:00
< hr className = "border-subtle mt-8" / >
< div className = "text-default align-center flex flex-row justify-center pt-8" >
< span className = "text-default flex self-center font-medium ltr:mr-2 rtl:ml-2 " >
2023-01-18 19:29:36 +00:00
{ t ( "add_to_calendar" ) }
< / span >
< div className = "justify-left mt-1 flex text-left sm:mt-0" >
< Link
href = {
` https://calendar.google.com/calendar/r/eventedit?dates= ${ date
. utc ( )
. format ( "YYYYMMDDTHHmmss[Z]" ) } / $ { date
. add ( calculatedDuration , "minute" )
. utc ( )
. format ( "YYYYMMDDTHHmmss[Z]" ) } & text = $ { eventName } & details = $ {
props . eventType . description
} ` +
2023-02-05 14:43:37 +00:00
( typeof locationVideoCallUrl === "string"
? "&location=" + encodeURIComponent ( locationVideoCallUrl )
2023-01-18 19:29:36 +00:00
: "" ) +
( props . eventType . recurringEvent
? "&recur=" +
encodeURIComponent ( new RRule ( props . eventType . recurringEvent ) . toString ( ) )
: "" )
}
2023-04-05 18:14:46 +00:00
className = "text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2" >
2023-01-18 19:29:36 +00:00
< svg
className = "-mt-1.5 inline-block h-4 w-4"
fill = "currentColor"
xmlns = "http://www.w3.org/2000/svg"
viewBox = "0 0 24 24" >
< title > Google < / title >
< path d = "M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" / >
< / svg >
< / Link >
< Link
href = {
encodeURI (
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
props . eventType . description +
"&enddt=" +
date . add ( calculatedDuration , "minute" ) . utc ( ) . format ( ) +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date . utc ( ) . format ( ) +
"&subject=" +
eventName
2023-02-05 14:43:37 +00:00
) +
( locationVideoCallUrl
? "&location=" + encodeURIComponent ( locationVideoCallUrl )
: "" )
2023-01-18 19:29:36 +00:00
}
2023-04-05 18:14:46 +00:00
className = "border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
2023-01-18 19:29:36 +00:00
target = "_blank" >
< svg
2023-06-22 22:25:37 +00:00
className = "-mt-1.5 mr-1 inline-block h-4 w-4"
2023-01-18 19:29:36 +00:00
fill = "currentColor"
xmlns = "http://www.w3.org/2000/svg"
viewBox = "0 0 24 24" >
< title > Microsoft Outlook < / title >
< path d = "M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" / >
< / svg >
< / Link >
< Link
href = {
encodeURI (
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
props . eventType . description +
"&enddt=" +
date . add ( calculatedDuration , "minute" ) . utc ( ) . format ( ) +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date . utc ( ) . format ( ) +
"&subject=" +
eventName
2023-02-05 14:43:37 +00:00
) +
( locationVideoCallUrl
? "&location=" + encodeURIComponent ( locationVideoCallUrl )
: "" )
2023-01-18 19:29:36 +00:00
}
2023-04-05 18:14:46 +00:00
className = "text-default border-subtle mx-2 h-10 w-10 rounded-sm border px-3 py-2"
2023-01-18 19:29:36 +00:00
target = "_blank" >
< svg
2023-06-22 22:25:37 +00:00
className = "-mt-1.5 mr-1 inline-block h-4 w-4"
2023-01-18 19:29:36 +00:00
fill = "currentColor"
xmlns = "http://www.w3.org/2000/svg"
viewBox = "0 0 24 24" >
< title > Microsoft Office < / title >
< path d = "M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" / >
< / svg >
< / Link >
< Link
href = { "data:text/calendar," + eventLink ( ) }
2023-04-05 18:14:46 +00:00
className = "border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
2023-01-18 19:29:36 +00:00
download = { props . eventType . title + ".ics" } >
< svg
version = "1.1"
fill = "currentColor"
xmlns = "http://www.w3.org/2000/svg"
viewBox = "0 0 1000 1000"
2023-06-22 22:25:37 +00:00
className = "-mt-1.5 mr-1 inline-block h-4 w-4" >
2023-01-18 19:29:36 +00:00
< title > { t ( "other" ) } < / title >
< path d = "M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" / >
< / svg >
< / Link >
< / div >
2022-09-02 13:12:58 +00:00
< / div >
2022-11-28 18:14:01 +00:00
< / >
) }
2023-03-14 04:19:05 +00:00
2023-01-18 19:29:36 +00:00
{ session === null && ! ( userIsOwner || props . hideBranding ) && (
< >
2023-04-05 18:14:46 +00:00
< hr className = "border-subtle mt-8" / >
< div className = "text-default pt-8 text-center text-xs" >
2023-01-18 19:29:36 +00:00
< a href = "https://cal.com/signup" >
{ t ( "create_booking_link_with_calcom" , { appName : APP_NAME } ) }
< / a >
< form
onSubmit = { ( e ) = > {
e . preventDefault ( ) ;
const target = e . target as typeof e . target & {
email : { value : string } ;
} ;
router . push ( ` https://cal.com/signup?email= ${ target . email . value } ` ) ;
} }
className = "mt-4 flex" >
< EmailInput
name = "email"
id = "email"
2023-01-31 19:47:32 +00:00
defaultValue = { email }
2023-04-05 18:14:46 +00:00
className = "mr- focus:border-brand-default border-default text-default mt-0 block w-full rounded-none rounded-l-md shadow-sm focus:ring-black sm:text-sm"
2023-01-18 19:29:36 +00:00
placeholder = "rick.astley@cal.com"
/ >
< Button
size = "lg"
type = "submit"
className = "min-w-max rounded-none rounded-r-md"
color = "primary" >
{ t ( "try_for_free" ) }
< / Button >
< / form >
< / div >
< / >
) }
2022-09-02 13:12:58 +00:00
< / div >
2023-06-23 17:13:16 +00:00
{ isGmail && (
< Alert
className = "main -mb-20 mt-4 inline-block text-left sm:-mt-4 sm:mb-4 sm:w-full sm:max-w-xl sm:align-middle"
severity = "warning"
message = {
< div >
< p className = "font-semibold" > { t ( "google_new_spam_policy" ) } < / p >
< span className = "underline" >
< a
target = "_blank"
href = "https://cal.com/blog/google-s-new-spam-policy-may-be-affecting-your-invitations" >
{ t ( "resolve" ) }
< / a >
< / span >
< / div >
}
CustomIcon = { AlertCircle }
customIconColor = "text-attention dark:text-orange-200"
/ >
) }
2022-09-02 13:12:58 +00:00
< / div >
< / div >
< / div >
< / main >
< / div >
) ;
}
2023-04-17 12:16:54 +00:00
Success . isBookingPage = true ;
2023-04-18 18:45:32 +00:00
Success . PageWrapper = PageWrapper ;
2023-04-17 12:16:54 +00:00
2022-09-02 13:12:58 +00:00
type RecurringBookingsProps = {
eventType : SuccessProps [ "eventType" ] ;
recurringBookings : SuccessProps [ "recurringBookings" ] ;
date : dayjs.Dayjs ;
2022-11-28 18:14:01 +00:00
duration : number | undefined ;
2022-09-02 13:12:58 +00:00
is24h : boolean ;
2022-11-16 19:48:17 +00:00
allRemainingBookings : boolean ;
2022-11-25 14:49:59 +00:00
isCancelled : boolean ;
2023-03-09 10:18:22 +00:00
tz : string ;
2022-09-02 13:12:58 +00:00
} ;
export function RecurringBookings ( {
eventType ,
recurringBookings ,
2022-11-28 18:14:01 +00:00
duration ,
2022-09-02 13:12:58 +00:00
date ,
2022-11-16 19:48:17 +00:00
allRemainingBookings ,
2022-11-04 16:43:02 +00:00
is24h ,
2022-11-25 14:49:59 +00:00
isCancelled ,
2023-03-09 10:18:22 +00:00
tz ,
2022-09-02 13:12:58 +00:00
} : RecurringBookingsProps ) {
const [ moreEventsVisible , setMoreEventsVisible ] = useState ( false ) ;
2023-02-16 17:16:22 +00:00
const {
t ,
i18n : { language } ,
} = useLocale ( ) ;
2022-09-02 13:12:58 +00:00
const recurringBookingsSorted = recurringBookings
2022-11-04 16:43:02 +00:00
? recurringBookings . sort ( ( a : ConfigType , b : ConfigType ) = > ( dayjs ( a ) . isAfter ( dayjs ( b ) ) ? 1 : - 1 ) )
2022-09-02 13:12:58 +00:00
: null ;
2022-11-28 18:14:01 +00:00
if ( ! duration ) return null ;
2022-11-16 19:48:17 +00:00
if ( recurringBookingsSorted && allRemainingBookings ) {
2022-10-17 17:28:57 +00:00
return (
< >
{ eventType . recurringEvent ? . count && (
< span className = "font-medium" >
{ getEveryFreqFor ( {
t ,
recurringEvent : eventType.recurringEvent ,
recurringCount : recurringBookings?.length ? ? undefined ,
} ) }
< / span >
) }
{ eventType . recurringEvent ? . count &&
2022-11-04 16:43:02 +00:00
recurringBookingsSorted . slice ( 0 , 4 ) . map ( ( dateStr : string , idx : number ) = > (
2022-11-25 14:49:59 +00:00
< div key = { idx } className = { classNames ( "mb-2" , isCancelled ? "line-through" : "" ) } >
2023-03-09 10:18:22 +00:00
{ formatToLocalizedDate ( dayjs . tz ( dateStr , tz ) , language , "full" , tz ) }
2022-10-17 17:28:57 +00:00
< br / >
2023-03-09 10:18:22 +00:00
{ formatToLocalizedTime ( dayjs ( dateStr ) , language , undefined , ! is24h , tz ) } - { " " }
{ formatToLocalizedTime ( dayjs ( dateStr ) . add ( duration , "m" ) , language , undefined , ! is24h , tz ) } { " " }
2023-02-16 17:16:22 +00:00
< span className = "text-bookinglight" >
2023-03-09 10:18:22 +00:00
( { formatToLocalizedTimezone ( dayjs ( dateStr ) , language , tz ) } )
2023-02-16 17:16:22 +00:00
< / span >
2022-10-17 17:28:57 +00:00
< / div >
) ) }
{ recurringBookingsSorted . length > 4 && (
< Collapsible open = { moreEventsVisible } onOpenChange = { ( ) = > setMoreEventsVisible ( ! moreEventsVisible ) } >
< CollapsibleTrigger
type = "button"
className = { classNames ( "flex w-full" , moreEventsVisible ? "hidden" : "" ) } >
2022-11-15 13:39:06 +00:00
+ { t ( "plus_more" , { count : recurringBookingsSorted.length - 4 } ) }
2022-10-17 17:28:57 +00:00
< / CollapsibleTrigger >
< CollapsibleContent >
{ eventType . recurringEvent ? . count &&
2022-11-04 16:43:02 +00:00
recurringBookingsSorted . slice ( 4 ) . map ( ( dateStr : string , idx : number ) = > (
2022-11-25 14:49:59 +00:00
< div key = { idx } className = { classNames ( "mb-2" , isCancelled ? "line-through" : "" ) } >
2023-03-09 10:18:22 +00:00
{ formatToLocalizedDate ( dayjs . tz ( date , tz ) , language , "full" , tz ) }
2022-10-17 17:28:57 +00:00
< br / >
2023-03-09 10:18:22 +00:00
{ formatToLocalizedTime ( date , language , undefined , ! is24h , tz ) } - { " " }
{ formatToLocalizedTime ( dayjs ( date ) . add ( duration , "m" ) , language , undefined , ! is24h , tz ) } { " " }
2023-02-16 17:16:22 +00:00
< span className = "text-bookinglight" >
2023-03-09 10:18:22 +00:00
( { formatToLocalizedTimezone ( dayjs ( dateStr ) , language , tz ) } )
2023-02-16 17:16:22 +00:00
< / span >
2022-10-17 17:28:57 +00:00
< / div >
) ) }
< / CollapsibleContent >
< / Collapsible >
) }
< / >
) ;
}
return (
2022-11-25 14:49:59 +00:00
< div className = { classNames ( isCancelled ? "line-through" : "" ) } >
2023-03-09 10:18:22 +00:00
{ formatToLocalizedDate ( date , language , "full" , tz ) }
2022-09-02 13:12:58 +00:00
< br / >
2023-03-09 10:18:22 +00:00
{ formatToLocalizedTime ( date , language , undefined , ! is24h , tz ) } - { " " }
{ formatToLocalizedTime ( dayjs ( date ) . add ( duration , "m" ) , language , undefined , ! is24h , tz ) } { " " }
< span className = "text-bookinglight" > ( { formatToLocalizedTimezone ( date , language , tz ) } ) < / span >
2022-11-25 14:49:59 +00:00
< / div >
2022-09-02 13:12:58 +00:00
) ;
}
const getEventTypesFromDB = async ( id : number ) = > {
2023-01-12 21:09:12 +00:00
const userSelect = {
id : true ,
name : true ,
username : true ,
hideBranding : true ,
theme : true ,
brandColor : true ,
darkBrandColor : true ,
email : true ,
timeZone : true ,
} ;
2022-09-02 13:12:58 +00:00
const eventType = await prisma . eventType . findUnique ( {
where : {
id ,
} ,
select : {
id : true ,
title : true ,
description : true ,
length : true ,
eventName : true ,
recurringEvent : true ,
requiresConfirmation : true ,
userId : true ,
successRedirectUrl : true ,
2022-12-01 21:53:52 +00:00
customInputs : true ,
2022-09-02 13:12:58 +00:00
locations : true ,
2022-10-14 16:24:43 +00:00
price : true ,
currency : true ,
2023-03-02 18:15:28 +00:00
bookingFields : true ,
disableGuests : true ,
2023-03-09 10:18:22 +00:00
timeZone : true ,
2023-01-12 21:09:12 +00:00
owner : {
select : userSelect ,
} ,
2022-09-02 13:12:58 +00:00
users : {
2023-01-12 21:09:12 +00:00
select : userSelect ,
} ,
hosts : {
2022-09-02 13:12:58 +00:00
select : {
2023-01-12 21:09:12 +00:00
user : {
select : userSelect ,
} ,
2022-09-02 13:12:58 +00:00
} ,
} ,
team : {
select : {
slug : true ,
name : true ,
hideBranding : true ,
} ,
} ,
2022-11-23 22:30:54 +00:00
workflows : {
select : {
workflow : {
select : {
2023-03-02 18:15:28 +00:00
id : true ,
2022-11-23 22:30:54 +00:00
steps : true ,
} ,
} ,
} ,
} ,
2022-09-02 13:12:58 +00:00
metadata : true ,
2022-11-15 13:39:06 +00:00
seatsPerTimeSlot : true ,
2022-10-18 19:41:50 +00:00
seatsShowAttendees : true ,
2022-11-15 19:00:02 +00:00
periodStartDate : true ,
periodEndDate : true ,
2022-09-02 13:12:58 +00:00
} ,
} ) ;
if ( ! eventType ) {
return eventType ;
}
2022-10-14 16:24:43 +00:00
const metadata = EventTypeMetaDataSchema . parse ( eventType . metadata ) ;
2022-09-02 13:12:58 +00:00
return {
isDynamic : false ,
. . . eventType ,
2023-03-02 18:15:28 +00:00
bookingFields : getBookingFieldsWithSystemFields ( eventType ) ,
2022-10-14 16:24:43 +00:00
metadata ,
2022-09-02 13:12:58 +00:00
} ;
} ;
2023-04-21 13:49:53 +00:00
const handleSeatsEventTypeOnBooking = async (
2022-11-05 18:47:29 +00:00
eventType : {
2022-11-15 13:39:06 +00:00
seatsPerTimeSlot? : number | null ;
2022-11-05 18:47:29 +00:00
seatsShowAttendees : boolean | null ;
[ x : string | number | symbol ] : unknown ;
} ,
2022-12-02 00:12:06 +00:00
bookingInfo : Partial <
2023-04-21 13:49:53 +00:00
Prisma . BookingGetPayload < {
include : {
attendees : { select : { name : true ; email : true } } ;
seatsReferences : { select : { referenceUid : true } } ;
user : {
select : {
id : true ;
name : true ;
email : true ;
username : true ;
timeZone : true ;
} ;
} ;
} ;
} >
2022-11-05 18:47:29 +00:00
> ,
2023-04-21 13:49:53 +00:00
seatReferenceUid? : string ,
userId? : number
2022-11-05 18:47:29 +00:00
) = > {
if ( eventType ? . seatsPerTimeSlot !== null ) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
2022-12-02 00:12:06 +00:00
delete bookingInfo . description ;
2022-11-15 13:39:06 +00:00
} else {
return ;
2022-11-05 18:47:29 +00:00
}
2023-04-21 13:49:53 +00:00
// @TODO: If handling teams, we need to do more check ups for this.
if ( bookingInfo ? . user ? . id === userId ) {
return ;
}
2022-11-05 18:47:29 +00:00
if ( ! eventType . seatsShowAttendees ) {
2023-04-21 13:49:53 +00:00
const seatAttendee = await prisma . bookingSeat . findFirst ( {
where : {
referenceUid : seatReferenceUid ,
} ,
include : {
attendee : {
select : {
name : true ,
email : true ,
} ,
} ,
} ,
2022-12-02 00:12:06 +00:00
} ) ;
2023-04-21 13:49:53 +00:00
if ( seatAttendee ) {
const attendee = bookingInfo ? . attendees ? . find ( ( a ) = > {
return a . email === seatAttendee . attendee ? . email ;
} ) ;
bookingInfo [ "attendees" ] = attendee ? [ attendee ] : [ ] ;
} else {
bookingInfo [ "attendees" ] = [ ] ;
}
2022-11-05 18:47:29 +00:00
}
2022-12-02 00:12:06 +00:00
return bookingInfo ;
2022-11-05 18:47:29 +00:00
} ;
2022-09-02 13:12:58 +00:00
export async function getServerSideProps ( context : GetServerSidePropsContext ) {
const ssr = await ssrInit ( context ) ;
2023-04-18 12:29:26 +00:00
const session = await getServerSession ( context ) ;
let tz : string | null = null ;
if ( session ) {
const user = await ssr . viewer . me . fetch ( ) ;
tz = user . timeZone ;
}
2023-04-04 08:52:27 +00:00
const parsedQuery = querySchema . safeParse ( context . query ) ;
2023-04-18 12:29:26 +00:00
2022-09-02 13:12:58 +00:00
if ( ! parsedQuery . success ) return { notFound : true } ;
2023-04-21 13:49:53 +00:00
const { uid , eventTypeSlug , seatReferenceUid } = parsedQuery . data ;
2022-11-15 19:00:02 +00:00
2023-03-02 18:15:28 +00:00
const bookingInfoRaw = await prisma . booking . findFirst ( {
2022-11-15 19:00:02 +00:00
where : {
2023-03-14 04:19:05 +00:00
uid : await maybeGetBookingUidFromSeat ( prisma , uid ) ,
2022-11-15 19:00:02 +00:00
} ,
select : {
title : true ,
id : true ,
uid : true ,
description : true ,
customInputs : true ,
smsReminderNumber : true ,
recurringEventId : true ,
startTime : true ,
2022-11-28 18:14:01 +00:00
endTime : true ,
2022-11-15 19:00:02 +00:00
location : true ,
status : true ,
2023-02-05 14:43:37 +00:00
metadata : true ,
2022-11-16 19:48:17 +00:00
cancellationReason : true ,
2023-03-02 18:15:28 +00:00
responses : true ,
2023-02-13 12:26:28 +00:00
rejectionReason : true ,
2022-11-15 19:00:02 +00:00
user : {
select : {
id : true ,
name : true ,
email : true ,
username : true ,
2023-03-09 10:18:22 +00:00
timeZone : true ,
2022-11-15 19:00:02 +00:00
} ,
} ,
attendees : {
select : {
name : true ,
email : true ,
2023-03-09 10:18:22 +00:00
timeZone : true ,
2022-11-15 19:00:02 +00:00
} ,
} ,
eventTypeId : true ,
eventType : {
select : {
eventName : true ,
slug : true ,
2023-03-09 10:18:22 +00:00
timeZone : true ,
2022-11-15 19:00:02 +00:00
} ,
} ,
2023-03-14 04:19:05 +00:00
seatsReferences : {
select : {
referenceUid : true ,
} ,
} ,
2022-11-15 19:00:02 +00:00
} ,
} ) ;
2023-03-02 18:15:28 +00:00
if ( ! bookingInfoRaw ) {
2022-11-15 19:00:02 +00:00
return {
notFound : true ,
} ;
}
2023-03-02 18:15:28 +00:00
const eventTypeRaw = ! bookingInfoRaw . eventTypeId
2022-11-15 19:00:02 +00:00
? getDefaultEvent ( eventTypeSlug || "" )
2023-03-02 18:15:28 +00:00
: await getEventTypesFromDB ( bookingInfoRaw . eventTypeId ) ;
2022-09-02 13:12:58 +00:00
if ( ! eventTypeRaw ) {
return {
notFound : true ,
} ;
}
2023-03-27 08:27:10 +00:00
const bookingInfo = getBookingWithResponses ( bookingInfoRaw ) ;
2023-03-02 18:15:28 +00:00
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo [ "startTime" ] = ( bookingInfo ? . startTime as Date ) ? . toISOString ( ) as unknown as Date ;
bookingInfo [ "endTime" ] = ( bookingInfo ? . endTime as Date ) ? . toISOString ( ) as unknown as Date ;
2023-01-12 21:09:12 +00:00
eventTypeRaw . users = ! ! eventTypeRaw . hosts ? . length
? eventTypeRaw . hosts . map ( ( host ) = > host . user )
: eventTypeRaw . users ;
2022-09-02 13:12:58 +00:00
if ( ! eventTypeRaw . users . length ) {
2023-01-12 21:09:12 +00:00
if ( ! eventTypeRaw . owner )
return {
notFound : true ,
} ;
eventTypeRaw . users . push ( {
. . . eventTypeRaw . owner ,
} ) ;
2022-09-02 13:12:58 +00:00
}
const eventType = {
. . . eventTypeRaw ,
2022-11-15 19:00:02 +00:00
periodStartDate : eventTypeRaw.periodStartDate?.toString ( ) ? ? null ,
periodEndDate : eventTypeRaw.periodEndDate?.toString ( ) ? ? null ,
2022-10-14 16:24:43 +00:00
metadata : EventTypeMetaDataSchema.parse ( eventTypeRaw . metadata ) ,
2022-09-02 13:12:58 +00:00
recurringEvent : parseRecurringEvent ( eventTypeRaw . recurringEvent ) ,
2022-12-01 21:53:52 +00:00
customInputs : customInputSchema.array ( ) . parse ( eventTypeRaw . customInputs ) ,
2022-09-02 13:12:58 +00:00
} ;
const profile = {
name : eventType.team?.name || eventType . users [ 0 ] ? . name || null ,
email : eventType.team ? null : eventType . users [ 0 ] . email || null ,
theme : ( ! eventType . team ? . name && eventType . users [ 0 ] ? . theme ) || null ,
brandColor : eventType.team ? null : eventType . users [ 0 ] . brandColor || null ,
darkBrandColor : eventType.team ? null : eventType . users [ 0 ] . darkBrandColor || null ,
slug : eventType.team?.slug || eventType . users [ 0 ] ? . username || null ,
} ;
2023-04-21 13:49:53 +00:00
if ( bookingInfo !== null && eventType . seatsPerTimeSlot ) {
await handleSeatsEventTypeOnBooking ( eventType , bookingInfo , seatReferenceUid , session ? . user . id ) ;
2022-11-05 18:47:29 +00:00
}
2023-02-13 12:26:28 +00:00
const payment = await prisma . payment . findFirst ( {
where : {
bookingId : bookingInfo.id ,
} ,
select : {
success : true ,
refunded : true ,
} ,
} ) ;
2022-09-02 13:12:58 +00:00
return {
props : {
2023-05-16 19:41:47 +00:00
themeBasis : eventType.team ? eventType.team.slug : eventType.users [ 0 ] ? . username ,
2022-09-15 09:33:34 +00:00
hideBranding : eventType.team ? eventType.team.hideBranding : eventType.users [ 0 ] . hideBranding ,
2022-09-02 13:12:58 +00:00
profile ,
eventType ,
2023-03-14 04:19:05 +00:00
recurringBookings : await getRecurringBookings ( bookingInfo . recurringEventId ) ,
2022-09-02 13:12:58 +00:00
trpcState : ssr.dehydrate ( ) ,
2022-11-15 19:00:02 +00:00
dynamicEventName : bookingInfo?.eventType?.eventName || "" ,
2022-09-02 13:12:58 +00:00
bookingInfo ,
2023-03-14 04:19:05 +00:00
paymentStatus : payment ,
2023-04-18 12:29:26 +00:00
. . . ( tz && { tz } ) ,
2022-09-02 13:12:58 +00:00
} ,
} ;
}
2023-03-14 04:19:05 +00:00
async function getRecurringBookings ( recurringEventId : string | null ) {
if ( ! recurringEventId ) return null ;
const recurringBookings = await prisma . booking . findMany ( {
where : {
recurringEventId ,
} ,
select : {
startTime : true ,
} ,
} ) ;
return recurringBookings . map ( ( obj ) = > obj . startTime . toString ( ) ) ;
}