bookito-widget 0.42.16-beta.2 → 0.42.16-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,470 +0,0 @@
1
- import { formatPrice, useApp } from '@carlito/api';
2
- import {
3
- ListBookingsResponse,
4
- LocalizedEventData,
5
- findOccurrenceForDate,
6
- useCreateBooking,
7
- usePmsEventBooking,
8
- } from '@carlito/bookito-client';
9
- import { MultiLangTextarea, SlideAnimation } from '@carlito/dashboard-ui';
10
- import { applyTimeOfDate } from '@carlito/date-utils';
11
- import { useGetHotelConfiguration, useLoginGuest } from '@carlito/hub-react';
12
- import { SUPPORTED_LANGUAGE_CODES, SupportedLanguages } from '@carlito/intl';
13
- import {
14
- BookingParams,
15
- BookitoGuestChip,
16
- BookitoGuestForm,
17
- BookitoParticipantsPicker,
18
- BookitoStepperBookingRequestForm,
19
- FormAlert,
20
- QuantityPicker,
21
- } from '@carlito/shared-ui';
22
- import { Formik, useFormikContext } from 'formik';
23
- import { FormikHelpers } from 'formik/dist/types';
24
- import humanizeDuration from 'humanize-duration';
25
- import { Fragment, useCallback, useState } from 'react';
26
- import { Trans, useTranslation } from 'react-i18next';
27
- import { IoArrowBack } from 'react-icons/io5';
28
- import FormSubmitButton from './FormSubmitButton';
29
- import { isEventAllDay } from './helper';
30
- type InitialBookingData = ListBookingsResponse['bookings'][0];
31
- type FormValues = Partial<InitialBookingData> & {
32
- selectedTimeSlot?: {
33
- startDateTime: string;
34
- numOfConfirmedParticipants?: number;
35
- };
36
- bookingRef?: string;
37
- birthDate?: string;
38
- };
39
- type EventInfoProps = {
40
- price?: number | null;
41
- overallDuration?: number | null;
42
- stringifiedDuration: string;
43
- activeLanguage: SupportedLanguages;
44
- isAllDay?: boolean;
45
- };
46
-
47
- export type BookingFormProps = {
48
- event: LocalizedEventData;
49
- date: Date;
50
- authId: string;
51
- availableOnDates: string[];
52
- shouldPreselectDateInCalendar?: boolean;
53
- };
54
-
55
- const supportedLanguages: Record<SupportedLanguages, string> = {
56
- en: 'English',
57
- de: 'Deutsch',
58
- it: 'Italiano',
59
- fr: 'Français',
60
- pl: 'Polski',
61
- };
62
-
63
- function getUserLanguage() {
64
- const browserLanguages = [...navigator.languages];
65
- const selectedLanugage = SUPPORTED_LANGUAGE_CODES.find((availableLanguage) =>
66
- browserLanguages.find((browserLanguage) => browserLanguage.includes(availableLanguage))
67
- );
68
-
69
- if (!selectedLanugage) {
70
- return 'en';
71
- }
72
-
73
- return selectedLanugage;
74
- }
75
-
76
- export const ChooseLanguage = () => {
77
- const { getFieldProps } = useFormikContext();
78
-
79
- const fieldName = `bookedBy.communicationLanguage`;
80
-
81
- const { value: language, onBlur, onChange } = getFieldProps<SupportedLanguages>(fieldName);
82
-
83
- return (
84
- <select
85
- className="text-sm mt-4 mb-6 rounded-lg outline-none border bg-light-default border-gray-300 focus:border-primary-default focus:ring-2 focus:ring-primary-default focus:ring-offset-1"
86
- value={language}
87
- onChange={onChange}
88
- onBlur={onBlur}
89
- name={fieldName}
90
- >
91
- {Object.entries(supportedLanguages).map(([key, value]) => (
92
- <option key={key} value={key}>
93
- {value}
94
- </option>
95
- ))}
96
- </select>
97
- );
98
- };
99
-
100
- const EventInfo = ({
101
- price,
102
- overallDuration,
103
- stringifiedDuration,
104
- activeLanguage,
105
- isAllDay,
106
- }: EventInfoProps) => {
107
- const { hotelCurrency } = useApp();
108
- return (
109
- <div className="flex justify-between items-start gap-2">
110
- {price && price !== null ? (
111
- <div>
112
- <p className="font-bold text-base">
113
- <Trans i18nKey="price" />
114
- </p>
115
- <p className="text-4xl font-bold">{formatPrice(price, activeLanguage, hotelCurrency)}</p>
116
- </div>
117
- ) : (
118
- <div />
119
- )}
120
- {overallDuration && (
121
- <div className="text-right bookito-form__duration">
122
- <p className="font-bold text-base">
123
- <Trans i18nKey="duration" />
124
- </p>
125
- {isAllDay ? (
126
- <p className="text-4xl font-normal">
127
- <Trans i18nKey="all-day" />
128
- </p>
129
- ) : (
130
- <p className="text-4xl font-normal">{stringifiedDuration}</p>
131
- )}
132
- </div>
133
- )}
134
- </div>
135
- );
136
- };
137
-
138
- export const BookingForm = ({
139
- event: eventParams,
140
- date: onDate,
141
- authId,
142
- availableOnDates,
143
- shouldPreselectDateInCalendar = true,
144
- }: BookingFormProps) => {
145
- const { mutate: createBooking, isLoading, isSuccess, reset: resetMutation } = useCreateBooking();
146
- const userLangauge = getUserLanguage();
147
- const { t } = useTranslation();
148
-
149
- const hotelConfig = useGetHotelConfiguration({
150
- hotelId: authId,
151
- });
152
-
153
- const guestLoginMutation = useLoginGuest();
154
-
155
- // TODO: Display loading state
156
- const settingsLoading = hotelConfig.isLoading;
157
- const hasPms = hotelConfig.hasPms;
158
-
159
- const initialValues: FormValues = {
160
- participants: [
161
- {
162
- name: '',
163
- email: '',
164
- roomNr: '',
165
- phoneNr: '',
166
- communicationLanguage: userLangauge,
167
- isMainBooker: true,
168
- isExternal: true,
169
- },
170
- ],
171
- comment: '',
172
- selectedTimeSlot: {
173
- startDateTime: '',
174
- numOfConfirmedParticipants: 0,
175
- },
176
- bookingRef: '',
177
- };
178
-
179
- const occurrence = findOccurrenceForDate(onDate, eventParams) ?? eventParams?.occurrences[0];
180
-
181
- const overallDuration =
182
- (eventParams?.timeBookability === 'custom'
183
- ? eventParams?.timeBookabilitySettings?.timeBlockDurationMs
184
- : occurrence?.durationMs) ?? occurrence.durationMs;
185
-
186
- const stringifiedDuration = humanizeDuration(overallDuration ?? 0, {
187
- language: 'shortDe',
188
- // fallbacks: ['en'],
189
- languages: {
190
- shortDe: {
191
- y: () => 'y',
192
- mo: () => 'mo',
193
- w: () => 'w',
194
- d: () => 'd',
195
- h: () => 'h',
196
- m: () => 'm',
197
- s: () => 's',
198
- ms: () => 'ms',
199
- },
200
- },
201
- units: ['h', 'm'],
202
- maxDecimalPoints: 0,
203
- delimiter: ' ',
204
- } as any);
205
-
206
- const startDateTime = applyTimeOfDate(onDate, new Date(occurrence.startDateTime));
207
- const isAllDay = eventParams ? isEventAllDay(startDateTime, eventParams) : undefined;
208
-
209
- const {
210
- loadGuestError,
211
- setSelectedParticipants,
212
- allParticipants,
213
- selectedParticipants,
214
- isLoadingGuest,
215
- loginGuest,
216
- resetValues,
217
- } = usePmsEventBooking({ initialBookingData: initialValues });
218
- const [isSelectingParticipant, setIsSelectingParticipant] = useState(false);
219
-
220
- async function onSubmit(
221
- values: FormValues,
222
- { resetForm }: Pick<FormikHelpers<FormValues>, 'resetForm'>
223
- ) {
224
- const guest = values.participants?.[0];
225
-
226
- let carlitoHubUserId: string | undefined = undefined;
227
-
228
- if (hasPms && !isSelectingParticipant) {
229
- if (guest && (guest.roomNr || values.bookingRef)) {
230
- const { isLoggedIn, loggedInUserId, hasMultipleParticipants } = await loginGuest({
231
- hotelId: authId,
232
- birthDate: values.birthDate,
233
- bookingReference: values.bookingRef,
234
- roomNumber: guest.roomNr,
235
- });
236
- if (!isLoggedIn) {
237
- return;
238
- } else if (hasMultipleParticipants) {
239
- return setIsSelectingParticipant(true);
240
- } else {
241
- carlitoHubUserId = loggedInUserId;
242
- }
243
- }
244
- }
245
-
246
- const participants =
247
- selectedParticipants.length > 0 && hasPms
248
- ? allParticipants
249
- .filter((el) => selectedParticipants.includes(el.id))
250
- .map((participant) => ({
251
- carlitoHubUserId: participant.id,
252
- isMainBooker: participant.isMainBooker,
253
- }))
254
- : [
255
- {
256
- ...guest,
257
- carlitoHubUserId,
258
- },
259
- ];
260
-
261
- createBooking(
262
- {
263
- bookedForDate: values.bookedForDate ? values.bookedForDate : startDateTime.toISOString(),
264
- eventId: eventParams.id,
265
- comment: values?.comment,
266
- priceVariants: values.priceVariants,
267
- numOfAdditionalParticipants: values.numOfAdditionalParticipants,
268
- participants,
269
- bookingSource: 'web_widget',
270
- },
271
- {
272
- onSuccess: () => {
273
- resetForm();
274
- },
275
- }
276
- );
277
- }
278
-
279
- const onSuccessAnimationEnd = useCallback(() => {
280
- resetMutation();
281
- setIsSelectingParticipant(false);
282
- resetValues();
283
- }, [resetMutation]);
284
-
285
- const priceVariants = eventParams.priceVariants?.map((priceVariant) => {
286
- return {
287
- id: priceVariant.id,
288
- displayName: priceVariant.description ?? '',
289
- price: priceVariant.priceVariation.amount,
290
- };
291
- });
292
- const [isTimeSelected, setTimeSelectedValue] = useState<boolean>(false);
293
- const [showGuestForm, updateGuestFormDisplayState] = useState<boolean>(false);
294
-
295
- const isTimeSlotEnabled =
296
- eventParams.timeBookability === 'custom' &&
297
- !!eventParams.timeBookabilitySettings?.timeBlockDurationMs;
298
-
299
- return (
300
- <Formik initialValues={initialValues} onSubmit={onSubmit}>
301
- {(props) => {
302
- const {
303
- values,
304
- resetForm,
305
- isSubmitting,
306
- dirty,
307
- handleChange,
308
- handleBlur,
309
- handleSubmit,
310
- setValues,
311
- } = props;
312
-
313
- // eslint-disable-next-line react-hooks/rules-of-hooks
314
- const onBookingParamsChange = useCallback(
315
- (requestParams: BookingParams) => {
316
- setValues({
317
- ...values,
318
- numOfAdditionalParticipants:
319
- (requestParams.numParticipants ?? 0) > 1
320
- ? (requestParams.numParticipants ?? 0) - 1
321
- : undefined,
322
- priceVariants: requestParams.priceVariants,
323
- bookedForDate: requestParams.selectedTimeSlot?.startDateTime
324
- ? requestParams.selectedTimeSlot.startDateTime
325
- : requestParams.selectedDateTime?.toISOString(),
326
- comment: requestParams.comment,
327
- selectedTimeSlot: requestParams.selectedTimeSlot,
328
- participants: [],
329
- });
330
- if (requestParams.selectedTimeSlot?.startDateTime) {
331
- setTimeSelectedValue(true);
332
- }
333
- },
334
- [setValues, values]
335
- );
336
-
337
- const roomNr = values.participants?.[0]?.roomNr; // bookedBy?.roomNr
338
- const bookingRef = values.bookingRef;
339
- const shouldHideExtendedForm = hasPms && (roomNr || bookingRef);
340
-
341
- const shouldShowBirthdateInput = shouldHideExtendedForm;
342
-
343
- const bookedBy = values.participants?.[0];
344
-
345
- console.log(values);
346
-
347
- return (
348
- <form className="bookito-form flex mt-10 flex-col min-h-[65vh]" onSubmit={handleSubmit}>
349
- <EventInfo
350
- price={eventParams.basePrice?.amount || 0}
351
- activeLanguage={bookedBy?.communicationLanguage ?? 'en'}
352
- isAllDay={isAllDay}
353
- stringifiedDuration={stringifiedDuration}
354
- overallDuration={overallDuration}
355
- />
356
-
357
- <div className="mt-12 flex flex-col">
358
- <BookitoStepperBookingRequestForm
359
- showExtraFields={false}
360
- eventPriceVariants={priceVariants}
361
- isTimeSlotEnabledEvent={isTimeSlotEnabled}
362
- bookingParams={{
363
- selectedDateTime: values.bookedForDate
364
- ? new Date(values.bookedForDate)
365
- : shouldPreselectDateInCalendar
366
- ? startDateTime
367
- : undefined,
368
- numParticipants: (values.numOfAdditionalParticipants ?? 0) + 1,
369
- priceVariants: values.priceVariants,
370
- selectedTimeSlot: values.selectedTimeSlot,
371
- }}
372
- updateBookingParams={onBookingParamsChange}
373
- maxParticipants={eventParams.maxParticipants}
374
- availableOnDates={availableOnDates}
375
- onSubmitBooking={() => {
376
- onSubmit(values, { resetForm });
377
- }}
378
- eventInfo={{
379
- id: eventParams.id,
380
- occurrences: eventParams.occurrences,
381
- bookability: eventParams.bookability,
382
- timeslotDuration: eventParams.timeBookabilitySettings?.timeBlockDurationMs ?? 0,
383
- }}
384
- updateSuccessMsg={() => {
385
- // no-op
386
- }}
387
- ownerAuthId={authId}
388
- userLanguage={userLangauge}
389
- updateGuestFormDisplayState={updateGuestFormDisplayState}
390
- />
391
-
392
- {showGuestForm && (
393
- <div className="mt-2 flex flex-col">
394
- <h4 className=" font-bold text-xl mb-2 ">
395
- <Trans i18nKey="booking-details" />
396
- </h4>
397
- {loadGuestError && (
398
- <FormAlert title={t('error-while-booking')} errors={[loadGuestError]} />
399
- )}
400
- <div className={'relative w-full'}>
401
- <SlideAnimation
402
- view={isSelectingParticipant ? 1 : 0}
403
- direction={isSelectingParticipant ? 'Right' : 'Left'}
404
- >
405
- <div className={'pb-5'}>
406
- {isSelectingParticipant ? (
407
- <Fragment>
408
- <div
409
- onClick={() => {
410
- setIsSelectingParticipant(false);
411
- }}
412
- role={'button'}
413
- className={
414
- 'bg-secondary-default text-secondary-contrast h-8 w-8 flex justify-center items-center rounded-full'
415
- }
416
- >
417
- <IoArrowBack />
418
- </div>
419
- <BookitoParticipantsPicker
420
- renderGuestChip={(props) => <BookitoGuestChip {...props} />}
421
- participants={allParticipants}
422
- selectedParticipants={selectedParticipants}
423
- onParticipantsChange={setSelectedParticipants}
424
- />
425
- <QuantityPicker
426
- count={values.numOfAdditionalParticipants ?? 0}
427
- onCountChange={() => {
428
- setValues({
429
- ...values,
430
- numOfAdditionalParticipants:
431
- (values.numOfAdditionalParticipants ?? 0) + 1,
432
- });
433
- }}
434
- />
435
- </Fragment>
436
- ) : (
437
- <div>
438
- <BookitoGuestForm hasPms={hasPms} />
439
- <div className="space-y-4">
440
- <MultiLangTextarea name="comment" label={t('comment')} />
441
- <ChooseLanguage />
442
- </div>
443
- </div>
444
- )}
445
- <div className="flex-1 my-4">
446
- <FormSubmitButton
447
- onSuccessAnimationEnd={onSuccessAnimationEnd}
448
- success={isSuccess}
449
- loading={isLoading}
450
- disabled={!dirty || isSubmitting || !isTimeSelected}
451
- >
452
- {hasPms && !isSelectingParticipant ? (
453
- <Trans i18nKey="next" />
454
- ) : (
455
- <Trans i18nKey="submit" />
456
- )}
457
- </FormSubmitButton>
458
- </div>
459
- </div>
460
- </SlideAnimation>
461
- </div>
462
- </div>
463
- )}
464
- </div>
465
- </form>
466
- );
467
- }}
468
- </Formik>
469
- );
470
- };