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