@arkipay/booking-widget 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,1721 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var client = require('botid/client');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var polyfill = require('@js-temporal/polyfill');
8
+
9
+ // src/provider.tsx
10
+ var BookingContext = react.createContext(null);
11
+ function useBookingContext() {
12
+ const ctx = react.useContext(BookingContext);
13
+ if (!ctx) {
14
+ throw new Error(`useBookingContext must be used within BookingProvider`);
15
+ }
16
+ return ctx;
17
+ }
18
+
19
+ // src/constants.ts
20
+ var ANY_PROVIDER_ID = `any`;
21
+
22
+ // src/utils/fields.ts
23
+ var emptyCustomerValues = () => ({
24
+ firstName: ``,
25
+ lastName: ``,
26
+ email: ``,
27
+ phone: ``,
28
+ address: ``,
29
+ city: ``,
30
+ state: ``,
31
+ zip: ``,
32
+ notes: ``,
33
+ customField1: ``,
34
+ customField2: ``,
35
+ customField3: ``,
36
+ customField4: ``,
37
+ customField5: ``,
38
+ timezone: typeof Intl !== `undefined` ? Intl.DateTimeFormat().resolvedOptions().timeZone : `UTC`
39
+ });
40
+ var FIELD_KEY_TO_VALUE = {
41
+ first_name: `firstName`,
42
+ last_name: `lastName`,
43
+ email: `email`,
44
+ phone: `phone`,
45
+ address: `address`,
46
+ city: `city`,
47
+ state: `state`,
48
+ zip: `zip`,
49
+ notes: `notes`,
50
+ custom_1: `customField1`,
51
+ custom_2: `customField2`,
52
+ custom_3: `customField3`,
53
+ custom_4: `customField4`,
54
+ custom_5: `customField5`
55
+ };
56
+ var FIELD_KEY_TO_LABEL = {
57
+ first_name: `fields.first_name`,
58
+ last_name: `fields.last_name`,
59
+ email: `fields.email`,
60
+ phone: `fields.phone`,
61
+ address: `fields.address`,
62
+ city: `fields.city`,
63
+ state: `fields.state`,
64
+ zip: `fields.zip`,
65
+ notes: `fields.notes`,
66
+ custom_1: `fields.custom_1`,
67
+ custom_2: `fields.custom_2`,
68
+ custom_3: `fields.custom_3`,
69
+ custom_4: `fields.custom_4`,
70
+ custom_5: `fields.custom_5`
71
+ };
72
+ function visibleFormFields(fields) {
73
+ return [...fields].filter((f) => f.display).sort((a, b) => a.sortOrder - b.sortOrder);
74
+ }
75
+ function customerValueKey(fieldKey) {
76
+ return FIELD_KEY_TO_VALUE[fieldKey];
77
+ }
78
+ function fieldLabelKey(fieldKey) {
79
+ return FIELD_KEY_TO_LABEL[fieldKey];
80
+ }
81
+ function validateCustomerFields(fields, values) {
82
+ const errors = {};
83
+ for (const field of fields) {
84
+ if (!field.display || !field.required) {
85
+ continue;
86
+ }
87
+ const key = customerValueKey(field.fieldKey);
88
+ if (!key) {
89
+ continue;
90
+ }
91
+ const value = values[key];
92
+ if (typeof value !== `string` || value.trim() === ``) {
93
+ errors[field.fieldKey] = `required`;
94
+ }
95
+ }
96
+ if (fields.some((f) => f.fieldKey === `email` && f.display) && values.email.trim()) {
97
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email.trim())) {
98
+ errors.email = `invalid`;
99
+ }
100
+ }
101
+ return errors;
102
+ }
103
+ function customerValuesToAppointmentBody(values) {
104
+ return {
105
+ firstName: values.firstName.trim(),
106
+ lastName: values.lastName.trim(),
107
+ email: values.email.trim(),
108
+ phone: values.phone.trim() || null,
109
+ address: values.address.trim() || null,
110
+ city: values.city.trim() || null,
111
+ state: values.state.trim() || null,
112
+ zip: values.zip.trim() || null,
113
+ notes: values.notes.trim() || null,
114
+ customField1: values.customField1.trim() || null,
115
+ customField2: values.customField2.trim() || null,
116
+ customField3: values.customField3.trim() || null,
117
+ customField4: values.customField4.trim() || null,
118
+ customField5: values.customField5.trim() || null,
119
+ customerTimezone: values.timezone || null
120
+ };
121
+ }
122
+ function needsLegalStep(legal) {
123
+ return legal.displayCookieNotice || legal.displayTerms || legal.displayPrivacy;
124
+ }
125
+
126
+ // src/hooks/use-booking.ts
127
+ var DEFAULT_TIME_DISPLAY_MODE = `business`;
128
+ function withStep(timeDisplayMode, step) {
129
+ return { timeDisplayMode, ...step };
130
+ }
131
+ function retainMode(state, step) {
132
+ return withStep(state.timeDisplayMode, step);
133
+ }
134
+ function createInitialBookingState(prefill) {
135
+ if (prefill?.serviceId && prefill.providerId) {
136
+ return withStep(DEFAULT_TIME_DISPLAY_MODE, {
137
+ step: `date`,
138
+ serviceId: prefill.serviceId,
139
+ providerId: prefill.providerId
140
+ });
141
+ }
142
+ if (prefill?.serviceId) {
143
+ return withStep(DEFAULT_TIME_DISPLAY_MODE, { step: `provider`, serviceId: prefill.serviceId });
144
+ }
145
+ return withStep(DEFAULT_TIME_DISPLAY_MODE, { step: `service` });
146
+ }
147
+ function bookingReducer(state, action, tenantConfig) {
148
+ switch (action.type) {
149
+ case `prefill`: {
150
+ return createInitialBookingState({
151
+ serviceId: action.serviceId,
152
+ providerId: action.providerId
153
+ });
154
+ }
155
+ case `set_time_display_mode`:
156
+ return { ...state, timeDisplayMode: action.mode };
157
+ case `select_service`:
158
+ return retainMode(state, { step: `provider`, serviceId: action.serviceId });
159
+ case `select_provider`:
160
+ if (state.step !== `provider`) {
161
+ return state;
162
+ }
163
+ return retainMode(state, {
164
+ step: `date`,
165
+ serviceId: state.serviceId,
166
+ providerId: action.providerId
167
+ });
168
+ case `select_date`:
169
+ if (state.step !== `date`) {
170
+ return state;
171
+ }
172
+ return retainMode(state, {
173
+ step: `time`,
174
+ serviceId: state.serviceId,
175
+ providerId: state.providerId,
176
+ date: action.date
177
+ });
178
+ case `select_slot`:
179
+ if (state.step !== `time`) {
180
+ return state;
181
+ }
182
+ return retainMode(state, {
183
+ step: `customer`,
184
+ serviceId: state.serviceId,
185
+ providerId: state.providerId,
186
+ date: state.date,
187
+ slot: action.slot
188
+ });
189
+ case `set_customer`:
190
+ if (state.step !== `customer`) {
191
+ return state;
192
+ }
193
+ if (tenantConfig && needsLegalStep(tenantConfig.legal)) {
194
+ return retainMode(state, {
195
+ step: `legal`,
196
+ serviceId: state.serviceId,
197
+ providerId: state.providerId,
198
+ date: state.date,
199
+ slot: state.slot,
200
+ customer: action.customer
201
+ });
202
+ }
203
+ return retainMode(state, {
204
+ step: `confirm`,
205
+ serviceId: state.serviceId,
206
+ providerId: state.providerId,
207
+ date: state.date,
208
+ slot: state.slot,
209
+ customer: action.customer,
210
+ consents: {}
211
+ });
212
+ case `set_consents`:
213
+ if (state.step !== `legal`) {
214
+ return state;
215
+ }
216
+ return retainMode(state, {
217
+ step: `confirm`,
218
+ serviceId: state.serviceId,
219
+ providerId: state.providerId,
220
+ date: state.date,
221
+ slot: state.slot,
222
+ customer: state.customer,
223
+ consents: action.consents
224
+ });
225
+ case `booked`:
226
+ return retainMode(state, { step: `booked`, appointment: action.appointment });
227
+ case `back`:
228
+ return backStep(state);
229
+ default:
230
+ return state;
231
+ }
232
+ }
233
+ function backStep(state) {
234
+ switch (state.step) {
235
+ case `provider`:
236
+ return retainMode(state, { step: `service` });
237
+ case `date`:
238
+ return retainMode(state, { step: `provider`, serviceId: state.serviceId });
239
+ case `time`:
240
+ return retainMode(state, {
241
+ step: `date`,
242
+ serviceId: state.serviceId,
243
+ providerId: state.providerId
244
+ });
245
+ case `customer`:
246
+ return retainMode(state, {
247
+ step: `time`,
248
+ serviceId: state.serviceId,
249
+ providerId: state.providerId,
250
+ date: state.date
251
+ });
252
+ case `legal`:
253
+ return retainMode(state, {
254
+ step: `customer`,
255
+ serviceId: state.serviceId,
256
+ providerId: state.providerId,
257
+ date: state.date,
258
+ slot: state.slot
259
+ });
260
+ case `confirm`:
261
+ return retainMode(state, {
262
+ step: `customer`,
263
+ serviceId: state.serviceId,
264
+ providerId: state.providerId,
265
+ date: state.date,
266
+ slot: state.slot
267
+ });
268
+ default:
269
+ return state;
270
+ }
271
+ }
272
+ function useBookingReducer(initial, tenantConfig) {
273
+ const [state, dispatch] = react.useReducer(
274
+ (current, action) => bookingReducer(current, action, tenantConfig),
275
+ void 0,
276
+ () => createInitialBookingState(initial)
277
+ );
278
+ return react.useMemo(() => ({ state, dispatch }), [state, dispatch]);
279
+ }
280
+ function useBooking() {
281
+ const { bookingState, dispatch, tenantConfig, onBooked } = useBookingContext();
282
+ return {
283
+ state: bookingState,
284
+ dispatch,
285
+ tenantConfig,
286
+ onBooked,
287
+ timeDisplayMode: bookingState.timeDisplayMode
288
+ };
289
+ }
290
+
291
+ // src/i18n/en.ts
292
+ var enCatalog = {
293
+ steps: {
294
+ service: `Choose a service`,
295
+ provider: `Choose a provider`,
296
+ date: `Choose a date`,
297
+ time: `Choose a time`,
298
+ customer: `Your details`,
299
+ legal: `Legal information`,
300
+ confirm: `Confirm your booking`,
301
+ booked: `Booking confirmed`
302
+ },
303
+ actions: {
304
+ back: `Back`,
305
+ continue: `Continue`,
306
+ confirm: `Confirm booking`,
307
+ submitting: `Booking\u2026`
308
+ },
309
+ service: {
310
+ empty: `No services are available right now.`,
311
+ durationMinutes: `{minutes} min`,
312
+ price: `{amount}`
313
+ },
314
+ provider: {
315
+ empty: `No providers are available for this service.`,
316
+ any: `Any available provider`
317
+ },
318
+ date: {
319
+ unavailable: `Unavailable`,
320
+ minHint: `Earliest booking is tomorrow.`
321
+ },
322
+ time: {
323
+ empty: `No time slots are available on this date.`,
324
+ timezone: `All times in {timezone}`,
325
+ timezoneHint: `Times shown in {timezone}`,
326
+ mismatchBanner: `This business uses {businessTimezone}. You are in {customerTimezone}.`,
327
+ showMyTime: `Show in my time`,
328
+ showBusinessTime: `Show business time`,
329
+ yourTimeLabel: `Your time ({timezone})`,
330
+ businessTimeLabel: `Business time ({timezone})`
331
+ },
332
+ customer: {
333
+ required: `Required`,
334
+ invalidEmail: `Enter a valid email address.`
335
+ },
336
+ legal: {
337
+ cookie: `Cookie notice`,
338
+ terms: `Terms & conditions`,
339
+ privacy: `Privacy policy`,
340
+ acceptCookie: `I accept the cookie notice`,
341
+ acceptTerms: `I accept the terms & conditions`,
342
+ acceptPrivacy: `I accept the privacy policy`,
343
+ readMore: `Read`
344
+ },
345
+ confirm: {
346
+ summary: `Review your appointment`,
347
+ service: `Service`,
348
+ provider: `Provider`,
349
+ when: `When`,
350
+ customer: `Customer`,
351
+ error: `We could not complete your booking. Please try again.`,
352
+ conflict: `That time slot is no longer available. Please choose another time.`
353
+ },
354
+ booked: {
355
+ reference: `Reference`,
356
+ message: `Your appointment has been booked.`
357
+ },
358
+ disabled: {
359
+ title: `Booking unavailable`
360
+ },
361
+ fields: {
362
+ first_name: `First name`,
363
+ last_name: `Last name`,
364
+ email: `Email`,
365
+ phone: `Phone`,
366
+ address: `Address`,
367
+ city: `City`,
368
+ state: `State / province`,
369
+ zip: `Postal code`,
370
+ notes: `Notes`,
371
+ custom_1: `Additional information`,
372
+ custom_2: `Additional information (2)`,
373
+ custom_3: `Additional information (3)`,
374
+ custom_4: `Additional information (4)`,
375
+ custom_5: `Additional information (5)`
376
+ }
377
+ };
378
+
379
+ // src/i18n/create-translator.ts
380
+ function getNestedValue(obj, path) {
381
+ const parts = path.split(`.`);
382
+ let current = obj;
383
+ for (const part of parts) {
384
+ if (current === null || typeof current !== `object` || !(part in current)) {
385
+ return void 0;
386
+ }
387
+ current = current[part];
388
+ }
389
+ return typeof current === `string` ? current : void 0;
390
+ }
391
+ function interpolate(template, params) {
392
+ if (!params) {
393
+ return template;
394
+ }
395
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
396
+ const value = params[key];
397
+ return value === void 0 ? `{${key}}` : String(value);
398
+ });
399
+ }
400
+ function createTranslator(locale, tenantDefaultLanguage) {
401
+ return (key, params) => {
402
+ const template = getNestedValue(enCatalog, key);
403
+ if (!template) {
404
+ return key;
405
+ }
406
+ return interpolate(template, params);
407
+ };
408
+ }
409
+
410
+ // ../api-contracts/src/idempotency.ts
411
+ var MUTATION_METHODS = /* @__PURE__ */ new Set([`POST`, `PUT`, `PATCH`, `DELETE`]);
412
+ function createIdempotencyKey() {
413
+ return crypto.randomUUID();
414
+ }
415
+ function clientMutationIdempotencyHeaders(method) {
416
+ const normalizedMethod = method.toUpperCase();
417
+ if (!MUTATION_METHODS.has(normalizedMethod)) {
418
+ return {};
419
+ }
420
+ return { "Idempotency-Key": createIdempotencyKey() };
421
+ }
422
+
423
+ // src/lib/fetch-booking-client.ts
424
+ var DEFAULT_API_URL = `https://api.easyappts.com`;
425
+ var BookingApiError = class extends Error {
426
+ constructor(status, code, message) {
427
+ super(message);
428
+ this.status = status;
429
+ this.code = code;
430
+ this.name = `BookingApiError`;
431
+ }
432
+ status;
433
+ code;
434
+ };
435
+ var BookingClient = class {
436
+ baseUrl;
437
+ publishableKey;
438
+ origin;
439
+ constructor(opts) {
440
+ if (!opts.publishableKey.startsWith(`pk_`)) {
441
+ throw new Error(`BookingClient requires a publishable key (pk_*).`);
442
+ }
443
+ this.publishableKey = opts.publishableKey;
444
+ this.baseUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, ``);
445
+ this.origin = opts.origin;
446
+ }
447
+ async getTenantConfig() {
448
+ return this.getJson(`/v1/public/tenant-config`);
449
+ }
450
+ async listServices() {
451
+ const body = await this.getJson(`/v1/public/services`);
452
+ return body.data;
453
+ }
454
+ async listProviders(params) {
455
+ const query = params?.serviceId ? `?serviceId=${encodeURIComponent(params.serviceId)}` : ``;
456
+ const body = await this.getJson(
457
+ `/v1/public/providers${query}`
458
+ );
459
+ return body.data;
460
+ }
461
+ async getAvailability(params) {
462
+ const query = new URLSearchParams({
463
+ providerId: params.providerId,
464
+ serviceId: params.serviceId,
465
+ date: params.date
466
+ });
467
+ const body = await this.getJson(
468
+ `/v1/public/availabilities?${query.toString()}`
469
+ );
470
+ return body.data;
471
+ }
472
+ async getUnavailableDates(params) {
473
+ const query = new URLSearchParams({
474
+ providerId: params.providerId,
475
+ serviceId: params.serviceId,
476
+ month: params.month
477
+ });
478
+ const body = await this.getJson(
479
+ `/v1/public/unavailable-dates?${query.toString()}`
480
+ );
481
+ return body.data;
482
+ }
483
+ async createAppointment(body) {
484
+ return this.postJson(`/v1/public/appointments`, body);
485
+ }
486
+ headers() {
487
+ const headers = {
488
+ Authorization: `Bearer ${this.publishableKey}`,
489
+ Accept: `application/json`
490
+ };
491
+ if (this.origin) {
492
+ headers.Origin = this.origin;
493
+ }
494
+ return headers;
495
+ }
496
+ async getJson(path) {
497
+ const res = await fetch(`${this.baseUrl}${path}`, { headers: this.headers() });
498
+ return this.parseJson(res);
499
+ }
500
+ async postJson(path, body) {
501
+ const res = await fetch(`${this.baseUrl}${path}`, {
502
+ method: `POST`,
503
+ headers: {
504
+ ...this.headers(),
505
+ "Content-Type": `application/json`,
506
+ ...clientMutationIdempotencyHeaders(`POST`)
507
+ },
508
+ body: JSON.stringify(body)
509
+ });
510
+ return this.parseJson(res);
511
+ }
512
+ async parseJson(res) {
513
+ const payload = await res.json().catch(() => null);
514
+ if (!res.ok) {
515
+ const err = payload !== null && typeof payload === `object` && `error` in payload && payload.error !== null && typeof payload.error === `object` ? payload.error : null;
516
+ const code = err !== null && `code` in err && typeof err.code === `string` ? err.code : `unknown`;
517
+ const message = err !== null && `message` in err && typeof err.message === `string` ? err.message : res.statusText;
518
+ throw new BookingApiError(res.status, code, message);
519
+ }
520
+ return payload;
521
+ }
522
+ };
523
+
524
+ // src/lib/create-booking-client.ts
525
+ function createBookingClient(opts) {
526
+ return new BookingClient(opts);
527
+ }
528
+
529
+ // src/utils/booking-timezone.ts
530
+ function getCustomerTimezone() {
531
+ if (typeof Intl === `undefined`) {
532
+ return `UTC`;
533
+ }
534
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || `UTC`;
535
+ }
536
+ function resolveBusinessTimezone(params) {
537
+ const anyId = params.anyProviderId ?? ANY_PROVIDER_ID;
538
+ if (params.providerId === anyId) {
539
+ return params.tenantDefaultTimezone;
540
+ }
541
+ const provider = params.providers.find((p) => p.id === params.providerId);
542
+ return provider?.timezone ?? params.tenantDefaultTimezone;
543
+ }
544
+ function allowsTimeDisplayToggle(bookingTimeDisplay) {
545
+ return bookingTimeDisplay === `customer`;
546
+ }
547
+ function resolveDisplayTimezone(params) {
548
+ const setting = params.bookingTimeDisplay ?? `business`;
549
+ const mode = params.timeDisplayMode ?? `business`;
550
+ if (setting === `business`) {
551
+ return params.businessTimezone;
552
+ }
553
+ if (setting === `customer_only`) {
554
+ return params.customerTimezone;
555
+ }
556
+ return mode === `customer` ? params.customerTimezone : params.businessTimezone;
557
+ }
558
+ function shouldShowSecondaryTimezone(params) {
559
+ if (!params.zonesDiffer) {
560
+ return false;
561
+ }
562
+ const setting = params.bookingTimeDisplay ?? `business`;
563
+ return setting === `customer` || setting === `customer_only`;
564
+ }
565
+ var DEFAULT_API_URL2 = `https://api.easyappts.com`;
566
+ function normalizeColorScheme(value) {
567
+ if (value === `dark` || value === `light` || value === `system`) {
568
+ return value;
569
+ }
570
+ return void 0;
571
+ }
572
+ function readUrlPrefill() {
573
+ if (typeof window === `undefined`) {
574
+ return {};
575
+ }
576
+ const params = new URLSearchParams(window.location.search);
577
+ const result = {};
578
+ const serviceId = params.get(`service`);
579
+ const providerId = params.get(`provider`);
580
+ if (serviceId) {
581
+ result.serviceId = serviceId;
582
+ }
583
+ if (providerId) {
584
+ result.providerId = providerId;
585
+ }
586
+ return result;
587
+ }
588
+ function BookingProvider({
589
+ publishableKey,
590
+ apiUrl = DEFAULT_API_URL2,
591
+ locale,
592
+ theme = {},
593
+ onBooked,
594
+ initialServiceId,
595
+ initialProviderId,
596
+ origin,
597
+ className,
598
+ style,
599
+ botProtection = true,
600
+ children
601
+ }) {
602
+ const resolvedApiUrl = apiUrl.replace(/\/$/, ``);
603
+ const client$1 = react.useMemo(
604
+ () => createBookingClient({ publishableKey, apiUrl: resolvedApiUrl, origin }),
605
+ [publishableKey, resolvedApiUrl, origin]
606
+ );
607
+ const [tenantConfig, setTenantConfig] = react.useState(null);
608
+ const [tenantConfigLoading, setTenantConfigLoading] = react.useState(true);
609
+ const [tenantConfigError, setTenantConfigError] = react.useState(null);
610
+ const prefill = react.useMemo(() => {
611
+ const fromUrl = readUrlPrefill();
612
+ return {
613
+ serviceId: initialServiceId ?? fromUrl.serviceId,
614
+ providerId: initialProviderId ?? fromUrl.providerId
615
+ };
616
+ }, [initialProviderId, initialServiceId]);
617
+ const { state: bookingState, dispatch } = useBookingReducer(prefill, tenantConfig);
618
+ const customerTimezone = react.useMemo(() => getCustomerTimezone(), []);
619
+ const setTimeDisplayMode = react.useCallback(
620
+ (mode) => {
621
+ dispatch({ type: `set_time_display_mode`, mode });
622
+ },
623
+ [dispatch]
624
+ );
625
+ react.useEffect(() => {
626
+ if (prefill.serviceId || prefill.providerId) {
627
+ dispatch({
628
+ type: `prefill`,
629
+ serviceId: prefill.serviceId,
630
+ providerId: prefill.providerId
631
+ });
632
+ }
633
+ }, [dispatch, prefill.providerId, prefill.serviceId]);
634
+ react.useEffect(() => {
635
+ let cancelled = false;
636
+ setTenantConfigLoading(true);
637
+ setTenantConfigError(null);
638
+ void client$1.getTenantConfig().then((config) => {
639
+ if (!cancelled) {
640
+ setTenantConfig(config);
641
+ }
642
+ }).catch((err) => {
643
+ if (!cancelled) {
644
+ setTenantConfigError(err instanceof Error ? err.message : `Failed to load settings`);
645
+ }
646
+ }).finally(() => {
647
+ if (!cancelled) {
648
+ setTenantConfigLoading(false);
649
+ }
650
+ });
651
+ return () => {
652
+ cancelled = true;
653
+ };
654
+ }, [client$1]);
655
+ const resolvedLocale = locale ?? tenantConfig?.localization.defaultLanguage ?? `en`;
656
+ const t = react.useMemo(
657
+ () => createTranslator(resolvedLocale, tenantConfig?.localization.defaultLanguage),
658
+ [resolvedLocale, tenantConfig?.localization.defaultLanguage]
659
+ );
660
+ const cssVars = react.useMemo(() => {
661
+ const vars = {};
662
+ if (tenantConfig?.business.brandColor) {
663
+ vars[`--ea-brand-fallback`] = tenantConfig.business.brandColor;
664
+ }
665
+ if (theme.brandColor) vars[`--ea-brand`] = theme.brandColor;
666
+ if (theme.onBrandColor) vars[`--ea-on-brand`] = theme.onBrandColor;
667
+ if (theme.textColor) vars[`--ea-text`] = theme.textColor;
668
+ if (theme.mutedColor) vars[`--ea-muted`] = theme.mutedColor;
669
+ if (theme.borderColor) vars[`--ea-border`] = theme.borderColor;
670
+ if (theme.surfaceColor) vars[`--ea-surface`] = theme.surfaceColor;
671
+ if (theme.errorColor) vars[`--ea-error`] = theme.errorColor;
672
+ if (theme.borderRadius) vars[`--ea-radius`] = theme.borderRadius;
673
+ if (theme.fontFamily) vars[`--ea-font`] = theme.fontFamily;
674
+ if (theme.maxWidth) vars[`--ea-max-width`] = theme.maxWidth;
675
+ return vars;
676
+ }, [
677
+ tenantConfig?.business.brandColor,
678
+ theme.brandColor,
679
+ theme.onBrandColor,
680
+ theme.textColor,
681
+ theme.mutedColor,
682
+ theme.borderColor,
683
+ theme.surfaceColor,
684
+ theme.errorColor,
685
+ theme.borderRadius,
686
+ theme.fontFamily,
687
+ theme.maxWidth
688
+ ]);
689
+ const colorScheme = theme.colorScheme ?? normalizeColorScheme(tenantConfig?.business.theme);
690
+ const rootStyle = react.useMemo(
691
+ () => ({ ...cssVars, ...style }),
692
+ [cssVars, style]
693
+ );
694
+ const rootClassName = className ? `ea-booking-widget ${className}` : `ea-booking-widget`;
695
+ const value = react.useMemo(
696
+ () => ({
697
+ client: client$1,
698
+ apiUrl: resolvedApiUrl,
699
+ tenantConfig,
700
+ tenantConfigLoading,
701
+ tenantConfigError,
702
+ locale: resolvedLocale,
703
+ theme,
704
+ t,
705
+ bookingState,
706
+ dispatch,
707
+ customerTimezone,
708
+ timeDisplayMode: bookingState.timeDisplayMode,
709
+ setTimeDisplayMode,
710
+ onBooked
711
+ }),
712
+ [
713
+ bookingState,
714
+ client$1,
715
+ customerTimezone,
716
+ dispatch,
717
+ onBooked,
718
+ setTimeDisplayMode,
719
+ resolvedApiUrl,
720
+ resolvedLocale,
721
+ t,
722
+ tenantConfig,
723
+ tenantConfigError,
724
+ tenantConfigLoading,
725
+ theme
726
+ ]
727
+ );
728
+ return /* @__PURE__ */ jsxRuntime.jsxs(BookingContext.Provider, { value, children: [
729
+ botProtection ? /* @__PURE__ */ jsxRuntime.jsx(client.BotIdClient, { protect: [{ path: `/v1/public/appointments`, method: `POST` }] }) : null,
730
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: rootClassName, "data-theme": colorScheme, style: rootStyle, children })
731
+ ] });
732
+ }
733
+
734
+ // src/hooks/use-tenant-config.ts
735
+ function useTenantConfig() {
736
+ const { tenantConfig, tenantConfigLoading, tenantConfigError } = useBookingContext();
737
+ return { tenantConfig, loading: tenantConfigLoading, error: tenantConfigError };
738
+ }
739
+ function useProviders(serviceId) {
740
+ const { client } = useBookingContext();
741
+ const [data, setData] = react.useState([]);
742
+ const [loading, setLoading] = react.useState(false);
743
+ const [error, setError] = react.useState(null);
744
+ const reload = react.useCallback(async () => {
745
+ if (!serviceId) {
746
+ setData([]);
747
+ return;
748
+ }
749
+ setLoading(true);
750
+ setError(null);
751
+ try {
752
+ const providers = await client.listProviders({ serviceId });
753
+ setData(providers);
754
+ } catch (err) {
755
+ setError(err instanceof Error ? err.message : `Failed to load providers`);
756
+ } finally {
757
+ setLoading(false);
758
+ }
759
+ }, [client, serviceId]);
760
+ react.useEffect(() => {
761
+ void reload();
762
+ }, [reload]);
763
+ return { providers: data, loading, error, reload };
764
+ }
765
+
766
+ // src/hooks/use-booking-timezones.ts
767
+ function useBookingTimezones(providerId, serviceId) {
768
+ const { customerTimezone, bookingState } = useBookingContext();
769
+ const { tenantConfig } = useTenantConfig();
770
+ const { providers } = useProviders(serviceId);
771
+ const bookingTimeDisplay = tenantConfig?.booking.bookingTimeDisplay;
772
+ const businessTimezone = react.useMemo(() => {
773
+ if (!tenantConfig) {
774
+ return `UTC`;
775
+ }
776
+ if (providerId === void 0) {
777
+ return tenantConfig.localization.defaultTimezone;
778
+ }
779
+ return resolveBusinessTimezone({
780
+ providerId,
781
+ providers,
782
+ tenantDefaultTimezone: tenantConfig.localization.defaultTimezone
783
+ });
784
+ }, [providerId, providers, tenantConfig]);
785
+ const displayTimezone = react.useMemo(
786
+ () => resolveDisplayTimezone({
787
+ businessTimezone,
788
+ customerTimezone,
789
+ bookingTimeDisplay,
790
+ timeDisplayMode: bookingState.timeDisplayMode
791
+ }),
792
+ [
793
+ bookingState.timeDisplayMode,
794
+ bookingTimeDisplay,
795
+ businessTimezone,
796
+ customerTimezone
797
+ ]
798
+ );
799
+ const zonesDiffer = customerTimezone !== businessTimezone;
800
+ const secondaryTimezone = react.useMemo(() => {
801
+ if (!shouldShowSecondaryTimezone({
802
+ zonesDiffer,
803
+ bookingTimeDisplay
804
+ })) {
805
+ return null;
806
+ }
807
+ return displayTimezone === businessTimezone ? customerTimezone : businessTimezone;
808
+ }, [bookingTimeDisplay, businessTimezone, customerTimezone, displayTimezone, zonesDiffer]);
809
+ return {
810
+ businessTimezone,
811
+ customerTimezone,
812
+ displayTimezone,
813
+ secondaryTimezone,
814
+ zonesDiffer,
815
+ bookingTimeDisplay,
816
+ timeDisplayMode: bookingState.timeDisplayMode
817
+ };
818
+ }
819
+ function todayInZone(timeZone) {
820
+ return polyfill.Temporal.Now.zonedDateTimeISO(timeZone).toPlainDate();
821
+ }
822
+ function minBookableFromToday(today) {
823
+ return today.add({ days: 1 });
824
+ }
825
+ function minBookableDate(timeZone) {
826
+ return minBookableFromToday(todayInZone(timeZone));
827
+ }
828
+ function maxBookableFromToday(today, futureBookingLimitDays) {
829
+ return today.add({ days: futureBookingLimitDays });
830
+ }
831
+ function maxBookableDate(futureBookingLimitDays, timeZone) {
832
+ return maxBookableFromToday(todayInZone(timeZone), futureBookingLimitDays);
833
+ }
834
+ function formatCalendarDate(date) {
835
+ return date.toString();
836
+ }
837
+ function startOfMonth(date) {
838
+ return date.with({ day: 1 });
839
+ }
840
+ function formatMonthKey(date) {
841
+ const y = date.year;
842
+ const m = String(date.month).padStart(2, `0`);
843
+ return `${y}-${m}`;
844
+ }
845
+ function buildMonthDays(month, firstWeekday) {
846
+ const start = startOfMonth(month);
847
+ const jsDay = start.dayOfWeek % 7;
848
+ const offset = firstWeekday === `monday` ? (jsDay + 6) % 7 : firstWeekday === `saturday` ? (jsDay + 1) % 7 : jsDay;
849
+ const cells = Array.from({ length: offset }, () => null);
850
+ let cursor = start;
851
+ while (cursor.month === start.month) {
852
+ cells.push(cursor);
853
+ cursor = cursor.add({ days: 1 });
854
+ }
855
+ while (cells.length % 7 !== 0) {
856
+ cells.push(null);
857
+ }
858
+ return cells;
859
+ }
860
+ function isDateInRange(value, min, max) {
861
+ return value >= min && value <= max;
862
+ }
863
+ function formatSlotTime(iso, locale, timeZone) {
864
+ return new Intl.DateTimeFormat(locale, {
865
+ hour: `numeric`,
866
+ minute: `2-digit`,
867
+ timeZone
868
+ }).format(new Date(iso));
869
+ }
870
+ function formatSlotDate(iso, locale, timeZone) {
871
+ return new Intl.DateTimeFormat(locale, {
872
+ weekday: `long`,
873
+ year: `numeric`,
874
+ month: `long`,
875
+ day: `numeric`,
876
+ timeZone
877
+ }).format(new Date(iso));
878
+ }
879
+ function formatSlotDateTimeDual(iso, locale, primaryTz, secondaryTz) {
880
+ const primary = `${formatSlotDate(iso, locale, primaryTz)} ${formatSlotTime(iso, locale, primaryTz)}`;
881
+ if (!secondaryTz || secondaryTz === primaryTz) {
882
+ return primary;
883
+ }
884
+ return `${primary} (${formatSlotTime(iso, locale, secondaryTz)})`;
885
+ }
886
+ function SlotTimeDisplay({
887
+ iso,
888
+ displayTimezone,
889
+ secondaryTimezone,
890
+ customerTimezone,
891
+ variant = `time`
892
+ }) {
893
+ const { locale, t } = useBookingContext();
894
+ const primary = variant === `datetime` ? `${formatSlotDate(iso, locale, displayTimezone)} ${formatSlotTime(iso, locale, displayTimezone)}` : formatSlotTime(iso, locale, displayTimezone);
895
+ if (!secondaryTimezone) {
896
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: primary });
897
+ }
898
+ const secondaryLabel = secondaryTimezone === customerTimezone ? t(`time.yourTimeLabel`, { timezone: secondaryTimezone }) : t(`time.businessTimeLabel`, { timezone: secondaryTimezone });
899
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ea-slot-time-display", children: [
900
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: primary }),
901
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ea-time-secondary", children: [
902
+ secondaryLabel,
903
+ ": ",
904
+ formatSlotTime(iso, locale, secondaryTimezone)
905
+ ] })
906
+ ] });
907
+ }
908
+ function TimezoneMismatchBanner({
909
+ businessTimezone,
910
+ customerTimezone,
911
+ timeDisplayMode,
912
+ bookingTimeDisplay
913
+ }) {
914
+ const { t, setTimeDisplayMode } = useBookingContext();
915
+ if (customerTimezone === businessTimezone) {
916
+ return null;
917
+ }
918
+ const showToggle = allowsTimeDisplayToggle(bookingTimeDisplay);
919
+ const nextMode = timeDisplayMode === `business` ? `customer` : `business`;
920
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-timezone-banner", role: "note", children: [
921
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-timezone-banner-text", children: t(`time.mismatchBanner`, { businessTimezone, customerTimezone }) }),
922
+ showToggle ? /* @__PURE__ */ jsxRuntime.jsx(
923
+ "button",
924
+ {
925
+ type: "button",
926
+ className: "ea-time-toggle ea-link",
927
+ onClick: () => setTimeDisplayMode(nextMode),
928
+ children: timeDisplayMode === `business` ? t(`time.showMyTime`) : t(`time.showBusinessTime`)
929
+ }
930
+ ) : null
931
+ ] });
932
+ }
933
+ function StepTitle({
934
+ children,
935
+ id
936
+ }) {
937
+ return /* @__PURE__ */ jsxRuntime.jsx("h2", { id, className: "ea-step-title", children });
938
+ }
939
+ function Button({
940
+ variant = `primary`,
941
+ ...props
942
+ }) {
943
+ return /* @__PURE__ */ jsxRuntime.jsx(
944
+ "button",
945
+ {
946
+ type: "button",
947
+ className: variant === `primary` ? `ea-btn ea-btn-primary` : `ea-btn ea-btn-secondary`,
948
+ ...props
949
+ }
950
+ );
951
+ }
952
+ function Field({
953
+ label,
954
+ error,
955
+ children
956
+ }) {
957
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ea-field", children: [
958
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-field-label", children: label }),
959
+ children,
960
+ error ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-field-error", children: error }) : null
961
+ ] });
962
+ }
963
+ function TextInput(props) {
964
+ return /* @__PURE__ */ jsxRuntime.jsx("input", { className: "ea-input", ...props });
965
+ }
966
+ function TextArea(props) {
967
+ return /* @__PURE__ */ jsxRuntime.jsx("textarea", { className: "ea-input ea-textarea", ...props });
968
+ }
969
+ function LoadingState({ label }) {
970
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: label });
971
+ }
972
+ function ErrorState({ message }) {
973
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-error", role: "alert", children: message });
974
+ }
975
+ function BookedSummary() {
976
+ const { t } = useBookingContext();
977
+ const { state } = useBooking();
978
+ const appointment = state.step === `booked` ? state.appointment : void 0;
979
+ const {
980
+ businessTimezone,
981
+ customerTimezone,
982
+ displayTimezone,
983
+ secondaryTimezone,
984
+ timeDisplayMode,
985
+ bookingTimeDisplay
986
+ } = useBookingTimezones(appointment?.providerId, appointment?.serviceId);
987
+ if (state.step !== `booked`) {
988
+ return null;
989
+ }
990
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step ea-booked", "aria-labelledby": "ea-step-booked", children: [
991
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-booked", children: t(`steps.booked`) }),
992
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: t(`booked.message`) }),
993
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "ea-booked-ref", children: [
994
+ t(`booked.reference`),
995
+ ": ",
996
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: state.appointment.hash })
997
+ ] }),
998
+ /* @__PURE__ */ jsxRuntime.jsx(
999
+ TimezoneMismatchBanner,
1000
+ {
1001
+ businessTimezone,
1002
+ customerTimezone,
1003
+ timeDisplayMode,
1004
+ bookingTimeDisplay
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: /* @__PURE__ */ jsxRuntime.jsx(
1008
+ SlotTimeDisplay,
1009
+ {
1010
+ iso: state.appointment.startAt,
1011
+ displayTimezone,
1012
+ secondaryTimezone,
1013
+ customerTimezone,
1014
+ variant: `datetime`
1015
+ }
1016
+ ) })
1017
+ ] });
1018
+ }
1019
+ function useServices() {
1020
+ const { client } = useBookingContext();
1021
+ const [data, setData] = react.useState([]);
1022
+ const [loading, setLoading] = react.useState(true);
1023
+ const [error, setError] = react.useState(null);
1024
+ const reload = react.useCallback(async () => {
1025
+ setLoading(true);
1026
+ setError(null);
1027
+ try {
1028
+ const services = await client.listServices();
1029
+ setData(services);
1030
+ } catch (err) {
1031
+ setError(err instanceof Error ? err.message : `Failed to load services`);
1032
+ } finally {
1033
+ setLoading(false);
1034
+ }
1035
+ }, [client]);
1036
+ react.useEffect(() => {
1037
+ void reload();
1038
+ }, [reload]);
1039
+ return { services: data, loading, error, reload };
1040
+ }
1041
+ async function resolveProviderForAny(client, providerIds, serviceId, date, startAt) {
1042
+ for (const providerId of providerIds) {
1043
+ const slots = await client.getAvailability({ providerId, serviceId, date });
1044
+ if (slots.some((slot) => slot.startAt === startAt)) {
1045
+ return providerId;
1046
+ }
1047
+ }
1048
+ return null;
1049
+ }
1050
+ function ConfirmStep() {
1051
+ const { t, dispatch, client, onBooked } = useBookingContext();
1052
+ const { state } = useBooking();
1053
+ const { tenantConfig } = useTenantConfig();
1054
+ const { services } = useServices();
1055
+ const serviceId = state.step === `confirm` ? state.serviceId : void 0;
1056
+ const { providers } = useProviders(serviceId);
1057
+ const providerId = state.step === `confirm` ? state.providerId : void 0;
1058
+ const {
1059
+ businessTimezone,
1060
+ customerTimezone,
1061
+ displayTimezone,
1062
+ secondaryTimezone,
1063
+ timeDisplayMode,
1064
+ bookingTimeDisplay
1065
+ } = useBookingTimezones(providerId, serviceId);
1066
+ const [submitting, setSubmitting] = react.useState(false);
1067
+ const [error, setError] = react.useState(null);
1068
+ if (state.step !== `confirm` || !tenantConfig) {
1069
+ return null;
1070
+ }
1071
+ const confirm = state;
1072
+ const service = services.find((s) => s.id === confirm.serviceId);
1073
+ const provider = confirm.providerId === ANY_PROVIDER_ID ? null : providers.find((p) => p.id === confirm.providerId);
1074
+ async function handleConfirm() {
1075
+ setSubmitting(true);
1076
+ setError(null);
1077
+ try {
1078
+ let providerId2;
1079
+ if (confirm.providerId === ANY_PROVIDER_ID) {
1080
+ const eligible = providers.filter((p) => service?.providerIds.includes(p.id) ?? true).map((p) => p.id);
1081
+ const resolved = await resolveProviderForAny(
1082
+ client,
1083
+ eligible,
1084
+ confirm.serviceId,
1085
+ confirm.date,
1086
+ confirm.slot.startAt
1087
+ );
1088
+ if (!resolved) {
1089
+ setError(t(`confirm.conflict`));
1090
+ return;
1091
+ }
1092
+ providerId2 = resolved;
1093
+ } else {
1094
+ providerId2 = confirm.providerId;
1095
+ }
1096
+ const appointment = await client.createAppointment({
1097
+ providerId: providerId2,
1098
+ serviceId: confirm.serviceId,
1099
+ startAt: confirm.slot.startAt,
1100
+ ...customerValuesToAppointmentBody(confirm.customer),
1101
+ consents: confirm.consents
1102
+ });
1103
+ dispatch({ type: `booked`, appointment });
1104
+ onBooked?.(appointment);
1105
+ } catch (err) {
1106
+ if (err instanceof BookingApiError && err.status === 409) {
1107
+ setError(t(`confirm.conflict`));
1108
+ } else {
1109
+ setError(t(`confirm.error`));
1110
+ }
1111
+ } finally {
1112
+ setSubmitting(false);
1113
+ }
1114
+ }
1115
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-confirm", children: [
1116
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-confirm", children: t(`steps.confirm`) }),
1117
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: t(`confirm.summary`) }),
1118
+ /* @__PURE__ */ jsxRuntime.jsx(
1119
+ TimezoneMismatchBanner,
1120
+ {
1121
+ businessTimezone,
1122
+ customerTimezone,
1123
+ timeDisplayMode,
1124
+ bookingTimeDisplay
1125
+ }
1126
+ ),
1127
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "ea-summary", children: [
1128
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1129
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: t(`confirm.service`) }),
1130
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: service?.name ?? confirm.serviceId })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1133
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: t(`confirm.provider`) }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: confirm.providerId === ANY_PROVIDER_ID ? t(`provider.any`) : provider?.displayName ?? confirm.providerId })
1135
+ ] }),
1136
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1137
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: t(`confirm.when`) }),
1138
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: /* @__PURE__ */ jsxRuntime.jsx(
1139
+ SlotTimeDisplay,
1140
+ {
1141
+ iso: confirm.slot.startAt,
1142
+ displayTimezone,
1143
+ secondaryTimezone,
1144
+ customerTimezone,
1145
+ variant: `datetime`
1146
+ }
1147
+ ) })
1148
+ ] }),
1149
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1150
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: t(`confirm.customer`) }),
1151
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { children: [
1152
+ confirm.customer.firstName,
1153
+ " ",
1154
+ confirm.customer.lastName,
1155
+ " \u2014 ",
1156
+ confirm.customer.email
1157
+ ] })
1158
+ ] })
1159
+ ] }),
1160
+ error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { message: error }) : null,
1161
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-step-actions", children: [
1162
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), disabled: submitting, children: t(`actions.back`) }),
1163
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: () => void handleConfirm(), disabled: submitting, children: submitting ? t(`actions.submitting`) : t(`actions.confirm`) })
1164
+ ] })
1165
+ ] });
1166
+ }
1167
+ function CustomerForm() {
1168
+ const { t, dispatch } = useBookingContext();
1169
+ const { state } = useBooking();
1170
+ const { tenantConfig } = useTenantConfig();
1171
+ const [values, setValues] = react.useState(emptyCustomerValues);
1172
+ const [errors, setErrors] = react.useState({});
1173
+ if (state.step !== `customer` || !tenantConfig) {
1174
+ return null;
1175
+ }
1176
+ const fields = visibleFormFields(tenantConfig.formFields);
1177
+ function setField(fieldKey, value) {
1178
+ const key = customerValueKey(fieldKey);
1179
+ if (!key) {
1180
+ return;
1181
+ }
1182
+ setValues((current) => ({ ...current, [key]: value }));
1183
+ }
1184
+ function handleSubmit() {
1185
+ const nextErrors = validateCustomerFields(tenantConfig.formFields, values);
1186
+ setErrors(nextErrors);
1187
+ if (Object.keys(nextErrors).length > 0) {
1188
+ return;
1189
+ }
1190
+ dispatch({ type: `set_customer`, customer: values });
1191
+ }
1192
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-customer", children: [
1193
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-customer", children: t(`steps.customer`) }),
1194
+ /* @__PURE__ */ jsxRuntime.jsxs(
1195
+ "form",
1196
+ {
1197
+ className: "ea-form",
1198
+ onSubmit: (event) => {
1199
+ event.preventDefault();
1200
+ handleSubmit();
1201
+ },
1202
+ children: [
1203
+ fields.map((field) => {
1204
+ const valueKey = customerValueKey(field.fieldKey);
1205
+ if (!valueKey) {
1206
+ return null;
1207
+ }
1208
+ const label = field.label ?? (fieldLabelKey(field.fieldKey) ? t(fieldLabelKey(field.fieldKey)) : field.fieldKey);
1209
+ const value = values[valueKey];
1210
+ const error = errors[field.fieldKey];
1211
+ const isNotes = field.fieldKey === `notes`;
1212
+ return /* @__PURE__ */ jsxRuntime.jsx(
1213
+ Field,
1214
+ {
1215
+ label: `${label}${field.required ? ` *` : ``}`,
1216
+ error: error === `required` ? t(`customer.required`) : error === `invalid` ? t(`customer.invalidEmail`) : void 0,
1217
+ children: isNotes ? /* @__PURE__ */ jsxRuntime.jsx(
1218
+ TextArea,
1219
+ {
1220
+ value,
1221
+ onChange: (e) => setField(field.fieldKey, e.target.value),
1222
+ rows: 3
1223
+ }
1224
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1225
+ TextInput,
1226
+ {
1227
+ type: field.fieldKey === `email` ? `email` : `text`,
1228
+ value,
1229
+ onChange: (e) => setField(field.fieldKey, e.target.value),
1230
+ autoComplete: field.fieldKey === `email` ? `email` : void 0
1231
+ }
1232
+ )
1233
+ },
1234
+ field.fieldKey
1235
+ );
1236
+ }),
1237
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-step-actions", children: [
1238
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", type: "button", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) }),
1239
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "submit", children: t(`actions.continue`) })
1240
+ ] })
1241
+ ]
1242
+ }
1243
+ )
1244
+ ] });
1245
+ }
1246
+ function useUnavailableDates(params) {
1247
+ const { client } = useBookingContext();
1248
+ const [dates, setDates] = react.useState([]);
1249
+ const [loading, setLoading] = react.useState(false);
1250
+ const [error, setError] = react.useState(null);
1251
+ const reload = react.useCallback(async () => {
1252
+ if (!params.serviceId || !params.month) {
1253
+ setDates([]);
1254
+ return;
1255
+ }
1256
+ setLoading(true);
1257
+ setError(null);
1258
+ try {
1259
+ const providerIds = params.providerId === ANY_PROVIDER_ID ? params.providerIdsForAny ?? [] : params.providerId ? [params.providerId] : [];
1260
+ if (providerIds.length === 0) {
1261
+ setDates([]);
1262
+ return;
1263
+ }
1264
+ const lists = await Promise.all(
1265
+ providerIds.map(
1266
+ (providerId) => client.getUnavailableDates({
1267
+ providerId,
1268
+ serviceId: params.serviceId,
1269
+ month: params.month
1270
+ })
1271
+ )
1272
+ );
1273
+ const merged = /* @__PURE__ */ new Set();
1274
+ for (const list of lists) {
1275
+ for (const date of list) {
1276
+ merged.add(date);
1277
+ }
1278
+ }
1279
+ setDates([...merged]);
1280
+ } catch (err) {
1281
+ setError(err instanceof Error ? err.message : `Failed to load unavailable dates`);
1282
+ } finally {
1283
+ setLoading(false);
1284
+ }
1285
+ }, [client, params.month, params.providerId, params.providerIdsForAny, params.serviceId]);
1286
+ react.useEffect(() => {
1287
+ void reload();
1288
+ }, [reload]);
1289
+ return { unavailableDates: dates, loading, error, reload };
1290
+ }
1291
+ function DatePicker() {
1292
+ const { t, dispatch } = useBookingContext();
1293
+ const { state } = useBooking();
1294
+ const { tenantConfig } = useTenantConfig();
1295
+ const [viewMonth, setViewMonth] = react.useState(() => startOfMonth(minBookableDate(`UTC`)));
1296
+ const serviceId = state.step === `date` ? state.serviceId : void 0;
1297
+ const providerId = state.step === `date` ? state.providerId : void 0;
1298
+ const { providers } = useProviders(serviceId);
1299
+ const providerIdsForAny = react.useMemo(
1300
+ () => providers.map((p) => p.id),
1301
+ [providers]
1302
+ );
1303
+ const { businessTimezone, customerTimezone, timeDisplayMode, bookingTimeDisplay } = useBookingTimezones(providerId, serviceId);
1304
+ const month = formatMonthKey(viewMonth);
1305
+ const { unavailableDates, loading, error } = useUnavailableDates({
1306
+ serviceId,
1307
+ providerId,
1308
+ month,
1309
+ providerIdsForAny
1310
+ });
1311
+ if (state.step !== `date` || !tenantConfig) {
1312
+ return null;
1313
+ }
1314
+ const minDate = formatCalendarDate(minBookableDate(businessTimezone));
1315
+ const maxDate = formatCalendarDate(
1316
+ maxBookableDate(tenantConfig.booking.futureBookingLimitDays, businessTimezone)
1317
+ );
1318
+ const unavailable = new Set(unavailableDates);
1319
+ const days = buildMonthDays(viewMonth, tenantConfig.localization.firstWeekday);
1320
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-date", children: [
1321
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-date", children: t(`steps.date`) }),
1322
+ /* @__PURE__ */ jsxRuntime.jsx(
1323
+ TimezoneMismatchBanner,
1324
+ {
1325
+ businessTimezone,
1326
+ customerTimezone,
1327
+ timeDisplayMode,
1328
+ bookingTimeDisplay
1329
+ }
1330
+ ),
1331
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: t(`date.minHint`) }),
1332
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-calendar-nav", children: [
1333
+ /* @__PURE__ */ jsxRuntime.jsx(
1334
+ Button,
1335
+ {
1336
+ variant: "secondary",
1337
+ onClick: () => setViewMonth((m) => startOfMonth(m.subtract({ months: 1 }))),
1338
+ "aria-label": "Previous month",
1339
+ children: "\u2039"
1340
+ }
1341
+ ),
1342
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-calendar-month", children: month }),
1343
+ /* @__PURE__ */ jsxRuntime.jsx(
1344
+ Button,
1345
+ {
1346
+ variant: "secondary",
1347
+ onClick: () => setViewMonth((m) => startOfMonth(m.add({ months: 1 }))),
1348
+ "aria-label": "Next month",
1349
+ children: "\u203A"
1350
+ }
1351
+ )
1352
+ ] }),
1353
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "\u2026" }) : null,
1354
+ error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { message: error }) : null,
1355
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ea-calendar-grid", role: "grid", children: days.map((day, index) => {
1356
+ if (!day) {
1357
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-calendar-pad" }, `pad-${index}`);
1358
+ }
1359
+ const value = formatCalendarDate(day);
1360
+ const inRange = isDateInRange(value, minDate, maxDate);
1361
+ const isUnavailable = unavailable.has(value) || !inRange;
1362
+ return /* @__PURE__ */ jsxRuntime.jsx(
1363
+ "button",
1364
+ {
1365
+ type: "button",
1366
+ className: isUnavailable ? `ea-calendar-day ea-calendar-day-disabled` : `ea-calendar-day`,
1367
+ disabled: isUnavailable,
1368
+ onClick: () => dispatch({ type: `select_date`, date: value }),
1369
+ "aria-label": value,
1370
+ children: day.day
1371
+ },
1372
+ value
1373
+ );
1374
+ }) }),
1375
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) })
1376
+ ] });
1377
+ }
1378
+ function DisabledBooking() {
1379
+ const { t } = useBookingContext();
1380
+ const { tenantConfig } = useTenantConfig();
1381
+ if (!tenantConfig?.booking.disableBooking) {
1382
+ return null;
1383
+ }
1384
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step ea-disabled", "aria-labelledby": "ea-disabled-title", children: [
1385
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-disabled-title", children: t(`disabled.title`) }),
1386
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: tenantConfig.booking.disableBookingMessage ?? t(`disabled.title`) })
1387
+ ] });
1388
+ }
1389
+ function LegalGate() {
1390
+ const { t, dispatch } = useBookingContext();
1391
+ const { state } = useBooking();
1392
+ const { tenantConfig } = useTenantConfig();
1393
+ const [consents, setConsents] = react.useState({});
1394
+ const [activeModal, setActiveModal] = react.useState(null);
1395
+ if (state.step !== `legal` || !tenantConfig) {
1396
+ return null;
1397
+ }
1398
+ const { legal } = tenantConfig;
1399
+ function toggle(key) {
1400
+ setConsents((current) => ({ ...current, [key]: !current[key] }));
1401
+ }
1402
+ function handleContinue() {
1403
+ dispatch({ type: `set_consents`, consents });
1404
+ }
1405
+ const modalContent = activeModal === `cookie` ? legal.cookieNoticeContent : activeModal === `terms` ? legal.termsContent : activeModal === `privacy` ? legal.privacyContent : null;
1406
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-legal", children: [
1407
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-legal", children: t(`steps.legal`) }),
1408
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-legal-list", children: [
1409
+ legal.displayCookieNotice ? /* @__PURE__ */ jsxRuntime.jsxs(Field, { label: "", children: [
1410
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ea-checkbox", children: [
1411
+ /* @__PURE__ */ jsxRuntime.jsx(
1412
+ "input",
1413
+ {
1414
+ type: "checkbox",
1415
+ checked: consents.cookie === true,
1416
+ onChange: () => toggle(`cookie`)
1417
+ }
1418
+ ),
1419
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t(`legal.acceptCookie`) })
1420
+ ] }),
1421
+ legal.cookieNoticeContent ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ea-link", onClick: () => setActiveModal(`cookie`), children: t(`legal.readMore`) }) : null
1422
+ ] }) : null,
1423
+ legal.displayTerms ? /* @__PURE__ */ jsxRuntime.jsxs(Field, { label: "", children: [
1424
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ea-checkbox", children: [
1425
+ /* @__PURE__ */ jsxRuntime.jsx(
1426
+ "input",
1427
+ {
1428
+ type: "checkbox",
1429
+ checked: consents.terms === true,
1430
+ onChange: () => toggle(`terms`)
1431
+ }
1432
+ ),
1433
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t(`legal.acceptTerms`) })
1434
+ ] }),
1435
+ legal.termsContent ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ea-link", onClick: () => setActiveModal(`terms`), children: t(`legal.readMore`) }) : null
1436
+ ] }) : null,
1437
+ legal.displayPrivacy ? /* @__PURE__ */ jsxRuntime.jsxs(Field, { label: "", children: [
1438
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ea-checkbox", children: [
1439
+ /* @__PURE__ */ jsxRuntime.jsx(
1440
+ "input",
1441
+ {
1442
+ type: "checkbox",
1443
+ checked: consents.privacy === true,
1444
+ onChange: () => toggle(`privacy`)
1445
+ }
1446
+ ),
1447
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t(`legal.acceptPrivacy`) })
1448
+ ] }),
1449
+ legal.privacyContent ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ea-link", onClick: () => setActiveModal(`privacy`), children: t(`legal.readMore`) }) : null
1450
+ ] }) : null
1451
+ ] }),
1452
+ activeModal && modalContent ? /* @__PURE__ */ jsxRuntime.jsxs("dialog", { open: true, className: "ea-modal", children: [
1453
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ea-modal-body", children: modalContent }),
1454
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => setActiveModal(null), children: t(`actions.back`) })
1455
+ ] }) : null,
1456
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-step-actions", children: [
1457
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) }),
1458
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: handleContinue, children: t(`actions.continue`) })
1459
+ ] })
1460
+ ] });
1461
+ }
1462
+ function ProviderPicker() {
1463
+ const { t, dispatch } = useBookingContext();
1464
+ const { state } = useBooking();
1465
+ const { tenantConfig } = useTenantConfig();
1466
+ const serviceId = state.step === `provider` ? state.serviceId : void 0;
1467
+ const { providers, loading, error } = useProviders(serviceId);
1468
+ if (state.step !== `provider`) {
1469
+ return null;
1470
+ }
1471
+ const showAny = tenantConfig?.booking.displayAnyProvider ?? false;
1472
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-provider", children: [
1473
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-provider", children: t(`steps.provider`) }),
1474
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "\u2026" }) : null,
1475
+ error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { message: error }) : null,
1476
+ /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "ea-card-list", children: [
1477
+ showAny ? /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
1478
+ "button",
1479
+ {
1480
+ type: "button",
1481
+ className: "ea-card",
1482
+ onClick: () => dispatch({ type: `select_provider`, providerId: ANY_PROVIDER_ID }),
1483
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-card-title", children: t(`provider.any`) })
1484
+ }
1485
+ ) }) : null,
1486
+ providers.map((provider) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
1487
+ "button",
1488
+ {
1489
+ type: "button",
1490
+ className: "ea-card",
1491
+ onClick: () => dispatch({ type: `select_provider`, providerId: provider.id }),
1492
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-card-title", children: provider.displayName })
1493
+ }
1494
+ ) }, provider.id))
1495
+ ] }),
1496
+ !loading && providers.length === 0 && !showAny ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: t(`provider.empty`) }) : null,
1497
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) })
1498
+ ] });
1499
+ }
1500
+ function ServicePicker() {
1501
+ const { t, dispatch } = useBookingContext();
1502
+ const { state } = useBooking();
1503
+ const { services, loading, error } = useServices();
1504
+ if (state.step !== `service`) {
1505
+ return null;
1506
+ }
1507
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-service", children: [
1508
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-service", children: t(`steps.service`) }),
1509
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "\u2026" }) : null,
1510
+ error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { message: error }) : null,
1511
+ !loading && !error && services.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: t(`service.empty`) }) : null,
1512
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "ea-card-list", children: services.map((service) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1513
+ "button",
1514
+ {
1515
+ type: "button",
1516
+ className: "ea-card",
1517
+ onClick: () => dispatch({ type: `select_service`, serviceId: service.id }),
1518
+ children: [
1519
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-card-title", children: service.name }),
1520
+ service.description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-card-desc", children: service.description }) : null,
1521
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ea-card-meta", children: t(`service.durationMinutes`, { minutes: service.durationMinutes }) })
1522
+ ]
1523
+ }
1524
+ ) }, service.id)) }),
1525
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", disabled: true, children: t(`actions.back`) })
1526
+ ] });
1527
+ }
1528
+ function useAvailability(params) {
1529
+ const { client } = useBookingContext();
1530
+ const [slots, setSlots] = react.useState([]);
1531
+ const [loading, setLoading] = react.useState(false);
1532
+ const [error, setError] = react.useState(null);
1533
+ const reload = react.useCallback(async () => {
1534
+ if (!params.serviceId || !params.date) {
1535
+ setSlots([]);
1536
+ return;
1537
+ }
1538
+ setLoading(true);
1539
+ setError(null);
1540
+ try {
1541
+ if (params.providerId && params.providerId !== ANY_PROVIDER_ID) {
1542
+ const data = await client.getAvailability({
1543
+ providerId: params.providerId,
1544
+ serviceId: params.serviceId,
1545
+ date: params.date
1546
+ });
1547
+ setSlots(data);
1548
+ return;
1549
+ }
1550
+ const ids = params.providerIdsForAny ?? [];
1551
+ if (ids.length === 0) {
1552
+ setSlots([]);
1553
+ return;
1554
+ }
1555
+ const results = await Promise.all(
1556
+ ids.map(
1557
+ (providerId) => client.getAvailability({
1558
+ providerId,
1559
+ serviceId: params.serviceId,
1560
+ date: params.date
1561
+ })
1562
+ )
1563
+ );
1564
+ const merged = /* @__PURE__ */ new Map();
1565
+ for (const list of results) {
1566
+ for (const slot of list) {
1567
+ merged.set(slot.startAt, slot);
1568
+ }
1569
+ }
1570
+ setSlots(
1571
+ [...merged.values()].sort((a, b) => a.startAt.localeCompare(b.startAt))
1572
+ );
1573
+ } catch (err) {
1574
+ setError(err instanceof Error ? err.message : `Failed to load availability`);
1575
+ } finally {
1576
+ setLoading(false);
1577
+ }
1578
+ }, [client, params.date, params.providerId, params.providerIdsForAny, params.serviceId]);
1579
+ react.useEffect(() => {
1580
+ void reload();
1581
+ }, [reload]);
1582
+ return { slots, loading, error, reload };
1583
+ }
1584
+ function TimeSlotPicker() {
1585
+ const { t, dispatch } = useBookingContext();
1586
+ const { state } = useBooking();
1587
+ const serviceId = state.step === `time` ? state.serviceId : void 0;
1588
+ const providerId = state.step === `time` ? state.providerId : void 0;
1589
+ const date = state.step === `time` ? state.date : void 0;
1590
+ const { providers } = useProviders(serviceId);
1591
+ const providerIdsForAny = react.useMemo(() => providers.map((p) => p.id), [providers]);
1592
+ const {
1593
+ businessTimezone,
1594
+ customerTimezone,
1595
+ displayTimezone,
1596
+ secondaryTimezone,
1597
+ timeDisplayMode,
1598
+ bookingTimeDisplay
1599
+ } = useBookingTimezones(providerId, serviceId);
1600
+ const { slots, loading, error } = useAvailability({
1601
+ serviceId,
1602
+ providerId,
1603
+ date,
1604
+ providerIdsForAny
1605
+ });
1606
+ const [selectedStartAt, setSelectedStartAt] = react.useState(null);
1607
+ react.useEffect(() => {
1608
+ if (slots.length > 0) {
1609
+ setSelectedStartAt(slots[0].startAt);
1610
+ } else {
1611
+ setSelectedStartAt(null);
1612
+ }
1613
+ }, [slots]);
1614
+ if (state.step !== `time`) {
1615
+ return null;
1616
+ }
1617
+ const selected = slots.find((s) => s.startAt === selectedStartAt) ?? null;
1618
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-time", children: [
1619
+ /* @__PURE__ */ jsxRuntime.jsx(StepTitle, { id: "ea-step-time", children: t(`steps.time`) }),
1620
+ /* @__PURE__ */ jsxRuntime.jsx(
1621
+ TimezoneMismatchBanner,
1622
+ {
1623
+ businessTimezone,
1624
+ customerTimezone,
1625
+ timeDisplayMode,
1626
+ bookingTimeDisplay
1627
+ }
1628
+ ),
1629
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: t(`time.timezoneHint`, { timezone: displayTimezone }) }),
1630
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "\u2026" }) : null,
1631
+ error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { message: error }) : null,
1632
+ !loading && slots.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ea-muted", children: t(`time.empty`) }) : null,
1633
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "ea-slot-list", children: slots.map((slot) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
1634
+ "button",
1635
+ {
1636
+ type: "button",
1637
+ className: slot.startAt === selectedStartAt ? `ea-slot ea-slot-selected` : `ea-slot`,
1638
+ onClick: () => setSelectedStartAt(slot.startAt),
1639
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1640
+ SlotTimeDisplay,
1641
+ {
1642
+ iso: slot.startAt,
1643
+ displayTimezone,
1644
+ secondaryTimezone,
1645
+ customerTimezone
1646
+ }
1647
+ )
1648
+ }
1649
+ ) }, slot.startAt)) }),
1650
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-step-actions", children: [
1651
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) }),
1652
+ /* @__PURE__ */ jsxRuntime.jsx(
1653
+ Button,
1654
+ {
1655
+ disabled: !selected,
1656
+ onClick: () => selected && dispatch({ type: `select_slot`, slot: selected }),
1657
+ children: t(`actions.continue`)
1658
+ }
1659
+ )
1660
+ ] })
1661
+ ] });
1662
+ }
1663
+ function BookingWidget() {
1664
+ const { tenantConfig, tenantConfigLoading, tenantConfigError } = useBookingContext();
1665
+ if (tenantConfigLoading) {
1666
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "\u2026" }) });
1667
+ }
1668
+ if (tenantConfigError) {
1669
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { message: tenantConfigError }) });
1670
+ }
1671
+ if (tenantConfig?.booking.disableBooking) {
1672
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsxRuntime.jsx(DisabledBooking, {}) });
1673
+ }
1674
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ea-booking-flow", children: [
1675
+ /* @__PURE__ */ jsxRuntime.jsx(ServicePicker, {}),
1676
+ /* @__PURE__ */ jsxRuntime.jsx(ProviderPicker, {}),
1677
+ /* @__PURE__ */ jsxRuntime.jsx(DatePicker, {}),
1678
+ /* @__PURE__ */ jsxRuntime.jsx(TimeSlotPicker, {}),
1679
+ /* @__PURE__ */ jsxRuntime.jsx(CustomerForm, {}),
1680
+ /* @__PURE__ */ jsxRuntime.jsx(LegalGate, {}),
1681
+ /* @__PURE__ */ jsxRuntime.jsx(ConfirmStep, {}),
1682
+ /* @__PURE__ */ jsxRuntime.jsx(BookedSummary, {})
1683
+ ] }) });
1684
+ }
1685
+
1686
+ exports.ANY_PROVIDER_ID = ANY_PROVIDER_ID;
1687
+ exports.BookedSummary = BookedSummary;
1688
+ exports.BookingApiError = BookingApiError;
1689
+ exports.BookingProvider = BookingProvider;
1690
+ exports.BookingWidget = BookingWidget;
1691
+ exports.ConfirmStep = ConfirmStep;
1692
+ exports.CustomerForm = CustomerForm;
1693
+ exports.DatePicker = DatePicker;
1694
+ exports.DisableBookingMessage = DisabledBooking;
1695
+ exports.DisabledBooking = DisabledBooking;
1696
+ exports.LegalGate = LegalGate;
1697
+ exports.ProviderPicker = ProviderPicker;
1698
+ exports.ServicePicker = ServicePicker;
1699
+ exports.TimeSlotPicker = TimeSlotPicker;
1700
+ exports.allowsTimeDisplayToggle = allowsTimeDisplayToggle;
1701
+ exports.bookingReducer = bookingReducer;
1702
+ exports.createBookingClient = createBookingClient;
1703
+ exports.createInitialBookingState = createInitialBookingState;
1704
+ exports.createTranslator = createTranslator;
1705
+ exports.enCatalog = enCatalog;
1706
+ exports.formatCalendarDate = formatCalendarDate;
1707
+ exports.formatSlotDateTimeDual = formatSlotDateTimeDual;
1708
+ exports.getCustomerTimezone = getCustomerTimezone;
1709
+ exports.maxBookableDate = maxBookableDate;
1710
+ exports.minBookableDate = minBookableDate;
1711
+ exports.resolveBusinessTimezone = resolveBusinessTimezone;
1712
+ exports.resolveDisplayTimezone = resolveDisplayTimezone;
1713
+ exports.shouldShowSecondaryTimezone = shouldShowSecondaryTimezone;
1714
+ exports.useAvailability = useAvailability;
1715
+ exports.useBooking = useBooking;
1716
+ exports.useProviders = useProviders;
1717
+ exports.useServices = useServices;
1718
+ exports.useTenantConfig = useTenantConfig;
1719
+ exports.useUnavailableDates = useUnavailableDates;
1720
+ //# sourceMappingURL=index.cjs.map
1721
+ //# sourceMappingURL=index.cjs.map