@asksable/site-connector 0.3.1 → 0.4.0
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.
- package/README.md +66 -7
- package/dist/analytics.d.ts +18 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +94 -0
- package/dist/analytics.js.map +1 -0
- package/dist/booking-widget-placeholder.d.ts +5 -0
- package/dist/booking-widget-placeholder.d.ts.map +1 -0
- package/dist/booking-widget-placeholder.js +9 -0
- package/dist/booking-widget-placeholder.js.map +1 -0
- package/dist/booking-widget.d.ts.map +1 -1
- package/dist/booking-widget.js +234 -60
- package/dist/booking-widget.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +4 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +70 -2
- package/dist/provider.js.map +1 -1
- package/dist/styles.css +239 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -2
package/dist/booking-widget.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { loadStripe } from '@stripe/stripe-js';
|
|
5
|
-
import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
5
|
+
import { Elements, PaymentElement, useElements, useStripe, } from '@stripe/react-stripe-js';
|
|
6
6
|
import { Skeleton } from 'boneyard-js/react';
|
|
7
7
|
// Side-effect: registers all named bones with the boneyard runtime.
|
|
8
8
|
// Regenerate by running `npx boneyard-js build` against a host site
|
|
9
9
|
// that mounts <BookingWidgetPanel /> in its loaded state.
|
|
10
10
|
import './bones/registry.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { BookingWidgetPlaceholder } from './booking-widget-placeholder.js';
|
|
12
|
+
import { useSableSiteAnalytics, useSableSiteClient, useSableSiteConfig, useTranslation, } from './provider.js';
|
|
13
|
+
import { DEFAULT_LOCALE, localeToIntl, pickLocaleField, } from './translations.js';
|
|
13
14
|
const stripePromises = new Map();
|
|
14
15
|
function getStripePromise(publishableKey, connectAccountId) {
|
|
15
16
|
const key = `${publishableKey}:${connectAccountId}`;
|
|
16
17
|
const cached = stripePromises.get(key);
|
|
17
18
|
if (cached)
|
|
18
19
|
return cached;
|
|
19
|
-
const promise = loadStripe(publishableKey, {
|
|
20
|
+
const promise = loadStripe(publishableKey, {
|
|
21
|
+
stripeAccount: connectAccountId,
|
|
22
|
+
});
|
|
20
23
|
stripePromises.set(key, promise);
|
|
21
24
|
return promise;
|
|
22
25
|
}
|
|
@@ -51,6 +54,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
51
54
|
const client = useSableSiteClient();
|
|
52
55
|
const { siteSlug } = useSableSiteConfig();
|
|
53
56
|
const { t, locale } = useTranslation();
|
|
57
|
+
const analytics = useSableSiteAnalytics();
|
|
54
58
|
const intlLocale = localeToIntl(locale);
|
|
55
59
|
const [setup, setSetup] = useState(null);
|
|
56
60
|
const [isSetupLoading, setIsSetupLoading] = useState(true);
|
|
@@ -103,7 +107,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
103
107
|
// Dev-only: force viewState to 'details' when previewing a payment
|
|
104
108
|
// variant, since the payment panel renders inside the details form.
|
|
105
109
|
useEffect(() => {
|
|
106
|
-
|
|
110
|
+
analytics.capture('sable_booking_widget_viewed', { mode });
|
|
111
|
+
}, [analytics, mode]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (__devForceState === 'payment-full' ||
|
|
114
|
+
__devForceState === 'payment-deposit') {
|
|
107
115
|
setViewState('details');
|
|
108
116
|
}
|
|
109
117
|
}, [__devForceState]);
|
|
@@ -176,7 +184,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
176
184
|
})
|
|
177
185
|
.catch((nextError) => {
|
|
178
186
|
if (!cancelled) {
|
|
179
|
-
setError(nextError instanceof Error
|
|
187
|
+
setError(nextError instanceof Error
|
|
188
|
+
? nextError.message
|
|
189
|
+
: 'Unable to load booking setup.');
|
|
180
190
|
}
|
|
181
191
|
})
|
|
182
192
|
.finally(() => {
|
|
@@ -188,9 +198,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
188
198
|
cancelled = true;
|
|
189
199
|
};
|
|
190
200
|
}, [client, siteSlug]);
|
|
191
|
-
const selectedService = useMemo(() => setup?.services.find((service) => service._id === selectedServiceId) ??
|
|
201
|
+
const selectedService = useMemo(() => setup?.services.find((service) => service._id === selectedServiceId) ??
|
|
202
|
+
null, [selectedServiceId, setup]);
|
|
192
203
|
const selectedServiceRequiresSlot = selectedService?.publicBookingMode !== 'async';
|
|
193
|
-
const selectedServicePath = selectedServiceRequiresSlot
|
|
204
|
+
const selectedServicePath = selectedServiceRequiresSlot
|
|
205
|
+
? 'scheduled'
|
|
206
|
+
: 'async';
|
|
194
207
|
const selectedServiceHasIntakeForm = hasIntakeFormSections(selectedService);
|
|
195
208
|
const isIntakeComplete = useMemo(() => !selectedService?.intakeForm ||
|
|
196
209
|
areRequiredIntakeFieldsComplete(selectedService.intakeForm, intakeResponses, selectedServicePath), [intakeResponses, selectedService?.intakeForm, selectedServicePath]);
|
|
@@ -216,7 +229,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
216
229
|
const uncategorizedKey = '__uncategorized__';
|
|
217
230
|
for (const service of visibleServices) {
|
|
218
231
|
const category = service.categoryId != null
|
|
219
|
-
? categoriesById.get(service.categoryId) ?? null
|
|
232
|
+
? (categoriesById.get(service.categoryId) ?? null)
|
|
220
233
|
: null;
|
|
221
234
|
const key = category?._id ?? uncategorizedKey;
|
|
222
235
|
const existing = groups.get(key);
|
|
@@ -285,7 +298,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
285
298
|
// wiped on the first render before the staff list arrives.
|
|
286
299
|
if (availableStaff.length === 0)
|
|
287
300
|
return;
|
|
288
|
-
if (selectedStaffId &&
|
|
301
|
+
if (selectedStaffId &&
|
|
302
|
+
!availableStaff.some((staffMember) => staffMember._id === selectedStaffId)) {
|
|
289
303
|
setSelectedStaffId(null);
|
|
290
304
|
}
|
|
291
305
|
}, [availableStaff, selectedStaffId]);
|
|
@@ -350,7 +364,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
350
364
|
if (!cancelled) {
|
|
351
365
|
setAvailabilityByDate(new Map());
|
|
352
366
|
setSelectedDate(null);
|
|
353
|
-
setError(nextError instanceof Error
|
|
367
|
+
setError(nextError instanceof Error
|
|
368
|
+
? nextError.message
|
|
369
|
+
: t('calendarLoadError'));
|
|
354
370
|
}
|
|
355
371
|
})
|
|
356
372
|
.finally(() => {
|
|
@@ -361,7 +377,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
361
377
|
return () => {
|
|
362
378
|
cancelled = true;
|
|
363
379
|
};
|
|
364
|
-
}, [
|
|
380
|
+
}, [
|
|
381
|
+
calendarMonth,
|
|
382
|
+
client,
|
|
383
|
+
selectedDate,
|
|
384
|
+
selectedService?.publicBookingMode,
|
|
385
|
+
selectedServiceId,
|
|
386
|
+
selectedStaffId,
|
|
387
|
+
siteSlug,
|
|
388
|
+
]);
|
|
365
389
|
useEffect(() => {
|
|
366
390
|
if (!selectedServiceId || selectedService?.publicBookingMode === 'async') {
|
|
367
391
|
return;
|
|
@@ -374,7 +398,14 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
374
398
|
calendarMonth: nextMonth,
|
|
375
399
|
cacheRef: availabilityCacheRef,
|
|
376
400
|
});
|
|
377
|
-
}, [
|
|
401
|
+
}, [
|
|
402
|
+
calendarMonth,
|
|
403
|
+
client,
|
|
404
|
+
selectedService?.publicBookingMode,
|
|
405
|
+
selectedServiceId,
|
|
406
|
+
selectedStaffId,
|
|
407
|
+
siteSlug,
|
|
408
|
+
]);
|
|
378
409
|
useEffect(() => {
|
|
379
410
|
setSelectedSlot(null);
|
|
380
411
|
setPendingSlotKey(null);
|
|
@@ -393,7 +424,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
393
424
|
else {
|
|
394
425
|
setViewState('slots');
|
|
395
426
|
}
|
|
396
|
-
}, [
|
|
427
|
+
}, [
|
|
428
|
+
selectedDate,
|
|
429
|
+
selectedService?.publicBookingMode,
|
|
430
|
+
selectedServiceId,
|
|
431
|
+
selectedStaffId,
|
|
432
|
+
]);
|
|
397
433
|
useEffect(() => {
|
|
398
434
|
setIntakeResponses({});
|
|
399
435
|
setQuote(null);
|
|
@@ -423,7 +459,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
423
459
|
})
|
|
424
460
|
.catch((nextError) => {
|
|
425
461
|
if (!cancelled) {
|
|
426
|
-
setError(nextError instanceof Error
|
|
462
|
+
setError(nextError instanceof Error
|
|
463
|
+
? nextError.message
|
|
464
|
+
: 'Unable to calculate price.');
|
|
427
465
|
}
|
|
428
466
|
})
|
|
429
467
|
.finally(() => {
|
|
@@ -435,7 +473,14 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
435
473
|
cancelled = true;
|
|
436
474
|
window.clearTimeout(handle);
|
|
437
475
|
};
|
|
438
|
-
}, [
|
|
476
|
+
}, [
|
|
477
|
+
client,
|
|
478
|
+
intakeResponses,
|
|
479
|
+
isIntakeComplete,
|
|
480
|
+
selectedService?.pricingMode,
|
|
481
|
+
selectedServiceId,
|
|
482
|
+
siteSlug,
|
|
483
|
+
]);
|
|
439
484
|
useEffect(() => {
|
|
440
485
|
return () => {
|
|
441
486
|
if (!holdId) {
|
|
@@ -575,9 +620,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
575
620
|
}
|
|
576
621
|
return set;
|
|
577
622
|
}, [availabilityByDate, selectedDate]);
|
|
578
|
-
const selectedDateSlots = selectedDate
|
|
579
|
-
|
|
580
|
-
|
|
623
|
+
const selectedDateSlots = selectedDate
|
|
624
|
+
? (filteredAvailabilityByDate.get(selectedDate) ?? [])
|
|
625
|
+
: [];
|
|
626
|
+
const selectedHeldStaff = availableStaff.find((staffMember) => staffMember._id === heldStaffId) ??
|
|
627
|
+
selectedStaff ??
|
|
628
|
+
null;
|
|
629
|
+
const holdSecondsRemaining = holdExpiresAt !== null
|
|
630
|
+
? Math.max(0, Math.floor((holdExpiresAt - holdNow) / 1000))
|
|
631
|
+
: null;
|
|
581
632
|
const monthOptions = useMemo(() => getMonthOptions(calendarMonth, 4, intlLocale), [calendarMonth, intlLocale]);
|
|
582
633
|
const calendarDays = useMemo(() => buildCalendarDays(calendarMonth), [calendarMonth]);
|
|
583
634
|
// Mobile 4-step gates. canAdvanceStepN = "the user can move PAST
|
|
@@ -622,11 +673,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
622
673
|
const slotKey = `${slot.time}-${slot.endTime}`;
|
|
623
674
|
const isPending = pendingSlotKey === slotKey;
|
|
624
675
|
const isActive = isPending ||
|
|
625
|
-
(selectedSlot?.time === slot.time &&
|
|
626
|
-
|
|
676
|
+
(selectedSlot?.time === slot.time &&
|
|
677
|
+
selectedSlot?.endTime === slot.endTime);
|
|
678
|
+
return (_jsx("button", { type: "button", className: `bw-slot${isActive ? ' is-active' : ''}${isPending ? ' is-pending' : ''}`, style: { '--bw-slot-i': index }, onClick: () => void handleSlotSelect(slot), disabled: isSubmitting, children: isPending
|
|
679
|
+
? t('slotSecuring')
|
|
680
|
+
: formatTimeLabel(slot.time, intlLocale) }, slotKey));
|
|
627
681
|
}) })) : (_jsx("p", { className: "bw-no-slots", children: t('slotsEmptyForDate') }))) : null }, slotsAreaKey));
|
|
628
682
|
const canSubmit = Boolean(selectedService &&
|
|
629
|
-
(!selectedServiceRequiresSlot ||
|
|
683
|
+
(!selectedServiceRequiresSlot ||
|
|
684
|
+
(selectedDate && selectedSlot && holdId)) &&
|
|
630
685
|
isIntakeComplete &&
|
|
631
686
|
(selectedService.pricingMode !== 'calculated' || quote) &&
|
|
632
687
|
customerName.trim() &&
|
|
@@ -667,7 +722,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
667
722
|
return null;
|
|
668
723
|
const visibleBlockers = submitBlockers.slice(0, 5);
|
|
669
724
|
const hiddenCount = submitBlockers.length - visibleBlockers.length;
|
|
670
|
-
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isQuoteLoading && visibleBlockers.length > 0 ? (_jsxs("ul", { children: [visibleBlockers.map((blocker) => (_jsx("li", { children: blocker }, blocker))), hiddenCount > 0 ? _jsx("li", { children: t('helpMoreFields', { count: hiddenCount }) }) : null] })) : null] }));
|
|
725
|
+
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isQuoteLoading && visibleBlockers.length > 0 ? (_jsxs("ul", { children: [visibleBlockers.map((blocker) => (_jsx("li", { children: blocker }, blocker))), hiddenCount > 0 ? (_jsx("li", { children: t('helpMoreFields', { count: hiddenCount }) })) : null] })) : null] }));
|
|
671
726
|
}
|
|
672
727
|
function renderContactFields(idSuffix) {
|
|
673
728
|
const nameId = `bw-name${idSuffix}`;
|
|
@@ -688,6 +743,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
688
743
|
return (_jsxs(_Fragment, { children: [contactFields, intakeFields] }));
|
|
689
744
|
}
|
|
690
745
|
function handleServiceSelect(service) {
|
|
746
|
+
analytics.capture('sable_booking_service_selected', {
|
|
747
|
+
service_id: service._id,
|
|
748
|
+
service_name: pickLocaleField(service, 'name', locale) ?? service.name,
|
|
749
|
+
booking_mode: service.publicBookingMode ?? 'scheduled',
|
|
750
|
+
requires_payment: service.requiresPayment === true,
|
|
751
|
+
});
|
|
691
752
|
if (holdId) {
|
|
692
753
|
void client
|
|
693
754
|
.releasePublicBookingHold({ siteSlug, holdId, sessionToken })
|
|
@@ -809,7 +870,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
809
870
|
async function handleConfirmBooking() {
|
|
810
871
|
if (!selectedServiceId ||
|
|
811
872
|
!selectedService ||
|
|
812
|
-
(selectedServiceRequiresSlot &&
|
|
873
|
+
(selectedServiceRequiresSlot &&
|
|
874
|
+
(!selectedDate || !selectedSlot || !holdId))) {
|
|
813
875
|
return;
|
|
814
876
|
}
|
|
815
877
|
setIsSubmitting(true);
|
|
@@ -831,6 +893,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
831
893
|
try {
|
|
832
894
|
await onRescheduleSubmit({ newStartTime, newEndTime });
|
|
833
895
|
setSuccess(t('successRescheduleConfirmed'));
|
|
896
|
+
analytics.capture('sable_booking_rescheduled', {
|
|
897
|
+
service_id: selectedServiceId,
|
|
898
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
899
|
+
selectedService.name,
|
|
900
|
+
});
|
|
834
901
|
setMobileStep(4);
|
|
835
902
|
}
|
|
836
903
|
catch (nextError) {
|
|
@@ -848,11 +915,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
848
915
|
siteSlug,
|
|
849
916
|
serviceId: selectedServiceId,
|
|
850
917
|
staffMemberId: selectedServiceRequiresSlot
|
|
851
|
-
? heldStaffId ??
|
|
918
|
+
? (heldStaffId ??
|
|
919
|
+
selectedStaffId ??
|
|
920
|
+
selectedSlot?.availableStaffIds[0])
|
|
852
921
|
: undefined,
|
|
853
922
|
startTime: newStartTime,
|
|
854
923
|
endTime: newEndTime,
|
|
855
|
-
timezone: selectedHeldStaff?.timezone ??
|
|
924
|
+
timezone: selectedHeldStaff?.timezone ??
|
|
925
|
+
availableStaff[0]?.timezone ??
|
|
926
|
+
setup?.workspaceTimezone ??
|
|
927
|
+
'UTC',
|
|
856
928
|
deliveryMethod: selectedService.deliveryMethods[0] ?? 'in-person',
|
|
857
929
|
customerName: customerName.trim(),
|
|
858
930
|
customerEmail: customerEmail.trim(),
|
|
@@ -861,15 +933,34 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
861
933
|
intakeResponses,
|
|
862
934
|
quotedTotalCents: quote?.totalCents,
|
|
863
935
|
bookingHoldId: selectedServiceRequiresSlot ? holdId : undefined,
|
|
864
|
-
bookingSessionToken: selectedServiceRequiresSlot
|
|
936
|
+
bookingSessionToken: selectedServiceRequiresSlot
|
|
937
|
+
? sessionToken
|
|
938
|
+
: undefined,
|
|
865
939
|
});
|
|
866
940
|
if (created.payment) {
|
|
941
|
+
analytics.capture('sable_booking_payment_started', {
|
|
942
|
+
appointment_id: created.appointmentId,
|
|
943
|
+
service_id: selectedServiceId,
|
|
944
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
945
|
+
selectedService.name,
|
|
946
|
+
amount_cents: created.payment.amountCents,
|
|
947
|
+
currency: created.payment.currency,
|
|
948
|
+
});
|
|
867
949
|
setPayment(created.payment);
|
|
868
950
|
setPaymentAppointmentId(created.appointmentId);
|
|
869
951
|
setError(null);
|
|
870
952
|
return;
|
|
871
953
|
}
|
|
872
954
|
setSuccess(t('successBookingConfirmed'));
|
|
955
|
+
analytics.capture('sable_booking_created', {
|
|
956
|
+
appointment_id: created.appointmentId,
|
|
957
|
+
service_id: selectedServiceId,
|
|
958
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
959
|
+
selectedService.name,
|
|
960
|
+
booking_mode: selectedService.publicBookingMode ?? 'scheduled',
|
|
961
|
+
requires_payment: selectedService.requiresPayment === true,
|
|
962
|
+
quoted_total_cents: quote?.totalCents ?? null,
|
|
963
|
+
});
|
|
873
964
|
setSelectedSlot(null);
|
|
874
965
|
setPendingSlotKey(null);
|
|
875
966
|
setHoldId(null);
|
|
@@ -886,7 +977,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
886
977
|
setMobileStep(4);
|
|
887
978
|
}
|
|
888
979
|
catch (nextError) {
|
|
889
|
-
setError(nextError instanceof Error
|
|
980
|
+
setError(nextError instanceof Error
|
|
981
|
+
? nextError.message
|
|
982
|
+
: t('errorConfirmFailed'));
|
|
890
983
|
}
|
|
891
984
|
finally {
|
|
892
985
|
setIsSubmitting(false);
|
|
@@ -894,9 +987,10 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
894
987
|
}
|
|
895
988
|
if (isSetupLoading) {
|
|
896
989
|
// Boneyard renders the bones JSON captured for "booking-widget"
|
|
897
|
-
// (see ./bones/booking-widget.bones.json).
|
|
898
|
-
//
|
|
899
|
-
|
|
990
|
+
// (see ./bones/booking-widget.bones.json). The CSS placeholder is
|
|
991
|
+
// still rendered underneath so SSR and no-bones fallbacks reserve
|
|
992
|
+
// the full widget height instead of letting the footer jump upward.
|
|
993
|
+
return (_jsx(Skeleton, { name: "booking-widget", loading: true, fallback: _jsx(BookingWidgetPlaceholder, {}), children: _jsx(BookingWidgetPlaceholder, {}) }));
|
|
900
994
|
}
|
|
901
995
|
if (!setup || setup.services.length === 0) {
|
|
902
996
|
return (_jsx("section", { className: "bw", children: _jsx("div", { className: "bw-empty", children: t('errorNoServices') }) }));
|
|
@@ -907,22 +1001,30 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
907
1001
|
}, children: t('cancelledCta') })] }) }));
|
|
908
1002
|
}
|
|
909
1003
|
if (success || forcedState === 'success') {
|
|
910
|
-
return (_jsx("section", { className: "bw", children: _jsxs("div", { className: "bw-done", children: [_jsx("div", { className: "bw-done-icon", children: _jsx(CheckIcon, {}) }), _jsx("h3", { className: "bw-done-title", children: isReschedule
|
|
1004
|
+
return (_jsx("section", { className: "bw", children: _jsxs("div", { className: "bw-done", children: [_jsx("div", { className: "bw-done-icon", children: _jsx(CheckIcon, {}) }), _jsx("h3", { className: "bw-done-title", children: isReschedule
|
|
1005
|
+
? t('successTitleReschedule')
|
|
1006
|
+
: t('successTitleCreate') }), _jsx("p", { className: "bw-done-text", children: isReschedule ? t('successBodyReschedule') : t('successBodyCreate') }), isReschedule ? null : (_jsx("button", { type: "button", className: "bw-btn-primary", onClick: () => {
|
|
911
1007
|
window.location.assign(successRedirectHref);
|
|
912
1008
|
}, children: t('successCtaBackHome') }))] }) }));
|
|
913
1009
|
}
|
|
914
|
-
return (_jsx(Skeleton, { name: "booking-widget", loading: false, children: _jsxs("section", { className: "bw", "data-view-state": viewState, ref: widgetRootRef, children: [mobileHeader ? _jsx("div", { className: "bw-mobile-header", children: mobileHeader }) : null, _jsxs("div", { className: "bw-header", children: [_jsxs("div", { className: "bw-header-row", children: [_jsx("h2", { className: "bw-title bw-title--desktop", children: title ||
|
|
1010
|
+
return (_jsx(Skeleton, { name: "booking-widget", loading: false, children: _jsxs("section", { className: "bw", "data-view-state": viewState, ref: widgetRootRef, children: [mobileHeader ? (_jsx("div", { className: "bw-mobile-header", children: mobileHeader })) : null, _jsxs("div", { className: "bw-header", children: [_jsxs("div", { className: "bw-header-row", children: [_jsx("h2", { className: "bw-title bw-title--desktop", children: title ||
|
|
915
1011
|
(isReschedule
|
|
916
1012
|
? rescheduleContext?.customerName
|
|
917
|
-
? t('rescheduleTitleNamed', {
|
|
1013
|
+
? t('rescheduleTitleNamed', {
|
|
1014
|
+
name: rescheduleContext.customerName,
|
|
1015
|
+
})
|
|
918
1016
|
: t('rescheduleTitle')
|
|
919
1017
|
: t('pageTitle')) }), _jsx("h2", { className: "bw-title bw-title--mobile", children: isReschedule
|
|
920
1018
|
? rescheduleContext?.customerName
|
|
921
|
-
? t('rescheduleTitleNamed', {
|
|
1019
|
+
? t('rescheduleTitleNamed', {
|
|
1020
|
+
name: rescheduleContext.customerName,
|
|
1021
|
+
})
|
|
922
1022
|
: t('rescheduleTitle')
|
|
923
1023
|
: mobileStepTitle(mobileStep, t) }), mobileStep > 1 ? (_jsx("div", { className: "bw-progress", "aria-hidden": "true", children: (selectedServiceRequiresSlot
|
|
924
1024
|
? MOBILE_PROGRESS_STEPS_SCHEDULED
|
|
925
|
-
: MOBILE_PROGRESS_STEPS_ASYNC).map((step) => (_jsx("span", { className: `bw-dot${mobileStep >= step ? ' is-filled' : ''}` }, step))) })) : null] }), description ? _jsx("p", { className: "bw-description", children: description }) : null] }), _jsxs("div", { className: "bw-content", children: [_jsx("div", { className: "bw-body", "data-mobile-step": mobileStep, children: _jsxs("div", { className: "bw-body-inner", children: [_jsxs("div", { className: "bw-col bw-col--left", children: [_jsxs("div", { className: "bw-step-1", children: [_jsx("div", { className: "bw-service-picker", children: selectedService && !isEditingService ? (_jsxs("div", { className: "bw-svc-selected", children: [_jsxs("div", { className: "bw-svc-selected-header", children: [_jsx("label", { className: "bw-label", children: t('selectedService') }), _jsx("button", { type: "button", className: "bw-svc-change", onClick: () => setIsEditingService(true), children: t('btnChange') })] }), _jsxs("div", { className: "bw-svc-row bw-svc-row--readonly", children: [selectedService.image ? (_jsxs("span", { className: "bw-svc-image is-checked", children: [_jsx("img", { src: selectedService.image.url, alt: selectedService.image.alt ??
|
|
1025
|
+
: MOBILE_PROGRESS_STEPS_ASYNC).map((step) => (_jsx("span", { className: `bw-dot${mobileStep >= step ? ' is-filled' : ''}` }, step))) })) : null] }), description ? _jsx("p", { className: "bw-description", children: description }) : null] }), _jsxs("div", { className: "bw-content", children: [_jsx("div", { className: "bw-body", "data-mobile-step": mobileStep, children: _jsxs("div", { className: "bw-body-inner", children: [_jsxs("div", { className: "bw-col bw-col--left", children: [_jsxs("div", { className: "bw-step-1", children: [_jsx("div", { className: "bw-service-picker", children: selectedService && !isEditingService ? (_jsxs("div", { className: "bw-svc-selected", children: [_jsxs("div", { className: "bw-svc-selected-header", children: [_jsx("label", { className: "bw-label", children: t('selectedService') }), _jsx("button", { type: "button", className: "bw-svc-change", onClick: () => setIsEditingService(true), children: t('btnChange') })] }), _jsxs("div", { className: "bw-svc-row bw-svc-row--readonly", children: [selectedService.image ? (_jsxs("span", { className: "bw-svc-image is-checked", children: [_jsx("img", { src: selectedService.image.url, alt: selectedService.image.alt ??
|
|
1026
|
+
pickLocaleField(selectedService, 'name', locale) ??
|
|
1027
|
+
selectedService.name, loading: "lazy" }), _jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })] })) : (_jsx("span", { className: "bw-check is-checked", children: _jsx(CheckIcon, {}) })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ?? selectedService.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(selectedService.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(selectedService, undefined, intlLocale) })] })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx("label", { className: "bw-label", children: t('selectService') }), _jsx("div", { className: "bw-svc-scroll is-open", children: _jsx("div", { className: "bw-svc-scroll-wrap", children: _jsx("div", { className: "bw-svc-scroll-inner", children: serviceGroups.map((group) => (_jsxs("div", { className: "bw-svc-group", children: [group.categoryName ? (_jsx("div", { className: "bw-svc-category", children: group.categoryName })) : null, group.services.map((service) => {
|
|
926
1028
|
const isActive = selectedServiceId === service._id;
|
|
927
1029
|
return (_jsxs("button", { type: "button", className: "bw-svc-row", onMouseEnter: () => {
|
|
928
1030
|
void prefetchAvailability({
|
|
@@ -942,8 +1044,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
942
1044
|
});
|
|
943
1045
|
}, onClick: () => {
|
|
944
1046
|
handleServiceSelect(service);
|
|
945
|
-
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
946
|
-
|
|
1047
|
+
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1048
|
+
pickLocaleField(service, 'name', locale) ??
|
|
1049
|
+
service.name, loading: "lazy" }), isActive ? (_jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })) : null] })) : (_jsx("span", { className: `bw-check${isActive ? ' is-checked' : ''}`, children: isActive ? _jsx(CheckIcon, {}) : null })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(service, 'name', locale) ?? service.name }), (pickLocaleField(service, 'description', locale) ?? service.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(service, 'description', locale) ?? service.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(service.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(service, undefined, intlLocale) })] })] })] }, service._id));
|
|
1050
|
+
})] }, group.key))) }) }) })] })) }), selectedService &&
|
|
1051
|
+
selectedServiceRequiresSlot &&
|
|
1052
|
+
!isEditingService ? (_jsxs("div", { className: "bw-provider-section", children: [_jsx("div", { className: "bw-section-divider" }), _jsx("label", { className: "bw-label", children: t('selectProvider') }), providerRow] })) : null] }), _jsxs("div", { className: "bw-step-2", children: [_jsx("p", { className: "bw-step-2-heading", children: t('chooseYourService') }), _jsx("div", { className: "bw-step-2-divider" }), serviceGroups.map((group) => (_jsxs("div", { className: "bw-svc-group", children: [group.categoryName ? (_jsx("div", { className: "bw-svc-category", children: group.categoryName })) : null, group.services.map((service) => {
|
|
947
1053
|
const isActive = selectedServiceId === service._id;
|
|
948
1054
|
return (_jsxs("div", { children: [_jsxs("button", { type: "button", className: "bw-svc-row bw-svc-row--full", onMouseEnter: () => {
|
|
949
1055
|
void prefetchAvailability({
|
|
@@ -963,12 +1069,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
963
1069
|
});
|
|
964
1070
|
}, onClick: () => {
|
|
965
1071
|
handleServiceSelect(service);
|
|
966
|
-
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image bw-svc-image--lg${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1072
|
+
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image bw-svc-image--lg${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1073
|
+
pickLocaleField(service, 'name', locale) ??
|
|
1074
|
+
service.name, loading: "lazy" }), isActive ? (_jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })) : null] })) : (_jsx("span", { className: `bw-check bw-check--lg${isActive ? ' is-checked' : ''}`, children: isActive ? _jsx(CheckIcon, {}) : null })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(service, 'name', locale) ??
|
|
1075
|
+
service.name }), (pickLocaleField(service, 'description', locale) ?? service.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(service, 'description', locale) ?? service.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(service.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(service, undefined, intlLocale) })] })] })] }), _jsx("div", { className: "bw-row-divider" })] }, service._id));
|
|
967
1076
|
})] }, group.key)))] })] }), _jsxs("div", { className: "bw-col bw-col--center", children: [_jsxs("div", { className: "bw-cal-card", ref: calCardRef, children: [_jsxs("div", { className: "bw-cal-header", children: [_jsxs("div", { className: "bw-month-dropdown", children: [_jsxs("button", { type: "button", className: "bw-month-btn", onClick: () => setMonthOpen((current) => !current), children: [_jsx("span", { children: formatMonthLabel(calendarMonth, intlLocale) }), _jsx(ChevronDownIcon, {})] }), monthOpen ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bw-month-overlay", "aria-label": t('closeMonthPicker'), onClick: () => setMonthOpen(false) }), _jsx("div", { className: "bw-month-list", children: monthOptions.map((option) => (_jsx("button", { type: "button", className: `bw-month-option${option.value === optionCurrentValue(calendarMonth) ? ' is-active' : ''}`, onClick: () => {
|
|
968
1077
|
setCalendarMonth(option.date);
|
|
969
1078
|
setMonthOpen(false);
|
|
970
1079
|
}, children: option.label }, option.value))) })] })) : null] }), _jsxs("div", { className: "bw-cal-navs", children: [_jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
971
|
-
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1080
|
+
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1081
|
+
selectedServiceId) {
|
|
972
1082
|
void prefetchAvailability({
|
|
973
1083
|
client,
|
|
974
1084
|
siteSlug,
|
|
@@ -978,7 +1088,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
978
1088
|
});
|
|
979
1089
|
}
|
|
980
1090
|
}, onClick: () => setCalendarMonth((current) => addMonths(current, -1)), disabled: sameMonth(calendarMonth, startOfMonth(new Date())), "aria-label": t('prevMonth'), children: _jsx(ChevronLeftIcon, {}) }), _jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
981
|
-
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1091
|
+
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1092
|
+
selectedServiceId) {
|
|
982
1093
|
void prefetchAvailability({
|
|
983
1094
|
client,
|
|
984
1095
|
siteSlug,
|
|
@@ -1008,22 +1119,53 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1008
1119
|
isSelected ? 'is-selected' : '',
|
|
1009
1120
|
isAvailable && !isPast ? 'is-available' : '',
|
|
1010
1121
|
isPast ? 'is-disabled' : '',
|
|
1011
|
-
!isAvailable && isCurrentMonth && !isPast
|
|
1012
|
-
|
|
1122
|
+
!isAvailable && isCurrentMonth && !isPast
|
|
1123
|
+
? 'is-blocked'
|
|
1124
|
+
: '',
|
|
1125
|
+
dateKey === formatDateKey(startOfDay(new Date()))
|
|
1126
|
+
? 'is-today'
|
|
1127
|
+
: '',
|
|
1013
1128
|
]
|
|
1014
1129
|
.filter(Boolean)
|
|
1015
1130
|
.join(' ');
|
|
1016
1131
|
return (_jsx("button", { type: "button", className: className, disabled: !isCurrentMonth || isPast || !isAvailable, onClick: () => {
|
|
1017
1132
|
setSelectedDate(dateKey);
|
|
1018
1133
|
setSelectedSlot(null);
|
|
1019
|
-
setMobileStep((current) =>
|
|
1134
|
+
setMobileStep((current) => current < 2 ? 2 : current);
|
|
1020
1135
|
}, children: day.getDate() }, dateKey));
|
|
1021
|
-
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
|
|
1136
|
+
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
|
|
1137
|
+
selectedStaff?.timezone ??
|
|
1138
|
+
setup?.workspaceTimezone ??
|
|
1139
|
+
setup?.staff?.[0]?.timezone ??
|
|
1140
|
+
'UTC' })] })] }), selectedService && !isEditingService ? (_jsxs("div", { className: "bw-mobile-card bw-provider-section--mobile", children: [_jsx("label", { className: "bw-label", children: t('selectProvider') }), providerRow] })) : null, selectedService && selectedDate ? (_jsxs("div", { className: "bw-mobile-card bw-slots-mobile", children: [_jsx("label", { className: "bw-label", children: t('selectTimePrompt') }), slotsArea] })) : null] }), _jsx("div", { className: "bw-col bw-col--review", "aria-hidden": mobileStep !== 4, children: selectedService ? (_jsxs("div", { className: "bw-summary bw-summary--review", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
|
|
1141
|
+
? t('summaryTitleReschedule')
|
|
1142
|
+
: t('summaryTitleCreate') }), holdSecondsRemaining !== null ? (_jsx("div", { className: "bw-summary-rows", children: _jsxs("div", { className: "bw-summary-row bw-summary-row--hold", "aria-live": "polite", children: [_jsx("span", { children: t('summaryReservationHold') }), _jsx("span", { className: "bw-summary-val bw-summary-val--hold", children: formatHoldCountdown(holdSecondsRemaining) })] }) })) : null, _jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewBookingDetails') }), _jsxs("div", { className: "bw-summary-rows", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerTime') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1143
|
+
...summaryValStyle,
|
|
1144
|
+
textDecoration: 'line-through',
|
|
1145
|
+
opacity: 0.55,
|
|
1146
|
+
}, children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })) : null, serviceChanged && formerService ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerService') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1147
|
+
...summaryValStyle,
|
|
1148
|
+
textDecoration: 'line-through',
|
|
1149
|
+
opacity: 0.55,
|
|
1150
|
+
}, children: formerService.name })] })) : null, staffChanged ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerStaff') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1151
|
+
...summaryValStyle,
|
|
1152
|
+
textDecoration: 'line-through',
|
|
1153
|
+
opacity: 0.55,
|
|
1154
|
+
}, children: formerStaffName ?? t('providerAny') })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryService') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: pickLocaleField(selectedService, 'name', locale) ??
|
|
1155
|
+
selectedService.name })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1156
|
+
selectedStaff?.name ??
|
|
1157
|
+
t('providerAny') })] }), selectedDate ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDate') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatReadableDate(selectedDate, intlLocale) })] })) : null, selectedSlot ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatTimeLabel(selectedSlot.time, intlLocale) })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDuration') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatDuration(selectedService.durationMinutes) })] })] })] }), _jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewYourDetails') }), _jsxs("div", { className: "bw-summary-rows", children: [_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactFullName') }), _jsx("span", { className: "bw-summary-val", children: customerName.trim() || t('reviewNotProvided') })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactEmail') }), _jsx("span", { className: "bw-summary-val", children: customerEmail.trim() || t('reviewNotProvided') })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactPhone') }), _jsx("span", { className: "bw-summary-val", children: customerPhone.trim() || t('reviewNotProvided') })] })] })] }), (() => {
|
|
1022
1158
|
const intakeRows = buildIntakeReviewRows(selectedService.intakeForm, intakeResponses, selectedServicePath, locale, t);
|
|
1023
1159
|
if (intakeRows.length === 0)
|
|
1024
1160
|
return null;
|
|
1025
1161
|
return (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewAdditionalInfo') }), _jsx("div", { className: "bw-summary-rows", children: intakeRows.map((row) => (_jsxs("div", { className: `bw-summary-row${row.multiline ? ' bw-summary-row--stack' : ''}`, children: [_jsx("span", { children: row.label }), _jsx("span", { className: "bw-summary-val", children: row.value })] }, row.key))) })] }));
|
|
1026
|
-
})(), customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1162
|
+
})(), customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1163
|
+
? t('notesLabelReschedule')
|
|
1164
|
+
: t('reviewNotesSection') }), _jsx("p", { className: "bw-summary-notes", children: customerNotes.trim() })] })) : null, _jsx("div", { className: "bw-summary-rows bw-summary-rows--total", children: _jsxs("div", { className: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isQuoteLoading
|
|
1165
|
+
? t('summaryCalculating')
|
|
1166
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, quote, intlLocale) })] }) })] })) : null }), _jsx("div", { className: "bw-col bw-col--right", children: _jsx("div", { className: "bw-right-scroll", children: _jsx("div", { className: "bw-right-inner", children: _jsxs("div", { className: "bw-right-stage", children: [_jsxs("div", { className: `bw-pane bw-pane--slots${viewState === 'slots' ? ' is-active' : ' is-hidden'}`, "aria-hidden": viewState !== 'slots', children: [_jsxs("div", { className: "bw-slots-heading bw-slots-heading--sticky", children: [_jsx("span", { children: t('slotsHeading') }), selectedDateSlots.length > 0 ? (_jsx("span", { className: "bw-slots-count", children: selectedDateSlots.length })) : null] }), _jsx("div", { className: "bw-slots-desktop", children: slotsArea })] }), _jsxs("div", { className: `bw-pane bw-pane--details${viewState === 'details' ? ' is-active' : ' is-hidden'}`, "aria-hidden": viewState !== 'details', children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsxs("div", { className: "bw-form", children: [serviceWarning ? (_jsx(ServiceWarningCallout, { warning: serviceWarning })) : null, renderIntakeAndContact('bw-intake', ''), renderNotesField('bw-notes', t('notesLabel'), t('notesPlaceholder')), selectedService ? (_jsxs("div", { className: "bw-summary", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
|
|
1167
|
+
? t('summaryTitleReschedule')
|
|
1168
|
+
: t('summaryTitleCreate') }), _jsxs("div", { className: "bw-summary-rows", children: [holdSecondsRemaining !== null ? (_jsxs("div", { className: "bw-summary-row bw-summary-row--hold", "aria-live": "polite", children: [_jsx("span", { children: t('summaryReservationHold') }), _jsx("span", { className: "bw-summary-val bw-summary-val--hold", children: formatHoldCountdown(holdSecondsRemaining) })] })) : null, isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerTime') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1027
1169
|
...summaryValStyle,
|
|
1028
1170
|
textDecoration: 'line-through',
|
|
1029
1171
|
opacity: 0.55,
|
|
@@ -1035,7 +1177,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1035
1177
|
...summaryValStyle,
|
|
1036
1178
|
textDecoration: 'line-through',
|
|
1037
1179
|
opacity: 0.55,
|
|
1038
|
-
}, children: formerStaffName ?? t('providerAny') })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryService') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1180
|
+
}, children: formerStaffName ?? t('providerAny') })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryService') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1181
|
+
selectedStaff?.name ??
|
|
1182
|
+
t('providerAny') })] }), selectedDate ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDate') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatReadableDate(selectedDate, intlLocale) })] })) : null, selectedSlot ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatTimeLabel(selectedSlot.time, intlLocale) })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDuration') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatDuration(selectedService.durationMinutes) })] }), _jsxs("div", { className: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isQuoteLoading
|
|
1183
|
+
? t('summaryCalculating')
|
|
1184
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, quote, intlLocale) })] })] })] })) : null, error ? (_jsx("div", { className: "bw-error", children: error })) : null, renderSubmitHelp(), payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, showInlineButton: false, actionTarget: mobilePaymentActionTarget, onSuccess: () => {
|
|
1039
1185
|
setSuccess(t('successPaymentReceived'));
|
|
1040
1186
|
setPayment(null);
|
|
1041
1187
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -1046,7 +1192,23 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1046
1192
|
? t('btnConfirmReschedule')
|
|
1047
1193
|
: selectedServiceRequiresPayment
|
|
1048
1194
|
? t('btnContinueToPayment')
|
|
1049
|
-
: t('btnConfirmBooking') }), !isSubmitting ? _jsx(ArrowRightIcon, {}) : null] }))] })] })] }) }) }) })] }) }), _jsxs("div", { className: "bw-details-view", children: [_jsx("div", { className: "bw-details-summary", children: selectedService ? (_jsxs(_Fragment, { children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsx("h3", { className: "bw-details-service", children: pickLocaleField(selectedService, 'name', locale) ??
|
|
1195
|
+
: t('btnConfirmBooking') }), !isSubmitting ? _jsx(ArrowRightIcon, {}) : null] }))] })] })] }) }) }) })] }) }), _jsxs("div", { className: "bw-details-view", children: [_jsx("div", { className: "bw-details-summary", children: selectedService ? (_jsxs(_Fragment, { children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsx("h3", { className: "bw-details-service", children: pickLocaleField(selectedService, 'name', locale) ??
|
|
1196
|
+
selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ??
|
|
1197
|
+
selectedService.description) ? (_jsx("p", { className: "bw-details-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("div", { className: "bw-details-meta", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--strike", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryFormerTime') }), _jsx("span", { className: "bw-details-meta-value", children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })] })) : null, selectedDate && selectedSlot ? (_jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isReschedule
|
|
1198
|
+
? t('summaryNewTime')
|
|
1199
|
+
: t('summaryWhen') }), _jsxs("span", { className: "bw-details-meta-value", children: [formatReadableDate(selectedDate, intlLocale), _jsx("br", {}), formatTimeLabel(selectedSlot.time, intlLocale), " \u2013", ' ', formatTimeLabel(selectedSlot.endTime, intlLocale)] })] })] })) : null, _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(ClockIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: formatDuration(selectedService.durationMinutes) })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(UserIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: selectedHeldStaff?.name ??
|
|
1200
|
+
selectedStaff?.name ??
|
|
1201
|
+
t('providerAny') })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(GlobeIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: selectedHeldStaff?.timezone ??
|
|
1202
|
+
selectedStaff?.timezone ??
|
|
1203
|
+
setup?.workspaceTimezone ??
|
|
1204
|
+
setup?.staff?.[0]?.timezone ??
|
|
1205
|
+
'UTC' })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", "aria-hidden": "true", children: "$" }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isQuoteLoading
|
|
1206
|
+
? t('summaryCalculating')
|
|
1207
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-details-meta-value", children: formatServicePrice(selectedService, quote, intlLocale) })] })] }), holdSecondsRemaining !== null ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--hold", "aria-live": "polite", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(ClockIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryReservationHold') }), _jsx("span", { className: "bw-details-meta-value bw-details-hold", children: formatHoldCountdown(holdSecondsRemaining) })] })] })) : null] })] })) : null }), _jsx("div", { className: "bw-details-form", children: _jsxs("div", { className: "bw-form bw-form--details", children: [serviceWarning ? (_jsx(ServiceWarningCallout, { warning: serviceWarning })) : null, renderIntakeAndContact('bw-intake-d', '-d'), renderNotesField('bw-notes-d', isReschedule ? t('notesLabelReschedule') : t('notesLabel'), isReschedule
|
|
1208
|
+
? t('notesPlaceholderReschedule')
|
|
1209
|
+
: t('notesPlaceholder')), error ? _jsx("div", { className: "bw-error", children: error }) : null, renderSubmitHelp(), !payment &&
|
|
1210
|
+
(forcedState === 'payment-full' ||
|
|
1211
|
+
forcedState === 'payment-deposit') ? (_jsx(PaymentPanel, { service: selectedService, mode: forcedState === 'payment-full' ? 'full' : 'deposit' })) : null, payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, onSuccess: () => {
|
|
1050
1212
|
setSuccess(t('successPaymentReceived'));
|
|
1051
1213
|
setPayment(null);
|
|
1052
1214
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -1054,7 +1216,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1054
1216
|
? t('btnRescheduling')
|
|
1055
1217
|
: t('btnBooking')
|
|
1056
1218
|
: forcedState === 'payment-full'
|
|
1057
|
-
? t('btnPayAndConfirm', {
|
|
1219
|
+
? t('btnPayAndConfirm', {
|
|
1220
|
+
price: formatPrice(selectedService, intlLocale),
|
|
1221
|
+
})
|
|
1058
1222
|
: forcedState === 'payment-deposit'
|
|
1059
1223
|
? t('btnPayDepositAndConfirm')
|
|
1060
1224
|
: isReschedule
|
|
@@ -1154,7 +1318,8 @@ function areRequiredIntakeFieldsComplete(form, responses, mode) {
|
|
|
1154
1318
|
}
|
|
1155
1319
|
function getSubmitBlockers({ selectedService, selectedServiceRequiresSlot, selectedDate, selectedSlot, holdId, isIntakeComplete, intakeResponses, selectedServicePath, quote, customerName, customerEmail, t, locale = DEFAULT_LOCALE, }) {
|
|
1156
1320
|
const blockers = [];
|
|
1157
|
-
if (selectedServiceRequiresSlot &&
|
|
1321
|
+
if (selectedServiceRequiresSlot &&
|
|
1322
|
+
(!selectedDate || !selectedSlot || !holdId)) {
|
|
1158
1323
|
blockers.push(t('blockerDateTime'));
|
|
1159
1324
|
}
|
|
1160
1325
|
if (!customerName.trim())
|
|
@@ -1282,7 +1447,9 @@ function buildIntakeReviewRows(form, responses, servicePath, locale, t) {
|
|
|
1282
1447
|
continue;
|
|
1283
1448
|
for (const field of section.fields) {
|
|
1284
1449
|
if (field.type === 'repeatable-group') {
|
|
1285
|
-
const items = Array.isArray(responses[field.id])
|
|
1450
|
+
const items = Array.isArray(responses[field.id])
|
|
1451
|
+
? responses[field.id]
|
|
1452
|
+
: [];
|
|
1286
1453
|
items.forEach((item, index) => {
|
|
1287
1454
|
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1288
1455
|
? item
|
|
@@ -1487,7 +1654,9 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
1487
1654
|
if (field.type === 'repeatable-group') {
|
|
1488
1655
|
const items = Array.isArray(value) && value.length > 0 ? value : [{}];
|
|
1489
1656
|
return (_jsxs("div", { className: "bw-field bw-field--wide bw-repeatable", children: [_jsx("label", { children: label }), items.map((item, index) => {
|
|
1490
|
-
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1657
|
+
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1658
|
+
? item
|
|
1659
|
+
: {};
|
|
1491
1660
|
return (_jsxs("div", { className: "bw-repeatable-item", children: [_jsxs("div", { className: "bw-repeatable-head", children: [_jsx("span", { children: t('repeatItemLabel', { index: index + 1 }) }), items.length > 1 ? (_jsx("button", { type: "button", className: "bw-link-btn", onClick: () => onChange(items.filter((_, itemIndex) => itemIndex !== index)), children: t('repeatRemove') })) : null] }), _jsx("div", { className: "bw-form-fields", children: (field.fields ?? []).map((child) => (_jsx(IntakeField, { field: child, value: record[child.id], idPrefix: `${id}-${index}`, onChange: (nextValue) => {
|
|
1492
1661
|
const nextItems = [...items];
|
|
1493
1662
|
nextItems[index] = { ...record, [child.id]: nextValue };
|
|
@@ -1511,7 +1680,11 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
1511
1680
|
if (field.type === 'textarea') {
|
|
1512
1681
|
return (_jsxs("div", { className: "bw-field bw-field--wide", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("textarea", { id: id, rows: 3, required: field.required, value: stringValue, placeholder: field.placeholder, onChange: (event) => onChange(event.target.value) }), field.helpText ? _jsx("p", { className: "bw-help", children: field.helpText }) : null] }));
|
|
1513
1682
|
}
|
|
1514
|
-
return (_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("input", { id: id, type: field.type === 'email'
|
|
1683
|
+
return (_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("input", { id: id, type: field.type === 'email'
|
|
1684
|
+
? 'email'
|
|
1685
|
+
: field.type === 'number'
|
|
1686
|
+
? 'number'
|
|
1687
|
+
: 'text', min: field.min, max: field.max, required: field.required, value: stringValue, placeholder: field.placeholder, onChange: (event) => {
|
|
1515
1688
|
onChange(field.type === 'number'
|
|
1516
1689
|
? event.target.value === ''
|
|
1517
1690
|
? ''
|
|
@@ -1594,10 +1767,9 @@ function readString(value) {
|
|
|
1594
1767
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
1595
1768
|
}
|
|
1596
1769
|
// BookingWidgetSkeleton was a hand-built first-load ghost that
|
|
1597
|
-
// constantly drifted out of sync with the real layout.
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
// pre-captured bones JSON regenerated via `npx boneyard-js build`.
|
|
1770
|
+
// constantly drifted out of sync with the real layout. Boneyard now
|
|
1771
|
+
// owns the detailed bones; BookingWidgetPlaceholder is the stable
|
|
1772
|
+
// SSR/no-bones space holder and Suspense fallback for host sites.
|
|
1601
1773
|
function AvailabilitySkeleton() {
|
|
1602
1774
|
// Reuses .bw-time-slots so the skeleton inherits the same layout
|
|
1603
1775
|
// overrides as the real slots — vertical 1-col on desktop (via the
|
|
@@ -1743,7 +1915,8 @@ function buildCalendarDays(month) {
|
|
|
1743
1915
|
});
|
|
1744
1916
|
}
|
|
1745
1917
|
function sameMonth(left, right) {
|
|
1746
|
-
return left.getFullYear() === right.getFullYear() &&
|
|
1918
|
+
return (left.getFullYear() === right.getFullYear() &&
|
|
1919
|
+
left.getMonth() === right.getMonth());
|
|
1747
1920
|
}
|
|
1748
1921
|
function getMonthOptions(currentMonth, total, locale = 'en-US') {
|
|
1749
1922
|
const start = startOfMonth(new Date());
|
|
@@ -1762,7 +1935,8 @@ function optionCurrentValue(date) {
|
|
|
1762
1935
|
}
|
|
1763
1936
|
function isDateInMonth(dateKey, month) {
|
|
1764
1937
|
const date = new Date(`${dateKey}T00:00:00`);
|
|
1765
|
-
return date.getMonth() === month.getMonth() &&
|
|
1938
|
+
return (date.getMonth() === month.getMonth() &&
|
|
1939
|
+
date.getFullYear() === month.getFullYear());
|
|
1766
1940
|
}
|
|
1767
1941
|
function findFirstAvailableDate(availabilityByDate, month) {
|
|
1768
1942
|
const dates = [...availabilityByDate.entries()]
|