@bigz-app/booking-widget 1.4.4 → 1.5.1

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.
Files changed (47) hide show
  1. package/dist/booking-widget.js +17856 -17103
  2. package/dist/booking-widget.js.map +1 -1
  3. package/dist/components/UniversalBookingWidget.d.ts +30 -4
  4. package/dist/components/UniversalBookingWidget.d.ts.map +1 -1
  5. package/dist/components/booking/BookingForm.d.ts +1 -3
  6. package/dist/components/booking/BookingForm.d.ts.map +1 -1
  7. package/dist/components/booking/GiftCardOnlyBooking.d.ts.map +1 -1
  8. package/dist/components/booking/HoldCountdown.d.ts +10 -0
  9. package/dist/components/booking/HoldCountdown.d.ts.map +1 -0
  10. package/dist/components/booking/MolliePaymentForm.d.ts +11 -1
  11. package/dist/components/booking/MolliePaymentForm.d.ts.map +1 -1
  12. package/dist/components/booking/StripePaymentForm.d.ts +11 -2
  13. package/dist/components/booking/StripePaymentForm.d.ts.map +1 -1
  14. package/dist/components/events/EventInstanceSelection.d.ts +3 -1
  15. package/dist/components/events/EventInstanceSelection.d.ts.map +1 -1
  16. package/dist/components/events/EventTypeDetailsDialog.d.ts +0 -1
  17. package/dist/components/events/EventTypeDetailsDialog.d.ts.map +1 -1
  18. package/dist/components/events/EventTypeSelection.d.ts +16 -1
  19. package/dist/components/events/EventTypeSelection.d.ts.map +1 -1
  20. package/dist/components/events/FreeformSelection.d.ts +49 -0
  21. package/dist/components/events/FreeformSelection.d.ts.map +1 -0
  22. package/dist/components/events/WaitlistDialog.d.ts +10 -0
  23. package/dist/components/events/WaitlistDialog.d.ts.map +1 -0
  24. package/dist/components/events/index.d.ts +1 -0
  25. package/dist/components/events/index.d.ts.map +1 -1
  26. package/dist/components/voucher/VoucherIntegration.d.ts +3 -2
  27. package/dist/components/voucher/VoucherIntegration.d.ts.map +1 -1
  28. package/dist/components/voucher/VoucherPurchaseForm.d.ts +8 -3
  29. package/dist/components/voucher/VoucherPurchaseForm.d.ts.map +1 -1
  30. package/dist/components/voucher/index.d.ts +1 -1
  31. package/dist/components/voucher/index.d.ts.map +1 -1
  32. package/dist/components/voucher/useVoucherConfig.d.ts +6 -1
  33. package/dist/components/voucher/useVoucherConfig.d.ts.map +1 -1
  34. package/dist/i18n/locales/de.d.ts.map +1 -1
  35. package/dist/i18n/locales/en.d.ts.map +1 -1
  36. package/dist/i18n/locales/es.d.ts.map +1 -1
  37. package/dist/i18n/locales/pt.d.ts.map +1 -1
  38. package/dist/i18n/locales/sv.d.ts.map +1 -1
  39. package/dist/index.cjs +1235 -482
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.esm.js +1228 -475
  42. package/dist/index.esm.js.map +1 -1
  43. package/dist/utils/analytics.d.ts +3 -1
  44. package/dist/utils/analytics.d.ts.map +1 -1
  45. package/dist/utils.d.ts.map +1 -1
  46. package/dist/validation/booking-schema.d.ts +4 -4
  47. package/package.json +1 -1
package/dist/index.esm.js CHANGED
@@ -4,170 +4,6 @@ import { createRoot } from 'react-dom/client';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import ReactDOM, { createPortal } from 'react-dom';
6
6
 
7
- var V3_URL = 'https://js.stripe.com/v3';
8
- var V3_URL_REGEX = /^https:\/\/js\.stripe\.com\/v3\/?(\?.*)?$/;
9
- var EXISTING_SCRIPT_MESSAGE = 'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used';
10
- var findScript = function findScript() {
11
- var scripts = document.querySelectorAll("script[src^=\"".concat(V3_URL, "\"]"));
12
-
13
- for (var i = 0; i < scripts.length; i++) {
14
- var script = scripts[i];
15
-
16
- if (!V3_URL_REGEX.test(script.src)) {
17
- continue;
18
- }
19
-
20
- return script;
21
- }
22
-
23
- return null;
24
- };
25
-
26
- var injectScript = function injectScript(params) {
27
- var queryString = '';
28
- var script = document.createElement('script');
29
- script.src = "".concat(V3_URL).concat(queryString);
30
- var headOrBody = document.head || document.body;
31
-
32
- if (!headOrBody) {
33
- throw new Error('Expected document.body not to be null. Stripe.js requires a <body> element.');
34
- }
35
-
36
- headOrBody.appendChild(script);
37
- return script;
38
- };
39
-
40
- var registerWrapper = function registerWrapper(stripe, startTime) {
41
- if (!stripe || !stripe._registerWrapper) {
42
- return;
43
- }
44
-
45
- stripe._registerWrapper({
46
- name: 'stripe-js',
47
- version: "4.6.0",
48
- startTime: startTime
49
- });
50
- };
51
-
52
- var stripePromise = null;
53
- var onErrorListener = null;
54
- var onLoadListener = null;
55
-
56
- var onError = function onError(reject) {
57
- return function () {
58
- reject(new Error('Failed to load Stripe.js'));
59
- };
60
- };
61
-
62
- var onLoad = function onLoad(resolve, reject) {
63
- return function () {
64
- if (window.Stripe) {
65
- resolve(window.Stripe);
66
- } else {
67
- reject(new Error('Stripe.js not available'));
68
- }
69
- };
70
- };
71
-
72
- var loadScript = function loadScript(params) {
73
- // Ensure that we only attempt to load Stripe.js at most once
74
- if (stripePromise !== null) {
75
- return stripePromise;
76
- }
77
-
78
- stripePromise = new Promise(function (resolve, reject) {
79
- if (typeof window === 'undefined' || typeof document === 'undefined') {
80
- // Resolve to null when imported server side. This makes the module
81
- // safe to import in an isomorphic code base.
82
- resolve(null);
83
- return;
84
- }
85
-
86
- if (window.Stripe) {
87
- resolve(window.Stripe);
88
- return;
89
- }
90
-
91
- try {
92
- var script = findScript();
93
-
94
- if (script && params) ; else if (!script) {
95
- script = injectScript(params);
96
- } else if (script && onLoadListener !== null && onErrorListener !== null) {
97
- var _script$parentNode;
98
-
99
- // remove event listeners
100
- script.removeEventListener('load', onLoadListener);
101
- script.removeEventListener('error', onErrorListener); // if script exists, but we are reloading due to an error,
102
- // reload script to trigger 'load' event
103
-
104
- (_script$parentNode = script.parentNode) === null || _script$parentNode === void 0 ? void 0 : _script$parentNode.removeChild(script);
105
- script = injectScript(params);
106
- }
107
-
108
- onLoadListener = onLoad(resolve, reject);
109
- onErrorListener = onError(reject);
110
- script.addEventListener('load', onLoadListener);
111
- script.addEventListener('error', onErrorListener);
112
- } catch (error) {
113
- reject(error);
114
- return;
115
- }
116
- }); // Resets stripePromise on error
117
-
118
- return stripePromise["catch"](function (error) {
119
- stripePromise = null;
120
- return Promise.reject(error);
121
- });
122
- };
123
- var initStripe = function initStripe(maybeStripe, args, startTime) {
124
- if (maybeStripe === null) {
125
- return null;
126
- }
127
-
128
- var stripe = maybeStripe.apply(undefined, args);
129
- registerWrapper(stripe, startTime);
130
- return stripe;
131
- }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
132
-
133
- var stripePromise$1;
134
- var loadCalled = false;
135
-
136
- var getStripePromise = function getStripePromise() {
137
- if (stripePromise$1) {
138
- return stripePromise$1;
139
- }
140
-
141
- stripePromise$1 = loadScript(null)["catch"](function (error) {
142
- // clear cache on error
143
- stripePromise$1 = null;
144
- return Promise.reject(error);
145
- });
146
- return stripePromise$1;
147
- }; // Execute our own script injection after a tick to give users time to do their
148
- // own script injection.
149
-
150
-
151
- Promise.resolve().then(function () {
152
- return getStripePromise();
153
- })["catch"](function (error) {
154
- if (!loadCalled) {
155
- console.warn(error);
156
- }
157
- });
158
- var loadStripe = function loadStripe() {
159
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
160
- args[_key] = arguments[_key];
161
- }
162
-
163
- loadCalled = true;
164
- var startTime = Date.now(); // if previous attempts are unsuccessful, will re-load script
165
-
166
- return getStripePromise().then(function (maybeStripe) {
167
- return initStripe(maybeStripe, args, startTime);
168
- });
169
- };
170
-
171
7
  const de$1 = {
172
8
  // Common
173
9
  "common.back": "← Zurück",
@@ -247,6 +83,24 @@ const de$1 = {
247
83
  "instances.bestPrice": "bester Preis !!!",
248
84
  "instances.goodPrice": "günstiger Preis",
249
85
  "instances.oclock": "Uhr",
86
+ // Warteliste
87
+ "waitlist.joinCta": "Auf die Warteliste",
88
+ "waitlist.title": "Auf die Warteliste setzen",
89
+ "waitlist.subtitle": "{{name}} ist ausgebucht. Hinterlassen Sie Ihre Daten und wir melden uns, falls ein Platz frei wird.",
90
+ "waitlist.name": "Name",
91
+ "waitlist.email": "E-Mail",
92
+ "waitlist.phone": "Telefon (optional)",
93
+ "waitlist.requiredFields": "Bitte geben Sie Ihren Namen und Ihre E-Mail-Adresse ein.",
94
+ "waitlist.join": "Eintragen",
95
+ "waitlist.submitting": "Wird eingetragen...",
96
+ "waitlist.cancel": "Abbrechen",
97
+ "waitlist.submitError": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.",
98
+ "waitlist.successTitle": "Sie sind auf der Liste!",
99
+ "waitlist.successMessage": "Sie sind Nummer {{position}} auf der Warteliste. Wir melden uns, sobald ein Platz frei wird.",
100
+ "waitlist.done": "Fertig",
101
+ // Reservierungs-Countdown
102
+ "hold.reservedFor": "Platz reserviert für {{time}}",
103
+ "hold.expired": "Ihre Reservierung ist abgelaufen",
250
104
  // Next events preview
251
105
  "nextEvents.title": "Nächste verfügbare Termine",
252
106
  "nextEvents.subtitle": "Wähle einen Termin aus oder zeige alle verfügbaren Termine an",
@@ -295,6 +149,10 @@ const de$1 = {
295
149
  "booking.stepUpsells": "Extras",
296
150
  "booking.stepDetails": "Angaben",
297
151
  "booking.stepPayment": "Zahlung",
152
+ "booking.steps": "{{count}} Schritt(e)",
153
+ "booking.freeformUnitsAvailable": "{{count}} Einheiten verfuegbar",
154
+ "booking.freeformNotAvailable": "Das gewaehlte Zeitfenster ist nicht verfuegbar",
155
+ "booking.freeformMissingConfig": "Die Freeform-Konfiguration fuer diesen Event-Typ fehlt.",
298
156
  // Booking summary
299
157
  "summary.title": "Buchungszusammenfassung",
300
158
  "summary.subtotal": "Zwischensumme:",
@@ -350,6 +208,13 @@ const de$1 = {
350
208
  "voucher.monetaryTypeDesc": "Beliebigen Betrag wählen",
351
209
  "voucher.eventType": "Erlebnisgutschein",
352
210
  "voucher.eventTypeDesc": "Für ein bestimmtes Erlebnis",
211
+ "voucher.categoryType": "Kategoriegutschein",
212
+ "voucher.categoryTypeDesc": "Für ein beliebiges Erlebnis einer Kategorie",
213
+ "voucher.categoryVoucher": "Kategoriegutschein",
214
+ "voucher.selectCategory": "Kategorie wählen",
215
+ "voucher.categoryLabel": "Kategorie",
216
+ "voucher.selectCategoryPlaceholder": "Kategorie auswählen...",
217
+ "voucher.categoryAmount": "Gutscheinwert",
353
218
  "voucher.selectAmount": "Betrag wählen",
354
219
  "voucher.customAmount": "Oder eigenen Betrag eingeben",
355
220
  "voucher.amountRange": "Betrag muss zwischen {{min}} und {{max}} liegen",
@@ -537,6 +402,24 @@ const en = {
537
402
  "instances.bestPrice": "best price !!!",
538
403
  "instances.goodPrice": "good price",
539
404
  "instances.oclock": "",
405
+ // Waitlist
406
+ "waitlist.joinCta": "Join waitlist",
407
+ "waitlist.title": "Join the waitlist",
408
+ "waitlist.subtitle": "{{name}} is fully booked. Leave your details and we'll contact you if a spot opens up.",
409
+ "waitlist.name": "Name",
410
+ "waitlist.email": "Email",
411
+ "waitlist.phone": "Phone (optional)",
412
+ "waitlist.requiredFields": "Please enter your name and email.",
413
+ "waitlist.join": "Join waitlist",
414
+ "waitlist.submitting": "Joining...",
415
+ "waitlist.cancel": "Cancel",
416
+ "waitlist.submitError": "Something went wrong. Please try again.",
417
+ "waitlist.successTitle": "You're on the list!",
418
+ "waitlist.successMessage": "You are number {{position}} on the waitlist. We'll be in touch if a spot becomes available.",
419
+ "waitlist.done": "Done",
420
+ // Slot hold countdown
421
+ "hold.reservedFor": "Slot reserved for {{time}}",
422
+ "hold.expired": "Your reservation has expired",
540
423
  // Next events preview
541
424
  "nextEvents.title": "Next available dates",
542
425
  "nextEvents.subtitle": "Select a date or view all available dates",
@@ -585,6 +468,10 @@ const en = {
585
468
  "booking.stepUpsells": "Extras",
586
469
  "booking.stepDetails": "Details",
587
470
  "booking.stepPayment": "Payment",
471
+ "booking.steps": "{{count}} step(s)",
472
+ "booking.freeformUnitsAvailable": "{{count}} units available",
473
+ "booking.freeformNotAvailable": "Selected window is not available",
474
+ "booking.freeformMissingConfig": "Missing freeform configuration for this event type.",
588
475
  // Booking summary
589
476
  "summary.title": "Booking Summary",
590
477
  "summary.subtotal": "Subtotal:",
@@ -640,6 +527,13 @@ const en = {
640
527
  "voucher.monetaryTypeDesc": "Choose any amount",
641
528
  "voucher.eventType": "Event Voucher",
642
529
  "voucher.eventTypeDesc": "For a specific experience",
530
+ "voucher.categoryType": "Category Voucher",
531
+ "voucher.categoryTypeDesc": "For any experience in a category",
532
+ "voucher.categoryVoucher": "Category Voucher",
533
+ "voucher.selectCategory": "Select Category",
534
+ "voucher.categoryLabel": "Category",
535
+ "voucher.selectCategoryPlaceholder": "Choose a category...",
536
+ "voucher.categoryAmount": "Voucher Value",
643
537
  "voucher.selectAmount": "Select Amount",
644
538
  "voucher.customAmount": "Or enter custom amount",
645
539
  "voucher.amountRange": "Amount must be between {{min}} and {{max}}",
@@ -930,6 +824,13 @@ const es = {
930
824
  "voucher.monetaryTypeDesc": "Elige cualquier cantidad",
931
825
  "voucher.eventType": "Vale de experiencia",
932
826
  "voucher.eventTypeDesc": "Para una experiencia específica",
827
+ "voucher.categoryType": "Vale de categoría",
828
+ "voucher.categoryTypeDesc": "Para cualquier experiencia de una categoría",
829
+ "voucher.categoryVoucher": "Vale de categoría",
830
+ "voucher.selectCategory": "Seleccionar categoría",
831
+ "voucher.categoryLabel": "Categoría",
832
+ "voucher.selectCategoryPlaceholder": "Elige una categoría...",
833
+ "voucher.categoryAmount": "Valor del vale",
933
834
  "voucher.selectAmount": "Selecciona el importe",
934
835
  "voucher.customAmount": "O introduce un importe personalizado",
935
836
  "voucher.amountRange": "El importe debe estar entre {{min}} y {{max}}",
@@ -1220,6 +1121,13 @@ const pt = {
1220
1121
  "voucher.monetaryTypeDesc": "Escolha qualquer quantia",
1221
1122
  "voucher.eventType": "Vale de experiência",
1222
1123
  "voucher.eventTypeDesc": "Para uma experiência específica",
1124
+ "voucher.categoryType": "Vale de categoria",
1125
+ "voucher.categoryTypeDesc": "Para qualquer experiência de uma categoria",
1126
+ "voucher.categoryVoucher": "Vale de categoria",
1127
+ "voucher.selectCategory": "Selecionar categoria",
1128
+ "voucher.categoryLabel": "Categoria",
1129
+ "voucher.selectCategoryPlaceholder": "Escolha uma categoria...",
1130
+ "voucher.categoryAmount": "Valor do vale",
1223
1131
  "voucher.selectAmount": "Selecione o valor",
1224
1132
  "voucher.customAmount": "Ou introduza um valor personalizado",
1225
1133
  "voucher.amountRange": "O valor deve estar entre {{min}} e {{max}}",
@@ -1510,6 +1418,13 @@ const sv = {
1510
1418
  "voucher.monetaryTypeDesc": "Välj valfritt belopp",
1511
1419
  "voucher.eventType": "Upplevelsebevis",
1512
1420
  "voucher.eventTypeDesc": "För en specifik upplevelse",
1421
+ "voucher.categoryType": "Kategoripresent",
1422
+ "voucher.categoryTypeDesc": "För alla upplevelser i en kategori",
1423
+ "voucher.categoryVoucher": "Kategoripresent",
1424
+ "voucher.selectCategory": "Välj kategori",
1425
+ "voucher.categoryLabel": "Kategori",
1426
+ "voucher.selectCategoryPlaceholder": "Välj en kategori...",
1427
+ "voucher.categoryAmount": "Presentkortsvärde",
1513
1428
  "voucher.selectAmount": "Välj belopp",
1514
1429
  "voucher.customAmount": "Eller ange eget belopp",
1515
1430
  "voucher.amountRange": "Beloppet måste vara mellan {{min}} och {{max}}",
@@ -2242,6 +2157,7 @@ const inferConfigFromUrl = (baseConfig) => {
2242
2157
  const categoryId = urlParams.get("categoryId") || urlParams.get("category");
2243
2158
  const eventTypeId = urlParams.get("eventTypeId") || urlParams.get("type");
2244
2159
  const eventTypeIds = urlParams.get("eventTypeIds") || urlParams.get("types");
2160
+ const partnerContractId = urlParams.get("partnerContractId") || urlParams.get("contractId");
2245
2161
  const voucherValue = urlParams.get("voucherValue");
2246
2162
  const voucherCategory = urlParams.get("voucherCategory");
2247
2163
  const voucherEventType = urlParams.get("voucherEventType");
@@ -2254,11 +2170,14 @@ const inferConfigFromUrl = (baseConfig) => {
2254
2170
  : ["0", "false", "no", "off"].includes(voucherCardIntegration.toLowerCase())
2255
2171
  ? false
2256
2172
  : undefined;
2257
- const omitSelectionKeys = ({ categoryId: _c, eventTypeId: _e, eventTypeIds: _es, eventInstanceId: _i, voucherValue: _vv, voucherCategory: _vc, voucherEventType: _ve, voucherImageId: _vi, ...rest }) => rest;
2173
+ const omitSelectionKeys = ({ categoryId: _c, eventTypeId: _e, eventTypeIds: _es, eventInstanceId: _i, voucherValue: _vv, voucherCategory: _vc, voucherEventType: _ve, voucherImageId: _vi, partnerContractId: _pc, ...rest }) => rest;
2258
2174
  if (eventInstanceId && !Number.isNaN(Number(eventInstanceId))) {
2259
2175
  return {
2260
2176
  ...omitSelectionKeys(baseConfig),
2261
2177
  eventInstanceId: Number(eventInstanceId),
2178
+ ...(partnerContractId
2179
+ ? { partnerContractId }
2180
+ : {}),
2262
2181
  ...(parsedVoucherCardIntegration !== undefined ? { voucherIntegration: parsedVoucherCardIntegration } : {}),
2263
2182
  };
2264
2183
  }
@@ -2266,6 +2185,9 @@ const inferConfigFromUrl = (baseConfig) => {
2266
2185
  return {
2267
2186
  ...omitSelectionKeys(baseConfig),
2268
2187
  categoryId: Number(categoryId),
2188
+ ...(partnerContractId
2189
+ ? { partnerContractId }
2190
+ : {}),
2269
2191
  ...(parsedVoucherCardIntegration !== undefined ? { voucherIntegration: parsedVoucherCardIntegration } : {}),
2270
2192
  };
2271
2193
  }
@@ -2273,6 +2195,9 @@ const inferConfigFromUrl = (baseConfig) => {
2273
2195
  return {
2274
2196
  ...omitSelectionKeys(baseConfig),
2275
2197
  eventTypeId: Number(eventTypeId),
2198
+ ...(partnerContractId
2199
+ ? { partnerContractId }
2200
+ : {}),
2276
2201
  ...(parsedVoucherCardIntegration !== undefined ? { voucherIntegration: parsedVoucherCardIntegration } : {}),
2277
2202
  };
2278
2203
  }
@@ -2285,6 +2210,9 @@ const inferConfigFromUrl = (baseConfig) => {
2285
2210
  return {
2286
2211
  ...omitSelectionKeys(baseConfig),
2287
2212
  eventTypeIds: typeIds,
2213
+ ...(partnerContractId
2214
+ ? { partnerContractId }
2215
+ : {}),
2288
2216
  ...(parsedVoucherCardIntegration !== undefined ? { voucherIntegration: parsedVoucherCardIntegration } : {}),
2289
2217
  };
2290
2218
  }
@@ -2301,6 +2229,9 @@ const inferConfigFromUrl = (baseConfig) => {
2301
2229
  if (parsedVoucherValues.length || parsedVoucherCategories.length || parsedVoucherEventTypes.length) {
2302
2230
  return {
2303
2231
  ...baseConfig,
2232
+ ...(partnerContractId
2233
+ ? { partnerContractId }
2234
+ : {}),
2304
2235
  ...(parsedVoucherCardIntegration !== undefined ? { voucherIntegration: parsedVoucherCardIntegration } : {}),
2305
2236
  ...(parsedVoucherValues.length > 0
2306
2237
  ? { voucherValue: parsedVoucherValues.length === 1 ? parsedVoucherValues[0] : parsedVoucherValues }
@@ -2318,6 +2249,9 @@ const inferConfigFromUrl = (baseConfig) => {
2318
2249
  }
2319
2250
  return {
2320
2251
  ...baseConfig,
2252
+ ...(partnerContractId
2253
+ ? { partnerContractId }
2254
+ : {}),
2321
2255
  ...(parsedVoucherCardIntegration !== undefined ? { voucherIntegration: parsedVoucherCardIntegration } : {}),
2322
2256
  };
2323
2257
  };
@@ -6330,6 +6264,7 @@ function GiftCardOnlyBooking({ config, eventDetails, formData, discountCode, gif
6330
6264
  customerPhone: formData.customerPhone?.trim(),
6331
6265
  comment: formData.comment?.trim(),
6332
6266
  paymentMethod: "gift_card",
6267
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6333
6268
  ...(upsellSelections.length > 0 && { upsellSelections }),
6334
6269
  };
6335
6270
  const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-gift-card-booking"), {
@@ -6460,7 +6395,7 @@ function loadMollieScript() {
6460
6395
  document.head.appendChild(script);
6461
6396
  });
6462
6397
  }
6463
- function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess: _onSuccess, onError, upsellSelections = [], mollieProfileId, mollieTestmode, }) {
6398
+ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess: _onSuccess, onError, upsellSelections = [], mollieProfileId, mollieTestmode, freeformSelection, }) {
6464
6399
  const t = useTranslations();
6465
6400
  const { locale } = useLocale();
6466
6401
  const [isLoading, setIsLoading] = useState(false);
@@ -6474,7 +6409,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6474
6409
  const cardFormRef = useRef(null);
6475
6410
  const cardContainerRef = useRef(null);
6476
6411
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
6477
- const isFullyCoveredByGiftCards = isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
6412
+ const isFullyCoveredByGiftCards = freeformSelection
6413
+ ? false
6414
+ : isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
6478
6415
  const isCreditCard = selectedMethod === "creditcard";
6479
6416
  useEffect(() => {
6480
6417
  if (isFullyCoveredByGiftCards)
@@ -6584,6 +6521,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6584
6521
  return (jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards, onSuccess: _onSuccess, onError: onError, upsellSelections: upsellSelections }));
6585
6522
  }
6586
6523
  const validateBeforePayment = () => {
6524
+ if (freeformSelection) {
6525
+ return null;
6526
+ }
6587
6527
  const participantCount = formData.participants.filter((p) => p.name?.trim()).length;
6588
6528
  const availableSpots = eventDetails.availableSpots || 0;
6589
6529
  if (participantCount > availableSpots) {
@@ -6595,20 +6535,41 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6595
6535
  return null;
6596
6536
  };
6597
6537
  const buildRequestData = (cardToken) => ({
6598
- eventInstanceId: config.eventInstanceId || eventDetails.id,
6599
- organizationId: config.organizationId,
6600
- amount: Math.round(totalAmount),
6601
- currency: "EUR",
6602
- participants: formData.participants.filter((p) => p.name?.trim()),
6603
- discountCode: discountCode?.code,
6604
- giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6605
- customerName: formData.customerName?.trim(),
6606
- customerEmail: formData.customerEmail?.trim(),
6607
- customerPhone: formData.customerPhone?.trim(),
6608
- comment: formData.comment?.trim(),
6609
- ...(cardToken && { cardToken }),
6610
- ...(selectedMethod && !cardToken && { method: selectedMethod }),
6611
- ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6538
+ ...(freeformSelection
6539
+ ? {
6540
+ eventTypeId: freeformSelection.eventTypeId,
6541
+ organizationId: config.organizationId,
6542
+ start: freeformSelection.start,
6543
+ end: freeformSelection.end,
6544
+ requestedUnits: freeformSelection.requestedUnits,
6545
+ amount: Math.round(totalAmount),
6546
+ currency: "EUR",
6547
+ customerName: freeformSelection.customerName.trim(),
6548
+ customerEmail: freeformSelection.customerEmail.trim(),
6549
+ customerPhone: freeformSelection.customerPhone?.trim(),
6550
+ comment: freeformSelection.comment?.trim(),
6551
+ locale,
6552
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6553
+ ...(cardToken && { cardToken }),
6554
+ ...(selectedMethod && !cardToken && { method: selectedMethod }),
6555
+ }
6556
+ : {
6557
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
6558
+ organizationId: config.organizationId,
6559
+ amount: Math.round(totalAmount),
6560
+ currency: "EUR",
6561
+ participants: formData.participants.filter((p) => p.name?.trim()),
6562
+ discountCode: discountCode?.code,
6563
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6564
+ customerName: formData.customerName?.trim(),
6565
+ customerEmail: formData.customerEmail?.trim(),
6566
+ customerPhone: formData.customerPhone?.trim(),
6567
+ comment: formData.comment?.trim(),
6568
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6569
+ ...(cardToken && { cardToken }),
6570
+ ...(selectedMethod && !cardToken && { method: selectedMethod }),
6571
+ ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6572
+ }),
6612
6573
  });
6613
6574
  const handleCardPayment = async () => {
6614
6575
  if (!mollieRef.current)
@@ -6626,7 +6587,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6626
6587
  setPaymentError(tokenError?.message || t("error.createPayment"));
6627
6588
  return;
6628
6589
  }
6629
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-mollie-payment"), {
6590
+ const response = await fetch(getApiUrl(config.apiBaseUrl, freeformSelection
6591
+ ? "/booking/create-freeform-mollie-payment"
6592
+ : "/booking/create-mollie-payment"), {
6630
6593
  method: "POST",
6631
6594
  headers: createApiHeaders(config, locale),
6632
6595
  body: JSON.stringify(createRequestBody(config, buildRequestData(token))),
@@ -6662,7 +6625,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6662
6625
  setPaymentError(validationError);
6663
6626
  return;
6664
6627
  }
6665
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-mollie-payment"), {
6628
+ const response = await fetch(getApiUrl(config.apiBaseUrl, freeformSelection
6629
+ ? "/booking/create-freeform-mollie-payment"
6630
+ : "/booking/create-mollie-payment"), {
6666
6631
  method: "POST",
6667
6632
  headers: createApiHeaders(config, locale),
6668
6633
  body: JSON.stringify(createRequestBody(config, buildRequestData())),
@@ -6690,7 +6655,7 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6690
6655
  ? t("button.processingPayment")
6691
6656
  : selectedMethodData
6692
6657
  ? t("payment.payWithMethod", { method: selectedMethodData.description })
6693
- : totalAmount < eventDetails.price * formData.participants.filter((p) => p.name?.trim()).length
6658
+ : !freeformSelection && totalAmount < eventDetails.price * formData.participants.filter((p) => p.name?.trim()).length
6694
6659
  ? t("button.depositAndBook")
6695
6660
  : t("button.bookNow");
6696
6661
  const isPayDisabled = isLoading || !selectedMethod || (isCreditCard && !isMollieReady);
@@ -6832,73 +6797,257 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6832
6797
  }, children: isLoading ? (jsxs(Fragment, { children: [spinner("var(--bw-surface-color)"), " ", buttonLabel] })) : (buttonLabel) }))] }));
6833
6798
  }
6834
6799
 
6835
- // Inner component that uses the Stripe hooks
6836
- function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onError, }) {
6837
- const t = useTranslations();
6838
- const participantCount = formData.participants.filter((p) => p.name.trim()).length;
6839
- const fullAmount = eventDetails.price * participantCount;
6840
- const submitLabel = totalAmount < fullAmount ? t("button.depositAndBook") : t("button.bookNow");
6841
- return (jsx(StripeElementsPaymentForm, { onValidate: () => {
6842
- const availableSpots = eventDetails.availableSpots || 0;
6843
- if (participantCount > availableSpots) {
6844
- return t("payment.tooManyParticipants", { count: participantCount, available: availableSpots });
6845
- }
6846
- if (participantCount === 0) {
6847
- return t("payment.needOneParticipant");
6848
- }
6849
- return null;
6850
- }, onSuccess: (paymentIntent) => onSuccess({
6851
- paymentIntent,
6852
- }), onError: onError, paymentElementOptions: {
6853
- layout: "accordion",
6854
- defaultValues: {
6855
- billingDetails: {
6856
- name: formData.customerName,
6857
- email: formData.customerEmail,
6858
- phone: formData.customerPhone,
6859
- },
6860
- },
6861
- }, submitContent: jsx(Fragment, { children: submitLabel }), loadingContent: jsxs(Fragment, { children: [spinner("var(--bw-surface-color)"), " ", t("button.processingPayment")] }) }));
6862
- }
6863
- function StripePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripePromise, stripeAppearance, upsellSelections = [], }) {
6864
- const t = useTranslations();
6865
- const { locale } = useLocale();
6866
- const [clientSecret, setClientSecret] = useState(null);
6867
- const [paymentIntentId, setPaymentIntentId] = useState(null);
6868
- const [isCreatingPaymentIntent, setIsCreatingPaymentIntent] = useState(false);
6869
- const [paymentError, setPaymentError] = useState(null);
6870
- const storageKey = typeof window !== "undefined"
6871
- ? `bw_pi_${config?.organizationId}_${config?.eventInstanceId || eventDetails?.id}`
6872
- : "";
6873
- const PAYMENT_INTENT_TTL = 24 * 60 * 60 * 1000;
6874
- function loadPersistedPaymentIntent() {
6875
- if (typeof window === "undefined" || !storageKey)
6876
- return null;
6877
- try {
6878
- const raw = window.localStorage.getItem(storageKey);
6879
- if (!raw)
6880
- return null;
6881
- const parsed = JSON.parse(raw);
6882
- if (!parsed?.id || !parsed?.ts)
6883
- return null;
6884
- if (Date.now() - parsed.ts > PAYMENT_INTENT_TTL) {
6885
- window.localStorage.removeItem(storageKey);
6886
- return null;
6887
- }
6888
- return parsed.id;
6889
- }
6890
- catch {
6891
- return null;
6892
- }
6893
- }
6894
- function persistPaymentIntent(id) {
6895
- if (typeof window === "undefined" || !storageKey || !id)
6896
- return;
6897
- try {
6898
- const payload = { id, ts: Date.now() };
6899
- window.localStorage.setItem(storageKey, JSON.stringify(payload));
6900
- }
6901
- catch { }
6800
+ var V3_URL = 'https://js.stripe.com/v3';
6801
+ var V3_URL_REGEX = /^https:\/\/js\.stripe\.com\/v3\/?(\?.*)?$/;
6802
+ var EXISTING_SCRIPT_MESSAGE = 'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used';
6803
+ var findScript = function findScript() {
6804
+ var scripts = document.querySelectorAll("script[src^=\"".concat(V3_URL, "\"]"));
6805
+
6806
+ for (var i = 0; i < scripts.length; i++) {
6807
+ var script = scripts[i];
6808
+
6809
+ if (!V3_URL_REGEX.test(script.src)) {
6810
+ continue;
6811
+ }
6812
+
6813
+ return script;
6814
+ }
6815
+
6816
+ return null;
6817
+ };
6818
+
6819
+ var injectScript = function injectScript(params) {
6820
+ var queryString = '';
6821
+ var script = document.createElement('script');
6822
+ script.src = "".concat(V3_URL).concat(queryString);
6823
+ var headOrBody = document.head || document.body;
6824
+
6825
+ if (!headOrBody) {
6826
+ throw new Error('Expected document.body not to be null. Stripe.js requires a <body> element.');
6827
+ }
6828
+
6829
+ headOrBody.appendChild(script);
6830
+ return script;
6831
+ };
6832
+
6833
+ var registerWrapper = function registerWrapper(stripe, startTime) {
6834
+ if (!stripe || !stripe._registerWrapper) {
6835
+ return;
6836
+ }
6837
+
6838
+ stripe._registerWrapper({
6839
+ name: 'stripe-js',
6840
+ version: "4.6.0",
6841
+ startTime: startTime
6842
+ });
6843
+ };
6844
+
6845
+ var stripePromise = null;
6846
+ var onErrorListener = null;
6847
+ var onLoadListener = null;
6848
+
6849
+ var onError = function onError(reject) {
6850
+ return function () {
6851
+ reject(new Error('Failed to load Stripe.js'));
6852
+ };
6853
+ };
6854
+
6855
+ var onLoad = function onLoad(resolve, reject) {
6856
+ return function () {
6857
+ if (window.Stripe) {
6858
+ resolve(window.Stripe);
6859
+ } else {
6860
+ reject(new Error('Stripe.js not available'));
6861
+ }
6862
+ };
6863
+ };
6864
+
6865
+ var loadScript = function loadScript(params) {
6866
+ // Ensure that we only attempt to load Stripe.js at most once
6867
+ if (stripePromise !== null) {
6868
+ return stripePromise;
6869
+ }
6870
+
6871
+ stripePromise = new Promise(function (resolve, reject) {
6872
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
6873
+ // Resolve to null when imported server side. This makes the module
6874
+ // safe to import in an isomorphic code base.
6875
+ resolve(null);
6876
+ return;
6877
+ }
6878
+
6879
+ if (window.Stripe) {
6880
+ resolve(window.Stripe);
6881
+ return;
6882
+ }
6883
+
6884
+ try {
6885
+ var script = findScript();
6886
+
6887
+ if (script && params) ; else if (!script) {
6888
+ script = injectScript(params);
6889
+ } else if (script && onLoadListener !== null && onErrorListener !== null) {
6890
+ var _script$parentNode;
6891
+
6892
+ // remove event listeners
6893
+ script.removeEventListener('load', onLoadListener);
6894
+ script.removeEventListener('error', onErrorListener); // if script exists, but we are reloading due to an error,
6895
+ // reload script to trigger 'load' event
6896
+
6897
+ (_script$parentNode = script.parentNode) === null || _script$parentNode === void 0 ? void 0 : _script$parentNode.removeChild(script);
6898
+ script = injectScript(params);
6899
+ }
6900
+
6901
+ onLoadListener = onLoad(resolve, reject);
6902
+ onErrorListener = onError(reject);
6903
+ script.addEventListener('load', onLoadListener);
6904
+ script.addEventListener('error', onErrorListener);
6905
+ } catch (error) {
6906
+ reject(error);
6907
+ return;
6908
+ }
6909
+ }); // Resets stripePromise on error
6910
+
6911
+ return stripePromise["catch"](function (error) {
6912
+ stripePromise = null;
6913
+ return Promise.reject(error);
6914
+ });
6915
+ };
6916
+ var initStripe = function initStripe(maybeStripe, args, startTime) {
6917
+ if (maybeStripe === null) {
6918
+ return null;
6919
+ }
6920
+
6921
+ var stripe = maybeStripe.apply(undefined, args);
6922
+ registerWrapper(stripe, startTime);
6923
+ return stripe;
6924
+ }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
6925
+
6926
+ var stripePromise$1;
6927
+ var loadCalled = false;
6928
+
6929
+ var getStripePromise = function getStripePromise() {
6930
+ if (stripePromise$1) {
6931
+ return stripePromise$1;
6932
+ }
6933
+
6934
+ stripePromise$1 = loadScript(null)["catch"](function (error) {
6935
+ // clear cache on error
6936
+ stripePromise$1 = null;
6937
+ return Promise.reject(error);
6938
+ });
6939
+ return stripePromise$1;
6940
+ }; // Execute our own script injection after a tick to give users time to do their
6941
+ // own script injection.
6942
+
6943
+
6944
+ Promise.resolve().then(function () {
6945
+ return getStripePromise();
6946
+ })["catch"](function (error) {
6947
+ if (!loadCalled) {
6948
+ console.warn(error);
6949
+ }
6950
+ });
6951
+ var loadStripe = function loadStripe() {
6952
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
6953
+ args[_key] = arguments[_key];
6954
+ }
6955
+
6956
+ loadCalled = true;
6957
+ var startTime = Date.now(); // if previous attempts are unsuccessful, will re-load script
6958
+
6959
+ return getStripePromise().then(function (maybeStripe) {
6960
+ return initStripe(maybeStripe, args, startTime);
6961
+ });
6962
+ };
6963
+
6964
+ // Inner component that uses the Stripe hooks
6965
+ function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onError, freeformSelection, }) {
6966
+ const t = useTranslations();
6967
+ const isFreeform = Boolean(freeformSelection);
6968
+ const participantCount = isFreeform
6969
+ ? 1
6970
+ : formData.participants.filter((p) => p.name.trim()).length;
6971
+ const fullAmount = isFreeform ? totalAmount : eventDetails.price * participantCount;
6972
+ const submitLabel = !isFreeform && totalAmount < fullAmount ? t("button.depositAndBook") : t("button.bookNow");
6973
+ return (jsx(StripeElementsPaymentForm, { onValidate: () => {
6974
+ if (isFreeform) {
6975
+ return null;
6976
+ }
6977
+ const availableSpots = eventDetails.availableSpots || 0;
6978
+ if (participantCount > availableSpots) {
6979
+ return t("payment.tooManyParticipants", { count: participantCount, available: availableSpots });
6980
+ }
6981
+ if (participantCount === 0) {
6982
+ return t("payment.needOneParticipant");
6983
+ }
6984
+ return null;
6985
+ }, onSuccess: (paymentIntent) => onSuccess({
6986
+ paymentIntent,
6987
+ }), onError: onError, paymentElementOptions: {
6988
+ layout: "accordion",
6989
+ defaultValues: {
6990
+ billingDetails: {
6991
+ name: formData.customerName,
6992
+ email: formData.customerEmail,
6993
+ phone: formData.customerPhone,
6994
+ },
6995
+ },
6996
+ }, submitContent: jsx(Fragment, { children: submitLabel }), loadingContent: jsxs(Fragment, { children: [spinner("var(--bw-surface-color)"), " ", t("button.processingPayment")] }) }));
6997
+ }
6998
+ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripeAppearance, upsellSelections = [], freeformSelection, }) {
6999
+ const t = useTranslations();
7000
+ const { locale } = useLocale();
7001
+ const [clientSecret, setClientSecret] = useState(null);
7002
+ const [paymentIntentId, setPaymentIntentId] = useState(null);
7003
+ const [isCreatingPaymentIntent, setIsCreatingPaymentIntent] = useState(false);
7004
+ const [paymentError, setPaymentError] = useState(null);
7005
+ const stripePromise = useMemo(() => {
7006
+ if (!systemConfig?.stripePublishableKey) {
7007
+ return null;
7008
+ }
7009
+ const stripeOptions = {
7010
+ locale: locale,
7011
+ };
7012
+ if (systemConfig.connectedAccountId) {
7013
+ stripeOptions.stripeAccount = systemConfig.connectedAccountId;
7014
+ }
7015
+ return loadStripe(systemConfig.stripePublishableKey, stripeOptions);
7016
+ }, [systemConfig?.stripePublishableKey, systemConfig?.connectedAccountId, locale]);
7017
+ const storageKey = typeof window !== "undefined"
7018
+ ? freeformSelection
7019
+ ? `bw_pi_freeform_${config?.organizationId}_${freeformSelection.eventTypeId}_${freeformSelection.start}_${freeformSelection.end}_${freeformSelection.requestedUnits}`
7020
+ : `bw_pi_${config?.organizationId}_${config?.eventInstanceId || eventDetails?.id}`
7021
+ : "";
7022
+ const PAYMENT_INTENT_TTL = 24 * 60 * 60 * 1000;
7023
+ function loadPersistedPaymentIntent() {
7024
+ if (typeof window === "undefined" || !storageKey)
7025
+ return null;
7026
+ try {
7027
+ const raw = window.localStorage.getItem(storageKey);
7028
+ if (!raw)
7029
+ return null;
7030
+ const parsed = JSON.parse(raw);
7031
+ if (!parsed?.id || !parsed?.ts)
7032
+ return null;
7033
+ if (Date.now() - parsed.ts > PAYMENT_INTENT_TTL) {
7034
+ window.localStorage.removeItem(storageKey);
7035
+ return null;
7036
+ }
7037
+ return parsed.id;
7038
+ }
7039
+ catch {
7040
+ return null;
7041
+ }
7042
+ }
7043
+ function persistPaymentIntent(id) {
7044
+ if (typeof window === "undefined" || !storageKey || !id)
7045
+ return;
7046
+ try {
7047
+ const payload = { id, ts: Date.now() };
7048
+ window.localStorage.setItem(storageKey, JSON.stringify(payload));
7049
+ }
7050
+ catch { }
6902
7051
  }
6903
7052
  function clearPersistedPaymentIntent() {
6904
7053
  if (typeof window === "undefined" || !storageKey)
@@ -6923,47 +7072,82 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
6923
7072
  }, [paymentIntentId]);
6924
7073
  useEffect(() => {
6925
7074
  const createStripePayment = async () => {
6926
- if (!systemConfig || !eventDetails || !formData.participants?.length) {
7075
+ if (!systemConfig || !eventDetails) {
7076
+ return;
7077
+ }
7078
+ if (freeformSelection) {
7079
+ if (!freeformSelection.customerEmail?.trim() ||
7080
+ !freeformSelection.customerName?.trim() ||
7081
+ !freeformSelection.start ||
7082
+ !freeformSelection.end) {
7083
+ return;
7084
+ }
7085
+ }
7086
+ else if (!formData.participants?.length) {
6927
7087
  return;
6928
7088
  }
6929
7089
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
6930
- if (participantCount === 0 || !formData.customerEmail?.trim() || !formData.customerName?.trim()) {
7090
+ if (!freeformSelection &&
7091
+ (participantCount === 0 || !formData.customerEmail?.trim() || !formData.customerName?.trim())) {
6931
7092
  return;
6932
7093
  }
6933
7094
  setIsCreatingPaymentIntent(true);
6934
7095
  setPaymentError(null);
6935
7096
  try {
6936
- const requestData = {
6937
- eventInstanceId: config.eventInstanceId || eventDetails.id,
6938
- organizationId: config.organizationId,
6939
- amount: Math.round(totalAmount),
6940
- currency: "eur",
6941
- participants: formData.participants.filter((p) => p.name?.trim()),
6942
- discountCode: discountCode?.code,
6943
- giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6944
- customerName: formData.customerName?.trim(),
6945
- customerEmail: formData.customerEmail?.trim(),
6946
- customerPhone: formData.customerPhone?.trim(),
6947
- comment: formData.comment?.trim(),
6948
- ...(paymentIntentId && { paymentIntentId }),
6949
- ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6950
- };
6951
- if (!requestData.eventInstanceId) {
6952
- throw new Error("Event instance ID is required");
6953
- }
7097
+ const endpoint = freeformSelection
7098
+ ? "/booking/create-freeform-stripe-payment"
7099
+ : "/booking/create-stripe-payment";
7100
+ const requestData = freeformSelection
7101
+ ? {
7102
+ eventTypeId: freeformSelection.eventTypeId,
7103
+ organizationId: config.organizationId,
7104
+ start: freeformSelection.start,
7105
+ end: freeformSelection.end,
7106
+ requestedUnits: freeformSelection.requestedUnits,
7107
+ amount: Math.round(totalAmount),
7108
+ currency: "eur",
7109
+ customerName: freeformSelection.customerName.trim(),
7110
+ customerEmail: freeformSelection.customerEmail.trim(),
7111
+ customerPhone: freeformSelection.customerPhone?.trim(),
7112
+ comment: freeformSelection.comment?.trim(),
7113
+ locale,
7114
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7115
+ ...(paymentIntentId && { paymentIntentId }),
7116
+ }
7117
+ : {
7118
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
7119
+ organizationId: config.organizationId,
7120
+ amount: Math.round(totalAmount),
7121
+ currency: "eur",
7122
+ participants: formData.participants.filter((p) => p.name?.trim()),
7123
+ discountCode: discountCode?.code,
7124
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
7125
+ customerName: formData.customerName?.trim(),
7126
+ customerEmail: formData.customerEmail?.trim(),
7127
+ customerPhone: formData.customerPhone?.trim(),
7128
+ comment: formData.comment?.trim(),
7129
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7130
+ ...(paymentIntentId && { paymentIntentId }),
7131
+ ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
7132
+ };
6954
7133
  if (!requestData.organizationId) {
6955
7134
  throw new Error("Organization ID is required");
6956
7135
  }
6957
- if (!requestData.amount || requestData.amount <= 0) {
7136
+ if (requestData.amount === undefined || requestData.amount < 0) {
6958
7137
  throw new Error("Valid amount is required");
6959
7138
  }
6960
- if (!requestData.participants || requestData.participants.length === 0) {
7139
+ if (!freeformSelection && "eventInstanceId" in requestData && !requestData.eventInstanceId) {
7140
+ throw new Error("Event instance ID is required");
7141
+ }
7142
+ if (!freeformSelection &&
7143
+ "participants" in requestData &&
7144
+ (!requestData.participants || requestData.participants.length === 0)) {
6961
7145
  throw new Error("At least one participant is required");
6962
7146
  }
6963
7147
  if (!requestData.customerEmail) {
6964
7148
  throw new Error("Customer email is required");
6965
7149
  }
6966
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-stripe-payment"), {
7150
+ const response = await fetch(getApiUrl(config.apiBaseUrl, endpoint), {
6967
7151
  method: "POST",
6968
7152
  headers: createApiHeaders(config, locale),
6969
7153
  body: JSON.stringify(createRequestBody(config, requestData)),
@@ -7008,13 +7192,17 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7008
7192
  giftCards,
7009
7193
  config,
7010
7194
  upsellSelections,
7195
+ freeformSelection,
7196
+ locale,
7011
7197
  ]);
7012
7198
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
7013
- const isFullyCoveredByGiftCards = isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
7199
+ const isFullyCoveredByGiftCards = freeformSelection
7200
+ ? false
7201
+ : isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
7014
7202
  if (isFullyCoveredByGiftCards && totalAmount <= 0) {
7015
7203
  return (jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards, onSuccess: onSuccess, onError: onError, upsellSelections: upsellSelections }));
7016
7204
  }
7017
- if (isCreatingPaymentIntent || !clientSecret) {
7205
+ if (isCreatingPaymentIntent || !clientSecret || !stripePromise) {
7018
7206
  return (jsxs("div", { style: {
7019
7207
  display: "flex",
7020
7208
  alignItems: "center",
@@ -7049,7 +7237,50 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7049
7237
  setPaymentIntentId(null);
7050
7238
  setClientSecret(null);
7051
7239
  onSuccess(result);
7052
- }, onError: onError }) }));
7240
+ }, onError: onError, ...(freeformSelection ? { freeformSelection } : {}) }) }));
7241
+ }
7242
+
7243
+ const HOLD_DURATION_SECONDS = 15 * 60;
7244
+ /**
7245
+ * Shows the remaining time on the 15-minute slot hold that the server places on
7246
+ * the pending order while the customer completes checkout. The timer is started
7247
+ * client-side when the payment step opens — matching the moment the pending
7248
+ * order (and its `holdExpiresAt`) is created server-side.
7249
+ */
7250
+ function HoldCountdown({ onExpire }) {
7251
+ const t = useTranslations();
7252
+ const [remaining, setRemaining] = useState(HOLD_DURATION_SECONDS);
7253
+ useEffect(() => {
7254
+ const startedAt = Date.now();
7255
+ const interval = setInterval(() => {
7256
+ const elapsed = Math.floor((Date.now() - startedAt) / 1000);
7257
+ const left = Math.max(0, HOLD_DURATION_SECONDS - elapsed);
7258
+ setRemaining(left);
7259
+ if (left <= 0) {
7260
+ clearInterval(interval);
7261
+ onExpire?.();
7262
+ }
7263
+ }, 1000);
7264
+ return () => clearInterval(interval);
7265
+ }, [onExpire]);
7266
+ const minutes = Math.floor(remaining / 60);
7267
+ const seconds = remaining % 60;
7268
+ const formatted = `${minutes}:${String(seconds).padStart(2, "0")}`;
7269
+ const isLow = remaining <= 60;
7270
+ return (jsxs("div", { style: {
7271
+ display: "flex",
7272
+ alignItems: "center",
7273
+ gap: "8px",
7274
+ padding: "10px 14px",
7275
+ borderRadius: "var(--bw-border-radius)",
7276
+ border: `1px solid ${isLow ? "var(--bw-error-color)" : "var(--bw-border-color)"}`,
7277
+ backgroundColor: "var(--bw-surface-color)",
7278
+ color: isLow ? "var(--bw-error-color)" : "var(--bw-text-color)",
7279
+ fontFamily: "var(--bw-font-family)",
7280
+ fontSize: "14px",
7281
+ }, children: [jsx(IconClock, {}), jsx("span", { children: remaining > 0
7282
+ ? t("hold.reservedFor", { time: formatted })
7283
+ : t("hold.expired") })] }));
7053
7284
  }
7054
7285
 
7055
7286
  const FLAG_EMOJIS = {
@@ -11463,6 +11694,11 @@ const buttonStyles = {
11463
11694
  color: "var(--bw-text-muted)",
11464
11695
  border: "1px solid var(--bw-border-color)",
11465
11696
  },
11697
+ // Size variants
11698
+ small: {
11699
+ padding: "8px 12px",
11700
+ fontSize: "13px",
11701
+ },
11466
11702
  // Full width modifier
11467
11703
  fullWidth: {
11468
11704
  width: "100%",
@@ -11535,6 +11771,12 @@ const sectionStyles = {
11535
11771
  // TEXT
11536
11772
  // ============================================
11537
11773
  const textStyles = {
11774
+ body: {
11775
+ fontSize: "14px",
11776
+ color: "var(--bw-text-color)",
11777
+ fontFamily: "var(--bw-font-family)",
11778
+ lineHeight: 1.5,
11779
+ },
11538
11780
  muted: {
11539
11781
  fontSize: "14px",
11540
11782
  color: "var(--bw-text-muted)",
@@ -11604,7 +11846,7 @@ const sectionHeaderStyles$1 = sectionStyles.header;
11604
11846
  const labelStyles$1 = formStyles.label;
11605
11847
  const inputStyles$1 = formStyles.input;
11606
11848
  const errorTextStyles$1 = formStyles.error;
11607
- function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError, isOpen, onClose, systemConfig, selectedUpsells = [], upsells = [], persistedState = null, onPersistedStateChange, }) {
11849
+ function BookingForm({ config, eventDetails, onSuccess, onError, isOpen, onClose, systemConfig, selectedUpsells = [], upsells = [], persistedState = null, onPersistedStateChange, }) {
11608
11850
  const t$1 = useTranslations();
11609
11851
  const { locale } = useLocale();
11610
11852
  const timezone = useTimezone();
@@ -11804,7 +12046,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11804
12046
  newTotal: appliedDiscountCode.newTotal,
11805
12047
  }
11806
12048
  : null;
11807
- const hasPaymentProvider = Boolean(stripePromise || systemConfig?.paymentProvider === "mollie");
12049
+ const hasPaymentProvider = systemConfig?.paymentProvider === "stripe" || systemConfig?.paymentProvider === "mollie";
11808
12050
  const handleVoucherValidated = useCallback((voucher, _error) => {
11809
12051
  if (voucher) {
11810
12052
  setAppliedVouchers((prev) => [...prev, voucher]);
@@ -12070,7 +12312,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
12070
12312
  ...inputStyles$1,
12071
12313
  resize: "vertical",
12072
12314
  minHeight: "80px",
12073
- } })] })] })] })), checkoutStep === "payment" && (jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "24px" }, children: [jsxs("div", { style: cardStyles$1, children: [jsx("h2", { style: { ...sectionHeaderStyles$1, marginBottom: "16px" }, children: t$1("summary.title") }), jsxs("div", { style: { marginTop: "10px", display: "flex", flexDirection: "column", gap: "12px" }, children: [jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: t$1("booking.price") }), jsxs("div", { style: {
12315
+ } })] })] })] })), checkoutStep === "payment" && (jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "24px" }, children: [jsx(HoldCountdown, {}), jsxs("div", { style: cardStyles$1, children: [jsx("h2", { style: { ...sectionHeaderStyles$1, marginBottom: "16px" }, children: t$1("summary.title") }), jsxs("div", { style: { marginTop: "10px", display: "flex", flexDirection: "column", gap: "12px" }, children: [jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: t$1("booking.price") }), jsxs("div", { style: {
12074
12316
  color: "var(--bw-text-color)",
12075
12317
  fontWeight: 500,
12076
12318
  fontFamily: "var(--bw-font-family)",
@@ -12174,7 +12416,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
12174
12416
  fontFamily: "var(--bw-font-family)",
12175
12417
  marginTop: "8px",
12176
12418
  textAlign: "right",
12177
- }, children: t$1("summary.remainingOnSite", { amount: formatCurrency(totalAmount - depositAmount) }) }))] })] })] }), depositsOnlyBlocked && (jsx("div", { style: cardStyles$1, children: jsx("p", { style: { ...errorTextStyles$1, margin: 0 }, children: t$1("booking.depositNotConfigured") }) })), !hasPaymentProvider && !depositsOnlyBlocked && (jsx("div", { style: cardStyles$1, children: jsx("p", { style: { ...errorTextStyles$1, margin: 0 }, children: t$1("booking.paymentUnavailable") }) })), hasPaymentProvider && !depositsOnlyBlocked && (systemConfig?.paymentProvider === "mollie" ? (jsxs("div", { style: cardStyles$1, children: [jsx("h2", { style: { ...sectionHeaderStyles$1 }, children: t$1("summary.payment") }), jsx(MolliePaymentForm, { config: config, eventDetails: eventDetails, formData: paymentFormData, totalAmount: paymentAmount, discountCode: discountCodeProp, giftCards: appliedGiftCards, onSuccess: onSuccess, onError: onError, upsellSelections: aggregatedUpsellSelections(), mollieProfileId: systemConfig?.mollieProfileId, mollieTestmode: systemConfig?.mollieTestmode })] })) : (jsxs("div", { style: cardStyles$1, children: [jsx("h2", { style: { ...sectionHeaderStyles$1 }, children: t$1("summary.payment") }), jsx(StripePaymentForm, { config: config, eventDetails: eventDetails, formData: paymentFormData, totalAmount: paymentAmount, discountCode: discountCodeProp, giftCards: appliedGiftCards, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig ?? null, stripePromise: stripePromise, stripeAppearance: stripeAppearance, upsellSelections: aggregatedUpsellSelections() })] })))] }))] }) }));
12419
+ }, children: t$1("summary.remainingOnSite", { amount: formatCurrency(totalAmount - depositAmount) }) }))] })] })] }), depositsOnlyBlocked && (jsx("div", { style: cardStyles$1, children: jsx("p", { style: { ...errorTextStyles$1, margin: 0 }, children: t$1("booking.depositNotConfigured") }) })), !hasPaymentProvider && !depositsOnlyBlocked && (jsx("div", { style: cardStyles$1, children: jsx("p", { style: { ...errorTextStyles$1, margin: 0 }, children: t$1("booking.paymentUnavailable") }) })), hasPaymentProvider && !depositsOnlyBlocked && (systemConfig?.paymentProvider === "mollie" ? (jsxs("div", { style: cardStyles$1, children: [jsx("h2", { style: { ...sectionHeaderStyles$1 }, children: t$1("summary.payment") }), jsx(MolliePaymentForm, { config: config, eventDetails: eventDetails, formData: paymentFormData, totalAmount: paymentAmount, discountCode: discountCodeProp, giftCards: appliedGiftCards, onSuccess: onSuccess, onError: onError, upsellSelections: aggregatedUpsellSelections(), mollieProfileId: systemConfig?.mollieProfileId, mollieTestmode: systemConfig?.mollieTestmode })] })) : (jsxs("div", { style: cardStyles$1, children: [jsx("h2", { style: { ...sectionHeaderStyles$1 }, children: t$1("summary.payment") }), jsx(StripePaymentForm, { config: config, eventDetails: eventDetails, formData: paymentFormData, totalAmount: paymentAmount, discountCode: discountCodeProp, giftCards: appliedGiftCards, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig ?? null, stripeAppearance: stripeAppearance, upsellSelections: aggregatedUpsellSelections() })] })))] }))] }) }));
12178
12420
  }
12179
12421
 
12180
12422
  /**
@@ -13255,20 +13497,22 @@ const sectionHeaderStyles = sectionStyles.header;
13255
13497
  const labelStyles = formStyles.label;
13256
13498
  const inputStyles = formStyles.input;
13257
13499
  const errorTextStyles = formStyles.error;
13258
- function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClose, onSuccess, onError, systemConfig, preselectedEventTypeId, isLoadingEventTypes = false, }) {
13500
+ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, categories, isOpen, onClose, onSuccess, onError, systemConfig, preselectedEventTypeId, isLoadingEventTypes = false, }) {
13259
13501
  const t = useTranslations();
13260
13502
  const { locale } = useLocale();
13261
13503
  const paymentProvider = systemConfig?.paymentProvider ?? "stripe";
13262
13504
  // Form state
13263
- // Show voucher type selection if both monetary and event vouchers are allowed
13264
- // The event type selection will handle the case when no event types are available
13265
13505
  const canUseMonetaryVoucherType = voucherConfig.allowMonetaryVouchers !== false;
13266
13506
  const canUseEventVoucherType = voucherConfig.allowEventVouchers;
13267
- const showVoucherTypeSelection = canUseMonetaryVoucherType && canUseEventVoucherType && !isLoadingEventTypes;
13507
+ const canUseCategoryVoucherType = canUseEventVoucherType && categories.length > 0;
13508
+ const availableTypeCount = [canUseMonetaryVoucherType, canUseEventVoucherType, canUseCategoryVoucherType].filter(Boolean).length;
13509
+ const showVoucherTypeSelection = availableTypeCount > 1 && !isLoadingEventTypes;
13268
13510
  const [voucherType, setVoucherType] = useState(canUseMonetaryVoucherType ? "monetary" : "event");
13269
13511
  const [monetaryAmount, setMonetaryAmount] = useState(voucherConfig.monetaryPresets[2] || 10000);
13270
13512
  const [selectedEventTypeId, setSelectedEventTypeId] = useState(null);
13271
13513
  const [eventQuantity, setEventQuantity] = useState(1);
13514
+ const [selectedCategoryId, setSelectedCategoryId] = useState(null);
13515
+ const [categoryAmount, setCategoryAmount] = useState(10000);
13272
13516
  const [recipientName, setRecipientName] = useState("");
13273
13517
  const [message, setMessage] = useState("");
13274
13518
  const [customerName, setCustomerName] = useState("");
@@ -13283,6 +13527,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13283
13527
  const [stripeAppearance, setStripeAppearance] = useState(null);
13284
13528
  // Initialize Stripe
13285
13529
  useEffect(() => {
13530
+ if (!isOpen)
13531
+ return;
13286
13532
  if (paymentProvider !== "stripe")
13287
13533
  return;
13288
13534
  if (systemConfig?.stripePublishableKey && !stripePromise) {
@@ -13294,7 +13540,7 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13294
13540
  }
13295
13541
  setStripePromise(loadStripe(systemConfig.stripePublishableKey, stripeOptions));
13296
13542
  }
13297
- }, [paymentProvider, systemConfig, locale, stripePromise]);
13543
+ }, [isOpen, paymentProvider, systemConfig, locale, stripePromise]);
13298
13544
  // Initialize Stripe appearance
13299
13545
  useEffect(() => {
13300
13546
  const container = document.querySelector(".booking-widget-container");
@@ -13326,6 +13572,9 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13326
13572
  if (voucherType === "monetary") {
13327
13573
  return monetaryAmount;
13328
13574
  }
13575
+ else if (voucherType === "category") {
13576
+ return categoryAmount;
13577
+ }
13329
13578
  else {
13330
13579
  const eventType = eventTypes.find((et) => et.id === selectedEventTypeId);
13331
13580
  if (eventType) {
@@ -13333,7 +13582,7 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13333
13582
  }
13334
13583
  return 0;
13335
13584
  }
13336
- }, [voucherType, monetaryAmount, selectedEventTypeId, eventQuantity, eventTypes]);
13585
+ }, [voucherType, monetaryAmount, selectedEventTypeId, eventQuantity, eventTypes, categoryAmount]);
13337
13586
  const selectedEventType = useMemo(() => {
13338
13587
  return eventTypes.find((et) => et.id === selectedEventTypeId);
13339
13588
  }, [eventTypes, selectedEventTypeId]);
@@ -13342,6 +13591,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13342
13591
  voucherType,
13343
13592
  selectedEventTypeId,
13344
13593
  eventQuantity,
13594
+ selectedCategoryId,
13595
+ categoryAmount,
13345
13596
  recipientName: recipientName.trim(),
13346
13597
  message: message.trim(),
13347
13598
  customerName: customerName.trim(),
@@ -13351,6 +13602,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13351
13602
  voucherType,
13352
13603
  selectedEventTypeId,
13353
13604
  eventQuantity,
13605
+ selectedCategoryId,
13606
+ categoryAmount,
13354
13607
  recipientName,
13355
13608
  message,
13356
13609
  customerName,
@@ -13369,24 +13622,37 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13369
13622
  return false;
13370
13623
  if (voucherType === "event" && !selectedEventTypeId)
13371
13624
  return false;
13625
+ if (voucherType === "category" && !selectedCategoryId)
13626
+ return false;
13372
13627
  return true;
13373
- }, [customerName, customerEmail, acceptTerms, totalAmount, voucherConfig, voucherType, selectedEventTypeId]);
13628
+ }, [customerName, customerEmail, acceptTerms, totalAmount, voucherType, selectedEventTypeId, selectedCategoryId]);
13374
13629
  const defaultMonetaryAmount = useMemo(() => voucherConfig.monetaryPresets[2] || voucherConfig.monetaryPresets[0] || 1000, [voucherConfig.monetaryPresets]);
13375
13630
  const handleVoucherTypeChange = useCallback((nextType) => {
13376
13631
  setVoucherType(nextType);
13377
13632
  setPaymentError(null);
13378
13633
  if (nextType === "event") {
13379
- // Keep voucher type mutually exclusive: entering event flow resets amount selection state.
13380
13634
  setMonetaryAmount(defaultMonetaryAmount);
13635
+ setSelectedCategoryId(null);
13636
+ setCategoryAmount(10000);
13381
13637
  if (eventTypes.length === 1) {
13382
13638
  setSelectedEventTypeId(eventTypes[0]?.id ?? null);
13383
13639
  }
13384
13640
  return;
13385
13641
  }
13386
- // Keep voucher type mutually exclusive: entering amount flow clears event selection.
13642
+ if (nextType === "category") {
13643
+ setMonetaryAmount(defaultMonetaryAmount);
13644
+ setSelectedEventTypeId(null);
13645
+ setEventQuantity(1);
13646
+ if (categories.length === 1) {
13647
+ setSelectedCategoryId(categories[0]?.id ?? null);
13648
+ }
13649
+ return;
13650
+ }
13387
13651
  setSelectedEventTypeId(null);
13388
13652
  setEventQuantity(1);
13389
- }, [defaultMonetaryAmount, eventTypes]);
13653
+ setSelectedCategoryId(null);
13654
+ setCategoryAmount(10000);
13655
+ }, [defaultMonetaryAmount, eventTypes, categories]);
13390
13656
  useEffect(() => {
13391
13657
  if (!canUseMonetaryVoucherType && voucherType === "monetary") {
13392
13658
  setVoucherType("event");
@@ -13427,9 +13693,12 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13427
13693
  organizationId: config.organizationId,
13428
13694
  amount: totalAmount,
13429
13695
  currency: "eur",
13430
- voucherType,
13431
- monetaryValue: voucherType === "monetary" ? totalAmount : undefined,
13696
+ voucherType: voucherType === "category" ? "monetary" : voucherType,
13697
+ monetaryValue: voucherType === "monetary" ? totalAmount
13698
+ : voucherType === "category" ? categoryAmount
13699
+ : undefined,
13432
13700
  eventTypeId: voucherType === "event" ? selectedEventTypeId : undefined,
13701
+ eventCategoryId: voucherType === "category" ? selectedCategoryId : undefined,
13433
13702
  eventAmount: voucherType === "event" ? eventQuantity : undefined,
13434
13703
  recipientName: recipientName.trim() || undefined,
13435
13704
  message: message.trim() || undefined,
@@ -13620,7 +13889,30 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13620
13889
  fontSize: "13px",
13621
13890
  color: "var(--bw-text-muted)",
13622
13891
  marginTop: "4px",
13623
- }, children: t("voucher.eventTypeDesc") })] }))] })] })), voucherType === "monetary" && (jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectAmount") }), jsx("div", { style: {
13892
+ }, children: t("voucher.eventTypeDesc") })] })), canUseCategoryVoucherType && (jsxs("button", { type: "button", onClick: () => handleVoucherTypeChange("category"), style: {
13893
+ flex: 1,
13894
+ padding: "16px",
13895
+ borderRadius: "var(--bw-border-radius)",
13896
+ border: voucherType === "category"
13897
+ ? "2px solid var(--bw-highlight-color)"
13898
+ : "1px solid var(--bw-border-color)",
13899
+ backgroundColor: voucherType === "category"
13900
+ ? "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)"
13901
+ : "var(--bw-surface-color)",
13902
+ cursor: "pointer",
13903
+ fontFamily: "var(--bw-font-family)",
13904
+ transition: "all 0.2s ease",
13905
+ textAlign: "center",
13906
+ }, children: [jsx("div", { style: {
13907
+ fontWeight: 600,
13908
+ color: voucherType === "category"
13909
+ ? "var(--bw-highlight-color)"
13910
+ : "var(--bw-text-color)",
13911
+ }, children: t("voucher.categoryType") }), jsx("div", { style: {
13912
+ fontSize: "13px",
13913
+ color: "var(--bw-text-muted)",
13914
+ marginTop: "4px",
13915
+ }, children: t("voucher.categoryTypeDesc") })] }))] })] })), voucherType === "monetary" && (jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectAmount") }), jsx("div", { style: {
13624
13916
  display: "grid",
13625
13917
  gridTemplateColumns: "repeat(3, 1fr)",
13626
13918
  gap: "8px",
@@ -13675,7 +13967,50 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13675
13967
  fontSize: "18px",
13676
13968
  color: "var(--bw-highlight-color)",
13677
13969
  fontFamily: "var(--bw-font-family)",
13678
- }, children: formatCurrency(totalAmount) })] }) }))] })), jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.recipientSection") }), jsx("p", { style: {
13970
+ }, children: formatCurrency(totalAmount) })] }) }))] })), voucherType === "category" && (jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectCategory") }), jsx("div", { style: { marginTop: "12px" }, children: jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [jsxs("div", { children: [jsx("label", { htmlFor: "category-select", style: labelStyles, children: t("voucher.categoryLabel") }), jsxs("select", { id: "category-select", value: selectedCategoryId || "", onChange: (e) => setSelectedCategoryId(e.target.value ? Number(e.target.value) : null), disabled: categories.length === 1, style: {
13971
+ ...inputStyles,
13972
+ cursor: categories.length === 1 ? "not-allowed" : "pointer",
13973
+ }, children: [categories.length > 1 && (jsx("option", { value: "", children: t("voucher.selectCategoryPlaceholder") })), categories.map((cat) => (jsx("option", { value: cat.id, children: cat.name }, cat.id)))] })] }), selectedCategoryId && (jsxs("div", { children: [jsx("label", { htmlFor: "category-amount", style: labelStyles, children: t("voucher.categoryAmount") }), jsx("div", { style: {
13974
+ display: "grid",
13975
+ gridTemplateColumns: "repeat(3, 1fr)",
13976
+ gap: "8px",
13977
+ marginTop: "4px",
13978
+ }, children: voucherConfig.monetaryPresets.map((preset) => (jsx("button", { type: "button", onClick: () => setCategoryAmount(preset), style: {
13979
+ padding: "10px",
13980
+ borderRadius: "var(--bw-border-radius)",
13981
+ border: categoryAmount === preset
13982
+ ? "2px solid var(--bw-highlight-color)"
13983
+ : "1px solid var(--bw-border-color)",
13984
+ backgroundColor: categoryAmount === preset
13985
+ ? "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)"
13986
+ : "var(--bw-surface-color)",
13987
+ cursor: "pointer",
13988
+ fontFamily: "var(--bw-font-family)",
13989
+ fontWeight: 600,
13990
+ fontSize: "14px",
13991
+ color: categoryAmount === preset
13992
+ ? "var(--bw-highlight-color)"
13993
+ : "var(--bw-text-color)",
13994
+ transition: "all 0.2s ease",
13995
+ }, children: formatCurrency(preset) }, preset))) })] }))] }) }), selectedCategoryId && categoryAmount > 0 && (jsx("div", { style: {
13996
+ marginTop: "16px",
13997
+ padding: "12px",
13998
+ backgroundColor: "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)",
13999
+ borderRadius: "var(--bw-border-radius)",
14000
+ border: "1px solid var(--bw-highlight-color)",
14001
+ }, children: jsxs("div", { style: {
14002
+ display: "flex",
14003
+ justifyContent: "space-between",
14004
+ alignItems: "center",
14005
+ }, children: [jsx("span", { style: {
14006
+ color: "var(--bw-text-color)",
14007
+ fontFamily: "var(--bw-font-family)",
14008
+ }, children: categories.find((c) => c.id === selectedCategoryId)?.name }), jsx("span", { style: {
14009
+ fontWeight: 700,
14010
+ fontSize: "18px",
14011
+ color: "var(--bw-highlight-color)",
14012
+ fontFamily: "var(--bw-font-family)",
14013
+ }, children: formatCurrency(categoryAmount) })] }) }))] })), jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.recipientSection") }), jsx("p", { style: {
13679
14014
  fontSize: "13px",
13680
14015
  color: "var(--bw-text-muted)",
13681
14016
  fontFamily: "var(--bw-font-family)",
@@ -13708,7 +14043,9 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13708
14043
  fontFamily: "var(--bw-font-family)",
13709
14044
  }, children: voucherType === "monetary"
13710
14045
  ? t("voucher.monetaryVoucher")
13711
- : `${eventQuantity}x ${selectedEventType?.name || t("voucher.eventVoucher")}` }), recipientName && (jsxs("div", { style: {
14046
+ : voucherType === "category"
14047
+ ? categories.find((c) => c.id === selectedCategoryId)?.name || t("voucher.categoryVoucher")
14048
+ : `${eventQuantity}x ${selectedEventType?.name || t("voucher.eventVoucher")}` }), recipientName && (jsxs("div", { style: {
13712
14049
  fontSize: "13px",
13713
14050
  color: "var(--bw-text-muted)",
13714
14051
  fontFamily: "var(--bw-font-family)",
@@ -13984,7 +14321,7 @@ function VoucherAttachment({ onClick }) {
13984
14321
  }, children: t("voucher.buyAsGift") })] }));
13985
14322
  }
13986
14323
 
13987
- function VoucherIntegration({ config, voucherConfig, eventTypes, systemConfig, isFormOpen, isLoadingConfig, preselectedEventTypeId, voucherPurchaseResult, isSuccess, showStandaloneCard, onCardClick, onFormClose, onSuccess, onError, onSuccessModalClose, }) {
14324
+ function VoucherIntegration({ config, voucherConfig, eventTypes, categories, systemConfig, isFormOpen, isLoadingConfig, preselectedEventTypeId, voucherPurchaseResult, isSuccess, showStandaloneCard, onCardClick, onFormClose, onSuccess, onError, onSuccessModalClose, }) {
13988
14325
  if (!voucherConfig?.enabled) {
13989
14326
  return null;
13990
14327
  }
@@ -13995,7 +14332,7 @@ function VoucherIntegration({ config, voucherConfig, eventTypes, systemConfig, i
13995
14332
  justifyContent: "center",
13996
14333
  }, children: jsx(VoucherPurchaseCard, { config: voucherConfig, minEventPrice: eventTypes.length > 0
13997
14334
  ? Math.min(...eventTypes.map((et) => et.maxPrice))
13998
- : undefined, fallbackImages: eventTypes.flatMap((eventType) => eventType.images || []), onClick: onCardClick, standalone: true }) }) })), jsx(VoucherPurchaseForm, { config: config, voucherConfig: voucherConfig, eventTypes: eventTypes, isOpen: isFormOpen, onClose: onFormClose, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, preselectedEventTypeId: preselectedEventTypeId, isLoadingEventTypes: isLoadingConfig }), isSuccess && voucherPurchaseResult && (jsx(VoucherSuccessModal, { isOpen: true, onClose: onSuccessModalClose, result: voucherPurchaseResult }))] }));
14335
+ : undefined, fallbackImages: eventTypes.flatMap((eventType) => eventType.images || []), onClick: onCardClick, standalone: true }) }) })), jsx(VoucherPurchaseForm, { config: config, voucherConfig: voucherConfig, eventTypes: eventTypes, categories: categories, isOpen: isFormOpen, onClose: onFormClose, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, preselectedEventTypeId: preselectedEventTypeId, isLoadingEventTypes: isLoadingConfig }), isSuccess && voucherPurchaseResult && (jsx(VoucherSuccessModal, { isOpen: true, onClose: onSuccessModalClose, result: voucherPurchaseResult }))] }));
13999
14336
  }
14000
14337
 
14001
14338
  // Helper function to preprocess markdown for underline support
@@ -14258,8 +14595,17 @@ function EventTypeSelection({ eventTypes, onEventTypeSelect, onInstancePreview,
14258
14595
  if (!item)
14259
14596
  return jsx("div", { style: { height: "34px" } }, i);
14260
14597
  const hasDiscount = item.basePrice > 0 && item.price < item.basePrice;
14261
- return (jsxs("div", { onClick: (e) => { e.stopPropagation(); onInstancePreview?.(item.id, eventType.id); }, onMouseEnter: (e) => { if (onInstancePreview)
14262
- e.currentTarget.style.backgroundColor = "var(--bw-border-color)"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "transparent"; }, style: {
14598
+ const isRowBookable = item.bookingOpen && item.availableSpots > 0;
14599
+ return (jsxs("div", { onClick: (e) => {
14600
+ e.stopPropagation();
14601
+ if (isRowBookable) {
14602
+ onInstancePreview?.(item.id, eventType.id);
14603
+ }
14604
+ }, onMouseEnter: (e) => {
14605
+ if (onInstancePreview && isRowBookable) {
14606
+ e.currentTarget.style.backgroundColor = "var(--bw-highlight-subtle)";
14607
+ }
14608
+ }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "transparent"; }, style: {
14263
14609
  height: "34px",
14264
14610
  display: "grid",
14265
14611
  gridTemplateColumns: "auto 1fr auto 20px",
@@ -14268,12 +14614,13 @@ function EventTypeSelection({ eventTypes, onEventTypeSelect, onInstancePreview,
14268
14614
  width: "100%",
14269
14615
  fontSize: "13px",
14270
14616
  color: "var(--bw-text-muted)",
14271
- cursor: onInstancePreview ? "pointer" : "default",
14617
+ cursor: onInstancePreview && isRowBookable ? "pointer" : "default",
14272
14618
  borderRadius: "4px",
14273
14619
  padding: "0 2px",
14274
14620
  transition: "background 0.15s",
14275
14621
  boxSizing: "border-box",
14276
- }, children: [jsxs("span", { style: { whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: "3px" }, children: [item.isSpecial && (jsx("span", { style: { color: "var(--bw-highlight-color)", fontSize: "11px", lineHeight: 1 }, children: "\u2605" })), formatWeekday(item.startTime, timezone, locale), " ", formatDate(item.startTime, timezone, locale)] }), jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", opacity: 0.75 }, children: item.name }), jsxs("span", { style: { display: "flex", alignItems: "center", gap: "4px", whiteSpace: "nowrap" }, children: [hasDiscount && (jsx("span", { style: { textDecoration: "line-through", opacity: 0.55, fontSize: "11px" }, children: formatCurrency(item.basePrice) })), jsx("span", { style: { fontWeight: 700, color: item.isSpecial ? "var(--bw-highlight-color)" : "var(--bw-text-color)" }, children: formatCurrency(item.price) })] }), item.specialDescription ? (jsx(InfoBadge, { text: item.specialDescription })) : (jsx("span", {}))] }, item.id));
14622
+ opacity: isRowBookable ? 1 : 0.55,
14623
+ }, children: [jsxs("span", { style: { whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: "3px" }, children: [item.isSpecial && (jsx("span", { style: { color: "var(--bw-highlight-color)", fontSize: "11px", lineHeight: 1 }, children: "\u2605" })), formatWeekday(item.startTime, timezone, locale), " ", formatDate(item.startTime, timezone, locale)] }), jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", opacity: 0.75 }, children: item.name }), jsxs("span", { style: { display: "flex", alignItems: "center", gap: "4px", whiteSpace: "nowrap" }, children: [hasDiscount && (jsx("span", { style: { textDecoration: "line-through", opacity: 0.55, fontSize: "11px" }, children: formatCurrency(item.basePrice) })), jsx("span", { style: { fontWeight: 700, color: item.isSpecial ? "var(--bw-highlight-color)" : "var(--bw-text-color)" }, children: formatCurrency(item.price) })] }), item.isSpecial && item.specialDescription ? (jsx(InfoBadge, { text: item.specialDescription })) : (jsx("span", {}))] }, item.id));
14277
14624
  }) }), hasMoreDates && (jsx("button", { type: "button", onClick: (e) => {
14278
14625
  e.stopPropagation();
14279
14626
  onEventTypeSelect(eventType);
@@ -14362,6 +14709,74 @@ function EventTypeSelection({ eventTypes, onEventTypeSelect, onInstancePreview,
14362
14709
  }) }) })), jsx(PaymentLogosStrip, {}), jsx(EventTypeDetailsDialog, { isOpen: detailsDialogOpen, onClose: handleCloseDetails, eventType: selectedEventTypeForDetails, onEventTypeSelect: onEventTypeSelect })] }));
14363
14710
  }
14364
14711
 
14712
+ function WaitlistDialog({ apiBaseUrl, organizationId, eventInstanceId, eventName, onClose, }) {
14713
+ const t = useTranslations();
14714
+ const [name, setName] = useState("");
14715
+ const [email, setEmail] = useState("");
14716
+ const [phone, setPhone] = useState("");
14717
+ const [isSubmitting, setIsSubmitting] = useState(false);
14718
+ const [error, setError] = useState(null);
14719
+ const [position, setPosition] = useState(null);
14720
+ const handleSubmit = async () => {
14721
+ setError(null);
14722
+ if (!name.trim() || !email.trim()) {
14723
+ setError(t("waitlist.requiredFields"));
14724
+ return;
14725
+ }
14726
+ setIsSubmitting(true);
14727
+ try {
14728
+ const response = await fetch(getApiUrl(apiBaseUrl, "/booking/waitlist"), {
14729
+ method: "POST",
14730
+ headers: { "Content-Type": "application/json" },
14731
+ body: JSON.stringify({
14732
+ organizationId,
14733
+ eventInstanceId,
14734
+ name: name.trim(),
14735
+ email: email.trim(),
14736
+ phone: phone.trim() || undefined,
14737
+ }),
14738
+ });
14739
+ if (!response.ok) {
14740
+ throw new Error("request_failed");
14741
+ }
14742
+ const data = (await response.json());
14743
+ setPosition(data.position ?? null);
14744
+ }
14745
+ catch {
14746
+ setError(t("waitlist.submitError"));
14747
+ }
14748
+ finally {
14749
+ setIsSubmitting(false);
14750
+ }
14751
+ };
14752
+ return (jsx("div", { style: {
14753
+ position: "fixed",
14754
+ inset: 0,
14755
+ zIndex: 1000,
14756
+ display: "flex",
14757
+ alignItems: "center",
14758
+ justifyContent: "center",
14759
+ backgroundColor: "rgba(15, 23, 42, 0.6)",
14760
+ padding: "16px",
14761
+ fontFamily: "var(--bw-font-family)",
14762
+ }, onClick: onClose, children: jsx("div", { style: {
14763
+ width: "100%",
14764
+ maxWidth: "420px",
14765
+ backgroundColor: "var(--bw-surface-color)",
14766
+ border: "1px solid var(--bw-border-color)",
14767
+ borderRadius: "var(--bw-border-radius)",
14768
+ padding: "24px",
14769
+ }, onClick: (e) => e.stopPropagation(), children: position !== null ? (jsxs("div", { style: { textAlign: "center" }, children: [jsx("h3", { style: {
14770
+ margin: "0 0 8px",
14771
+ color: "var(--bw-highlight-color)",
14772
+ fontSize: "18px",
14773
+ }, children: t("waitlist.successTitle") }), jsx("p", { style: mergeStyles(textStyles.body, { marginBottom: "20px" }), children: t("waitlist.successMessage", { position }) }), jsx("button", { type: "button", style: mergeStyles(buttonStyles.primary, buttonStyles.fullWidth), onClick: onClose, children: t("waitlist.done") })] })) : (jsxs(Fragment, { children: [jsx("h3", { style: {
14774
+ margin: "0 0 4px",
14775
+ color: "var(--bw-highlight-color)",
14776
+ fontSize: "18px",
14777
+ }, children: t("waitlist.title") }), jsx("p", { style: mergeStyles(textStyles.muted, { marginBottom: "16px" }), children: t("waitlist.subtitle", { name: eventName }) }), jsxs("div", { style: { marginBottom: "12px" }, children: [jsx("label", { style: formStyles.label, htmlFor: "wl-name", children: t("waitlist.name") }), jsx("input", { id: "wl-name", style: formStyles.input, value: name, onChange: (e) => setName(e.target.value) })] }), jsxs("div", { style: { marginBottom: "12px" }, children: [jsx("label", { style: formStyles.label, htmlFor: "wl-email", children: t("waitlist.email") }), jsx("input", { id: "wl-email", type: "email", style: formStyles.input, value: email, onChange: (e) => setEmail(e.target.value) })] }), jsxs("div", { style: { marginBottom: "12px" }, children: [jsx("label", { style: formStyles.label, htmlFor: "wl-phone", children: t("waitlist.phone") }), jsx("input", { id: "wl-phone", type: "tel", style: formStyles.input, value: phone, onChange: (e) => setPhone(e.target.value) })] }), error && jsx("p", { style: formStyles.error, children: error }), jsxs("div", { style: { display: "flex", gap: "8px", marginTop: "16px" }, children: [jsx("button", { type: "button", style: mergeStyles(buttonStyles.secondary, buttonStyles.fullWidth), onClick: onClose, disabled: isSubmitting, children: t("waitlist.cancel") }), jsx("button", { type: "button", style: mergeStyles(buttonStyles.primary, buttonStyles.fullWidth), onClick: handleSubmit, disabled: isSubmitting, children: isSubmitting ? t("waitlist.submitting") : t("waitlist.join") })] })] })) }) }));
14778
+ }
14779
+
14365
14780
  const getAllocationBadgeInfo = (availableSpots, maxParticipants, t) => {
14366
14781
  const availabilityRatio = availableSpots / maxParticipants;
14367
14782
  if (availabilityRatio >= 0.6)
@@ -14486,12 +14901,13 @@ const PriceDisplay = ({ price, yearPrices, t }) => {
14486
14901
  boxShadow: displayInfo ? "0 2px 4px rgba(0, 0, 0, 0.2)" : "none",
14487
14902
  }, children: formatCurrency(price) }));
14488
14903
  };
14489
- function EventInstanceSelection({ eventInstances, selectedEventType, onEventInstanceSelect, onBackToEventTypes, isOpen, onClose, isLoadingEventInstances = false, isLoadingEventDetails = false, hasUpsellsStep = false, }) {
14904
+ function EventInstanceSelection({ eventInstances, selectedEventType, onEventInstanceSelect, onBackToEventTypes, isOpen, onClose, isLoadingEventInstances = false, isLoadingEventDetails = false, hasUpsellsStep = false, apiBaseUrl, organizationId, }) {
14490
14905
  const t = useTranslations();
14491
14906
  const { locale } = useLocale();
14492
14907
  const timezone = useTimezone();
14493
14908
  const [selectedEventInstanceId, setSelectedEventInstanceId] = useState(null);
14494
14909
  const [openGroups, setOpenGroups] = useState(new Set());
14910
+ const [waitlistEvent, setWaitlistEvent] = useState(null);
14495
14911
  const getMonthPriceDisplayInfo = (minPrice) => {
14496
14912
  return getPriceDisplayInfo(minPrice, yearPrices, t);
14497
14913
  };
@@ -14652,116 +15068,127 @@ function EventInstanceSelection({ eventInstances, selectedEventType, onEventInst
14652
15068
  fontFamily: "var(--bw-font-family)",
14653
15069
  }, children: t("instances.noAvailable") }), jsx("p", { style: { color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: t("instances.noAvailableMessage") })] }) }) }));
14654
15070
  }
14655
- return (jsx(Sidebar, { isOpen: isOpen, onClose: handleClose, title: `${selectedEventType?.name}`, footer: footerNav, children: jsxs("div", { style: { padding: "20px 10px" }, children: [jsx(FlowProgress, { currentStep: "date", hasUpsellsStep: hasUpsellsStep }), jsx("div", { style: { display: "flex", flexDirection: "column", gap: "16px" }, children: monthYearGroups.map(({ key, label, events, minPrice, year }, idx) => {
14656
- const monthPriceDisplayInfo = getMonthPriceDisplayInfo(minPrice);
14657
- return (jsxs(Fragment$1, { children: [idx > 0 && monthYearGroups[idx - 1].year !== year && (jsx("div", { style: { height: "1px", backgroundColor: "var(--bw-border-color)", margin: "4px 0" } })), jsx(Accordion, { title: label, priceInfo: jsx("div", { style: {
14658
- fontSize: "16px",
14659
- fontWeight: 500,
14660
- marginLeft: "auto",
14661
- padding: "4px 8px",
14662
- borderRadius: "var(--bw-border-radius-small)",
14663
- backgroundColor: monthPriceDisplayInfo
14664
- ? monthPriceDisplayInfo.backgroundColor
14665
- : "#14532d",
14666
- color: monthPriceDisplayInfo
14667
- ? monthPriceDisplayInfo.textColor
14668
- : "#4ade80",
14669
- boxShadow: monthPriceDisplayInfo
14670
- ? "0 2px 4px rgba(0, 0, 0, 0.2)"
14671
- : undefined,
14672
- }, children: `${t("common.from")} ${formatCurrency(minPrice)}` }), isOpen: openGroups.has(key), onToggle: () => toggleGroup(key), children: jsx("div", { style: { display: "flex", flexDirection: "column", gap: "12px", paddingTop: "12px" }, children: events.map((event) => {
14673
- const availableSpots = event.maxParticipants - event.participantCount;
14674
- const isFullyBooked = availableSpots === 0;
14675
- const startDate = new Date(event.startTime);
14676
- const isPastEvent = today.toISOString() >= startDate.toISOString();
14677
- const isDisabled = isFullyBooked || isPastEvent || !event.bookingOpen;
14678
- return (jsxs("div", { style: {
14679
- position: "relative",
14680
- border: "1px solid var(--bw-border-color)",
14681
- backgroundColor: "var(--bw-surface-color)",
14682
- borderRadius: "var(--bw-border-radius)",
14683
- padding: "16px 10px",
14684
- transition: "all 0.2s ease",
14685
- fontFamily: "var(--bw-font-family)",
14686
- opacity: isDisabled ? 0.3 : 1,
14687
- filter: isDisabled ? "grayscale(40%)" : "none",
14688
- cursor: isDisabled ? "not-allowed" : "pointer",
14689
- }, onClick: () => {
14690
- if (!isDisabled) {
14691
- handleEventInstanceSelect(event);
14692
- }
14693
- }, children: [selectedEventInstanceId === event.id && isLoadingEventDetails && (jsx("div", { style: {
14694
- position: "absolute",
14695
- inset: 0,
14696
- backgroundColor: "rgba(15, 23, 42, 0.8)",
14697
- borderRadius: "var(--bw-border-radius)",
14698
- display: "flex",
14699
- alignItems: "center",
14700
- justifyContent: "center",
14701
- }, children: jsx("div", { style: {
14702
- width: "32px",
14703
- height: "32px",
14704
- color: "var(--bw-highlight-color)",
14705
- opacity: 0.8,
14706
- fontSize: "32px",
14707
- }, children: spinner() }) })), jsx(SpecialPriceBadge, { price: event.price, yearPrices: yearPrices, t: t }), jsx(AllocationBadge, { availableSpots: availableSpots, maxParticipants: event.maxParticipants, t: t }), jsxs("div", { style: {
14708
- display: "flex",
14709
- justifyContent: "space-between",
14710
- width: "100%",
14711
- alignItems: "flex-start",
14712
- gap: "12px",
14713
- marginBottom: "4px",
14714
- }, children: [jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: "12px" }, children: [jsx("div", { style: {
14715
- fontSize: "16px",
14716
- transition: "all 0.2s ease",
14717
- borderRadius: "var(--bw-border-radius-small)",
14718
- borderTop: "4px solid var(--bw-border-color)",
14719
- border: "1px solid var(--bw-border-color)",
14720
- width: "40px",
14721
- height: "40px",
14722
- display: "flex",
14723
- alignItems: "center",
14724
- justifyContent: "center",
14725
- fontWeight: 700,
14726
- color: "var(--bw-text-color)",
14727
- backgroundColor: "var(--bw-background-color)",
14728
- }, children: startDate.getDate() }), jsxs("div", { style: {
14729
- fontSize: "16px",
14730
- color: "var(--bw-text-color)",
14731
- display: "flex",
14732
- flexDirection: "column",
14733
- alignItems: "flex-start",
14734
- justifyContent: "flex-start",
14735
- lineHeight: 1.25,
14736
- }, children: [jsxs("div", { children: [jsx("span", { style: { fontWeight: 600, marginBottom: "2px", textTransform: "capitalize" }, children: formatWeekday(event.startTime, timezone, locale) }), formatWeekday(event.startTime, timezone, locale) !==
14737
- formatWeekday(event.endTime, timezone, locale) && (jsxs(Fragment, { children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: " - " }), jsx("span", { style: { fontWeight: 600, marginBottom: "2px", textTransform: "capitalize" }, children: formatWeekday(event.endTime, timezone, locale) })] }))] }), jsx("div", { children: formatWeekday(event.startTime, timezone, locale) ===
14738
- formatWeekday(event.endTime, timezone, locale) ? (jsxs(Fragment, { children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: formatTime(event.startTime, timezone, locale) }), jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: " - " }), jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: formatTime(event.endTime, timezone, locale) })] })) : (jsxs("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: [formatTime(event.startTime, timezone, locale), " ", t("instances.oclock")] })) })] }), jsxs("span", { style: {
14739
- fontSize: "12px",
14740
- fontWeight: 400,
14741
- color: "var(--bw-text-muted)",
14742
- marginLeft: "6px",
14743
- backgroundColor: "rgba(0, 0, 0, 0.05)",
14744
- whiteSpace: "nowrap",
14745
- padding: "2px 6px",
14746
- borderRadius: "var(--bw-border-radius-small)",
14747
- }, children: [event.durationDays, " ", event.durationDays > 1 ? t("common.days") : t("common.day")] })] }), jsx("div", { style: {
14748
- textAlign: "right",
14749
- display: "flex",
14750
- flexDirection: "column",
14751
- alignItems: "flex-end",
14752
- }, children: jsx(PriceDisplay, { price: event.price, yearPrices: yearPrices, t: t }) })] }), event.name !== selectedEventType?.name && (jsx("h4", { style: {
14753
- fontSize: "16px",
14754
- fontWeight: 600,
14755
- color: "var(--bw-text-color)",
14756
- lineHeight: 1.25,
14757
- margin: "0 0 2px 0",
14758
- display: "flex",
14759
- alignItems: "center",
14760
- gap: "8px",
14761
- maxWidth: "230px",
14762
- }, children: event.name }))] }, event.id));
14763
- }) }) })] }, key));
14764
- }) })] }) }));
15071
+ return (jsxs(Sidebar, { isOpen: isOpen, onClose: handleClose, title: `${selectedEventType?.name}`, footer: footerNav, children: [jsxs("div", { style: { padding: "20px 10px" }, children: [jsx(FlowProgress, { currentStep: "date", hasUpsellsStep: hasUpsellsStep }), jsx("div", { style: { display: "flex", flexDirection: "column", gap: "16px" }, children: monthYearGroups.map(({ key, label, events, minPrice, year }, idx) => {
15072
+ const monthPriceDisplayInfo = getMonthPriceDisplayInfo(minPrice);
15073
+ return (jsxs(Fragment$1, { children: [idx > 0 && monthYearGroups[idx - 1].year !== year && (jsx("div", { style: { height: "1px", backgroundColor: "var(--bw-border-color)", margin: "4px 0" } })), jsx(Accordion, { title: label, priceInfo: jsx("div", { style: {
15074
+ fontSize: "16px",
15075
+ fontWeight: 500,
15076
+ marginLeft: "auto",
15077
+ padding: "4px 8px",
15078
+ borderRadius: "var(--bw-border-radius-small)",
15079
+ backgroundColor: monthPriceDisplayInfo
15080
+ ? monthPriceDisplayInfo.backgroundColor
15081
+ : "#14532d",
15082
+ color: monthPriceDisplayInfo
15083
+ ? monthPriceDisplayInfo.textColor
15084
+ : "#4ade80",
15085
+ boxShadow: monthPriceDisplayInfo
15086
+ ? "0 2px 4px rgba(0, 0, 0, 0.2)"
15087
+ : undefined,
15088
+ }, children: `${t("common.from")} ${formatCurrency(minPrice)}` }), isOpen: openGroups.has(key), onToggle: () => toggleGroup(key), children: jsx("div", { style: { display: "flex", flexDirection: "column", gap: "12px", paddingTop: "12px" }, children: events.map((event) => {
15089
+ const availableSpots = event.maxParticipants - event.participantCount;
15090
+ const isFullyBooked = availableSpots === 0;
15091
+ const startDate = new Date(event.startTime);
15092
+ const isPastEvent = today.toISOString() >= startDate.toISOString();
15093
+ const isDisabled = isFullyBooked || isPastEvent || !event.bookingOpen;
15094
+ const canWaitlist = isFullyBooked &&
15095
+ !isPastEvent &&
15096
+ !!apiBaseUrl &&
15097
+ !!organizationId;
15098
+ return (jsxs("div", { style: {
15099
+ position: "relative",
15100
+ border: "1px solid var(--bw-border-color)",
15101
+ backgroundColor: "var(--bw-surface-color)",
15102
+ borderRadius: "var(--bw-border-radius)",
15103
+ padding: "16px 10px",
15104
+ transition: "all 0.2s ease",
15105
+ fontFamily: "var(--bw-font-family)",
15106
+ opacity: isDisabled && !canWaitlist ? 0.3 : 1,
15107
+ filter: isDisabled && !canWaitlist ? "grayscale(40%)" : "none",
15108
+ cursor: isDisabled
15109
+ ? canWaitlist
15110
+ ? "default"
15111
+ : "not-allowed"
15112
+ : "pointer",
15113
+ }, onClick: () => {
15114
+ if (!isDisabled) {
15115
+ handleEventInstanceSelect(event);
15116
+ }
15117
+ }, children: [selectedEventInstanceId === event.id && isLoadingEventDetails && (jsx("div", { style: {
15118
+ position: "absolute",
15119
+ inset: 0,
15120
+ backgroundColor: "rgba(15, 23, 42, 0.8)",
15121
+ borderRadius: "var(--bw-border-radius)",
15122
+ display: "flex",
15123
+ alignItems: "center",
15124
+ justifyContent: "center",
15125
+ }, children: jsx("div", { style: {
15126
+ width: "32px",
15127
+ height: "32px",
15128
+ color: "var(--bw-highlight-color)",
15129
+ opacity: 0.8,
15130
+ fontSize: "32px",
15131
+ }, children: spinner() }) })), jsx(SpecialPriceBadge, { price: event.price, yearPrices: yearPrices, t: t }), jsx(AllocationBadge, { availableSpots: availableSpots, maxParticipants: event.maxParticipants, t: t }), jsxs("div", { style: {
15132
+ display: "flex",
15133
+ justifyContent: "space-between",
15134
+ width: "100%",
15135
+ alignItems: "flex-start",
15136
+ gap: "12px",
15137
+ marginBottom: "4px",
15138
+ }, children: [jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: "12px" }, children: [jsx("div", { style: {
15139
+ fontSize: "16px",
15140
+ transition: "all 0.2s ease",
15141
+ borderRadius: "var(--bw-border-radius-small)",
15142
+ borderTop: "4px solid var(--bw-border-color)",
15143
+ border: "1px solid var(--bw-border-color)",
15144
+ width: "40px",
15145
+ height: "40px",
15146
+ display: "flex",
15147
+ alignItems: "center",
15148
+ justifyContent: "center",
15149
+ fontWeight: 700,
15150
+ color: "var(--bw-text-color)",
15151
+ backgroundColor: "var(--bw-background-color)",
15152
+ }, children: startDate.getDate() }), jsxs("div", { style: {
15153
+ fontSize: "16px",
15154
+ color: "var(--bw-text-color)",
15155
+ display: "flex",
15156
+ flexDirection: "column",
15157
+ alignItems: "flex-start",
15158
+ justifyContent: "flex-start",
15159
+ lineHeight: 1.25,
15160
+ }, children: [jsxs("div", { children: [jsx("span", { style: { fontWeight: 600, marginBottom: "2px", textTransform: "capitalize" }, children: formatWeekday(event.startTime, timezone, locale) }), formatWeekday(event.startTime, timezone, locale) !==
15161
+ formatWeekday(event.endTime, timezone, locale) && (jsxs(Fragment, { children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: " - " }), jsx("span", { style: { fontWeight: 600, marginBottom: "2px", textTransform: "capitalize" }, children: formatWeekday(event.endTime, timezone, locale) })] }))] }), jsx("div", { children: formatWeekday(event.startTime, timezone, locale) ===
15162
+ formatWeekday(event.endTime, timezone, locale) ? (jsxs(Fragment, { children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: formatTime(event.startTime, timezone, locale) }), jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: " - " }), jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: formatTime(event.endTime, timezone, locale) })] })) : (jsxs("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: [formatTime(event.startTime, timezone, locale), " ", t("instances.oclock")] })) })] }), jsxs("span", { style: {
15163
+ fontSize: "12px",
15164
+ fontWeight: 400,
15165
+ color: "var(--bw-text-muted)",
15166
+ marginLeft: "6px",
15167
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
15168
+ whiteSpace: "nowrap",
15169
+ padding: "2px 6px",
15170
+ borderRadius: "var(--bw-border-radius-small)",
15171
+ }, children: [event.durationDays, " ", event.durationDays > 1 ? t("common.days") : t("common.day")] })] }), jsx("div", { style: {
15172
+ textAlign: "right",
15173
+ display: "flex",
15174
+ flexDirection: "column",
15175
+ alignItems: "flex-end",
15176
+ }, children: jsx(PriceDisplay, { price: event.price, yearPrices: yearPrices, t: t }) })] }), event.name !== selectedEventType?.name && (jsx("h4", { style: {
15177
+ fontSize: "16px",
15178
+ fontWeight: 600,
15179
+ color: "var(--bw-text-color)",
15180
+ lineHeight: 1.25,
15181
+ margin: "0 0 2px 0",
15182
+ display: "flex",
15183
+ alignItems: "center",
15184
+ gap: "8px",
15185
+ maxWidth: "230px",
15186
+ }, children: event.name })), canWaitlist && (jsx("button", { type: "button", style: mergeStyles(buttonStyles.secondary, buttonStyles.fullWidth, buttonStyles.small, { marginTop: "8px" }), onClick: (e) => {
15187
+ e.stopPropagation();
15188
+ setWaitlistEvent(event);
15189
+ }, children: t("waitlist.joinCta") }))] }, event.id));
15190
+ }) }) })] }, key));
15191
+ }) })] }), waitlistEvent && apiBaseUrl && organizationId && (jsx(WaitlistDialog, { apiBaseUrl: apiBaseUrl, organizationId: organizationId, eventInstanceId: waitlistEvent.id, eventName: waitlistEvent.name, onClose: () => setWaitlistEvent(null) }))] }));
14765
15192
  }
14766
15193
 
14767
15194
  function NextEventsPreview({ events, onEventSelect, onShowAll, showAllButtonText, showAllButton, isLoadingEventDetails = false, isLoadingShowAll = false, isLoading = false, count = 3, }) {
@@ -15173,6 +15600,312 @@ function SpecialsView({ specials, onEventSelect, isLoading = false, showSavingsA
15173
15600
  }) })] }));
15174
15601
  }
15175
15602
 
15603
+ function toMinutesForUnit(unit) {
15604
+ switch (unit.toLowerCase()) {
15605
+ case "minute":
15606
+ case "minutes":
15607
+ return 1;
15608
+ case "hour":
15609
+ case "hours":
15610
+ return 60;
15611
+ case "day":
15612
+ case "days":
15613
+ return 24 * 60;
15614
+ default:
15615
+ return 1;
15616
+ }
15617
+ }
15618
+ // --- Timezone helpers ------------------------------------------------------
15619
+ // The booking is evaluated server-side against the organization's local
15620
+ // operating hours. The customer's browser may be in a different timezone, so we
15621
+ // interpret the datetime-local field as a wall-clock time in the ORG timezone
15622
+ // (not the browser's) and convert to a UTC instant for the API.
15623
+ function getTzParts(date, timeZone) {
15624
+ const dtf = new Intl.DateTimeFormat("en-CA", {
15625
+ timeZone,
15626
+ hour12: false,
15627
+ year: "numeric",
15628
+ month: "2-digit",
15629
+ day: "2-digit",
15630
+ hour: "2-digit",
15631
+ minute: "2-digit",
15632
+ second: "2-digit",
15633
+ });
15634
+ const parts = {};
15635
+ for (const part of dtf.formatToParts(date)) {
15636
+ if (part.type !== "literal")
15637
+ parts[part.type] = part.value;
15638
+ }
15639
+ return parts;
15640
+ }
15641
+ function tzOffsetMs(utcMs, timeZone) {
15642
+ const p = getTzParts(new Date(utcMs), timeZone);
15643
+ const hour = p.hour === "24" ? "00" : p.hour;
15644
+ const asLocalUtc = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(hour), Number(p.minute), Number(p.second));
15645
+ return asLocalUtc - utcMs;
15646
+ }
15647
+ /** Convert an org-tz wall-clock string ("YYYY-MM-DDTHH:mm") to a UTC instant. */
15648
+ function zonedWallTimeToUtc(wallTime, timeZone) {
15649
+ const [datePart, timePart] = wallTime.split("T");
15650
+ if (!datePart || !timePart)
15651
+ return new Date(NaN);
15652
+ const [y, mo, d] = datePart.split("-").map(Number);
15653
+ const [h, mi] = timePart.split(":").map(Number);
15654
+ const naiveUtc = Date.UTC(y, (mo ?? 1) - 1, d, h ?? 0, mi ?? 0);
15655
+ // Single-pass offset correction (good enough outside the ~1h DST fold).
15656
+ const offset = tzOffsetMs(naiveUtc, timeZone);
15657
+ return new Date(naiveUtc - offset);
15658
+ }
15659
+ /** Render a UTC instant as an org-tz datetime-local input value. */
15660
+ function utcToZonedInputValue(date, timeZone) {
15661
+ const p = getTzParts(date, timeZone);
15662
+ const hour = p.hour === "24" ? "00" : p.hour;
15663
+ return `${p.year}-${p.month}-${p.day}T${hour}:${p.minute}`;
15664
+ }
15665
+ /** Format a UTC instant for display in the org timezone. */
15666
+ function formatInTimeZone(date, timeZone, locale) {
15667
+ return new Intl.DateTimeFormat(locale, {
15668
+ timeZone,
15669
+ dateStyle: "medium",
15670
+ timeStyle: "short",
15671
+ }).format(date);
15672
+ }
15673
+ function calculateTieredPrice(durationUnits, config) {
15674
+ const tiers = [...(config.tiers ?? [])]
15675
+ .filter((tier) => tier.unitCount > 0 && tier.price >= 0)
15676
+ .sort((a, b) => b.unitCount - a.unitCount);
15677
+ if (tiers.length === 0) {
15678
+ return 0;
15679
+ }
15680
+ let total = 0;
15681
+ let remaining = durationUnits;
15682
+ for (const tier of tiers) {
15683
+ const count = Math.floor(remaining / tier.unitCount);
15684
+ if (count <= 0)
15685
+ continue;
15686
+ total += count * tier.price;
15687
+ remaining -= count * tier.unitCount;
15688
+ }
15689
+ if (remaining > 0) {
15690
+ if (typeof config.perUnitPrice === "number") {
15691
+ total += remaining * config.perUnitPrice;
15692
+ }
15693
+ else {
15694
+ total += tiers[tiers.length - 1]?.price ?? 0;
15695
+ }
15696
+ }
15697
+ return total;
15698
+ }
15699
+ function FreeformSelection({ config, eventType, systemConfig, isOpen, onClose, onSuccess, onError, }) {
15700
+ const t = useTranslations();
15701
+ const { locale } = useLocale();
15702
+ const freeformConfig = eventType.freeformConfig;
15703
+ const timeZone = eventType.organizationTimezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
15704
+ const isNamedResource = eventType.resourceKind === "named";
15705
+ const maxUnits = isNamedResource ? 1 : Math.max(1, eventType.resourceQuantity ?? 1);
15706
+ const now = useMemo(() => {
15707
+ const next = new Date();
15708
+ next.setMinutes(next.getMinutes() + 30, 0, 0);
15709
+ return next;
15710
+ }, []);
15711
+ const [startInput, setStartInput] = useState(utcToZonedInputValue(now, timeZone));
15712
+ const [duration, setDuration] = useState(Math.max(1, freeformConfig?.minDuration ?? 1));
15713
+ const [requestedUnits, setRequestedUnits] = useState(1);
15714
+ const [customerName, setCustomerName] = useState("");
15715
+ const [customerEmail, setCustomerEmail] = useState("");
15716
+ const [customerPhone, setCustomerPhone] = useState("");
15717
+ const [comment, setComment] = useState("");
15718
+ const [acceptTerms, setAcceptTerms] = useState(false);
15719
+ const [checkoutStep, setCheckoutStep] = useState("details");
15720
+ const [availability, setAvailability] = useState(null);
15721
+ const [availabilityError, setAvailabilityError] = useState(null);
15722
+ const [isCheckingAvailability, setIsCheckingAvailability] = useState(false);
15723
+ useEffect(() => {
15724
+ if (!isOpen)
15725
+ return;
15726
+ setCheckoutStep("details");
15727
+ }, [isOpen]);
15728
+ useEffect(() => {
15729
+ if (!freeformConfig)
15730
+ return;
15731
+ setDuration(Math.max(1, freeformConfig.minDuration));
15732
+ setRequestedUnits(1);
15733
+ setAvailability(null);
15734
+ setAvailabilityError(null);
15735
+ }, [freeformConfig?.resourceId]);
15736
+ const unitMinutes = toMinutesForUnit(freeformConfig?.unit ?? "hour");
15737
+ const startDate = useMemo(() => zonedWallTimeToUtc(startInput, timeZone), [startInput, timeZone]);
15738
+ const endDate = useMemo(() => new Date(startDate.getTime() + duration * unitMinutes * 60000), [duration, startDate, unitMinutes]);
15739
+ useEffect(() => {
15740
+ if (!isOpen || !freeformConfig)
15741
+ return;
15742
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
15743
+ return;
15744
+ const timer = setTimeout(async () => {
15745
+ setIsCheckingAvailability(true);
15746
+ setAvailabilityError(null);
15747
+ try {
15748
+ const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/freeform-availability"), {
15749
+ method: "POST",
15750
+ headers: createApiHeaders(config, locale),
15751
+ body: JSON.stringify(createRequestBody(config, {
15752
+ eventTypeId: eventType.id,
15753
+ requestedUnits,
15754
+ start: startDate.toISOString(),
15755
+ end: endDate.toISOString(),
15756
+ })),
15757
+ });
15758
+ const data = await response.json();
15759
+ if (!response.ok) {
15760
+ setAvailability(null);
15761
+ setAvailabilityError(data.error || t("error.loadBookingData"));
15762
+ return;
15763
+ }
15764
+ setAvailability({
15765
+ available: Boolean(data.available),
15766
+ freeUnits: Number(data.freeUnits ?? 0),
15767
+ capacityUnits: Number(data.capacityUnits ?? 0),
15768
+ usedUnits: Number(data.usedUnits ?? 0),
15769
+ reservationUsedUnits: Number(data.reservationUsedUnits ?? 0),
15770
+ blockUsedUnits: Number(data.blockUsedUnits ?? 0),
15771
+ durationUnits: Number(data.durationUnits ?? 0),
15772
+ roundedDurationMinutes: Number(data.roundedDurationMinutes ?? 0),
15773
+ });
15774
+ }
15775
+ catch (_error) {
15776
+ setAvailability(null);
15777
+ setAvailabilityError(t("error.loadBookingData"));
15778
+ }
15779
+ finally {
15780
+ setIsCheckingAvailability(false);
15781
+ }
15782
+ }, 350);
15783
+ return () => clearTimeout(timer);
15784
+ }, [
15785
+ config,
15786
+ endDate,
15787
+ eventType.id,
15788
+ freeformConfig,
15789
+ isOpen,
15790
+ locale,
15791
+ requestedUnits,
15792
+ startDate,
15793
+ t,
15794
+ ]);
15795
+ const billedDurationUnits = availability?.durationUnits ?? Math.max(1, Math.ceil(duration / Math.max(1, freeformConfig?.stepDuration ?? 1)));
15796
+ const basePrice = useMemo(() => {
15797
+ if (!freeformConfig)
15798
+ return 0;
15799
+ if (freeformConfig.pricingModel === "perUnit") {
15800
+ return (freeformConfig.perUnitPrice ?? 0) * billedDurationUnits;
15801
+ }
15802
+ return calculateTieredPrice(billedDurationUnits, freeformConfig);
15803
+ }, [billedDurationUnits, freeformConfig]);
15804
+ const totalPrice = Math.max(0, basePrice * requestedUnits);
15805
+ const canContinueToPayment = !!freeformConfig &&
15806
+ customerName.trim().length >= 2 &&
15807
+ customerEmail.trim().length > 3 &&
15808
+ acceptTerms &&
15809
+ !!availability?.available &&
15810
+ !isCheckingAvailability;
15811
+ const freeformSelection = useMemo(() => ({
15812
+ eventTypeId: eventType.id,
15813
+ start: startDate.toISOString(),
15814
+ end: endDate.toISOString(),
15815
+ requestedUnits,
15816
+ customerName: customerName.trim(),
15817
+ customerEmail: customerEmail.trim(),
15818
+ customerPhone: customerPhone.trim(),
15819
+ comment: comment.trim(),
15820
+ }), [
15821
+ comment,
15822
+ customerEmail,
15823
+ customerName,
15824
+ customerPhone,
15825
+ endDate,
15826
+ eventType.id,
15827
+ requestedUnits,
15828
+ startDate,
15829
+ ]);
15830
+ const paymentFormData = useMemo(() => ({
15831
+ customerName: customerName.trim(),
15832
+ customerEmail: customerEmail.trim(),
15833
+ customerPhone: customerPhone.trim(),
15834
+ participants: [{ name: customerName.trim() || customerEmail.trim(), level: undefined }],
15835
+ comment: comment.trim(),
15836
+ }), [comment, customerEmail, customerName, customerPhone]);
15837
+ const eventDetailsForPayment = useMemo(() => ({
15838
+ id: eventType.id,
15839
+ name: eventType.name,
15840
+ startTime: startDate.toISOString(),
15841
+ endTime: endDate.toISOString(),
15842
+ price: basePrice,
15843
+ maxParticipants: availability?.capacityUnits ?? requestedUnits,
15844
+ participantCount: 0,
15845
+ availableSpots: availability?.freeUnits ?? requestedUnits,
15846
+ durationDays: 1,
15847
+ durationPerDay: availability?.roundedDurationMinutes ?? duration * unitMinutes,
15848
+ images: eventType.images ?? [],
15849
+ category: eventType.category,
15850
+ organization: {
15851
+ id: config.organizationId,
15852
+ name: "",
15853
+ },
15854
+ bookingOpen: availability?.available ?? false,
15855
+ }), [
15856
+ availability?.available,
15857
+ availability?.capacityUnits,
15858
+ availability?.freeUnits,
15859
+ availability?.roundedDurationMinutes,
15860
+ basePrice,
15861
+ config.organizationId,
15862
+ duration,
15863
+ endDate,
15864
+ eventType.category,
15865
+ eventType.id,
15866
+ eventType.images,
15867
+ eventType.name,
15868
+ requestedUnits,
15869
+ startDate,
15870
+ unitMinutes,
15871
+ ]);
15872
+ const footer = (jsxs("div", { style: { width: "100%", display: "flex", gap: "12px" }, children: [jsx("button", { type: "button", onClick: () => {
15873
+ if (checkoutStep === "payment") {
15874
+ setCheckoutStep("details");
15875
+ }
15876
+ else {
15877
+ onClose();
15878
+ }
15879
+ }, style: {
15880
+ flex: 1,
15881
+ padding: "12px 16px",
15882
+ border: "1px solid var(--bw-border-color)",
15883
+ borderRadius: "var(--bw-border-radius)",
15884
+ backgroundColor: "var(--bw-surface-color)",
15885
+ color: "var(--bw-text-color)",
15886
+ fontFamily: "var(--bw-font-family)",
15887
+ fontWeight: 600,
15888
+ cursor: "pointer",
15889
+ }, children: checkoutStep === "payment" ? t("button.backToDetails") : t("common.back") }), checkoutStep === "details" && (jsx("button", { type: "button", onClick: () => setCheckoutStep("payment"), disabled: !canContinueToPayment, style: {
15890
+ flex: 1,
15891
+ padding: "12px 16px",
15892
+ border: "none",
15893
+ borderRadius: "var(--bw-border-radius)",
15894
+ backgroundColor: "var(--bw-highlight-color)",
15895
+ color: "var(--bw-button-text-color, #ffffff)",
15896
+ fontFamily: "var(--bw-font-family)",
15897
+ fontWeight: 700,
15898
+ cursor: canContinueToPayment ? "pointer" : "not-allowed",
15899
+ opacity: canContinueToPayment ? 1 : 0.6,
15900
+ }, children: t("button.continueToPayment") }))] }));
15901
+ if (!freeformConfig) {
15902
+ return (jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: eventType.name, children: jsx("div", { style: { padding: "16px", color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: t("booking.freeformMissingConfig") }) }));
15903
+ }
15904
+ return (jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: eventType.name, footer: footer, children: jsx("div", { style: { padding: "16px", display: "flex", flexDirection: "column", gap: "16px" }, children: checkoutStep === "details" ? (jsxs(Fragment, { children: [jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("booking.eventDetails") }), jsxs("div", { style: { display: "grid", gap: "10px" }, children: [jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("booking.date"), jsx("input", { type: "datetime-local", value: startInput, onChange: (event) => setStartInput(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } })] }), jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("booking.duration"), jsx("input", { type: "number", min: freeformConfig.minDuration, max: freeformConfig.maxDuration, step: freeformConfig.stepDuration, value: duration, onChange: (event) => setDuration(Number(event.target.value)), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } })] }), !isNamedResource && (jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("voucher.quantity"), jsx("input", { type: "number", min: 1, max: maxUnits, value: requestedUnits, onChange: (event) => setRequestedUnits(Math.min(maxUnits, Math.max(1, Number(event.target.value)))), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } })] }))] }), jsxs("div", { style: { marginTop: "12px", fontSize: "13px", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: [formatInTimeZone(startDate, timeZone, locale), " - ", formatInTimeZone(endDate, timeZone, locale), eventType.organizationTimezone ? ` (${timeZone})` : ""] }), isCheckingAvailability && (jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: t("common.loading") })), availabilityError && (jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: availabilityError })), !availabilityError && availability && (jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: availability.available ? "var(--bw-success-color)" : "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: availability.available
15905
+ ? t("booking.freeformUnitsAvailable", { count: availability.freeUnits })
15906
+ : t("booking.freeformNotAvailable") }))] }), jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("booking.contactInfo") }), jsxs("div", { style: { display: "grid", gap: "10px" }, children: [jsx("input", { type: "text", placeholder: t("booking.namePlaceholder"), value: customerName, onChange: (event) => setCustomerName(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } }), jsx("input", { type: "email", placeholder: t("booking.emailPlaceholder"), value: customerEmail, onChange: (event) => setCustomerEmail(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } }), jsx("input", { type: "tel", placeholder: t("booking.phonePlaceholder"), value: customerPhone, onChange: (event) => setCustomerPhone(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } }), jsx("textarea", { placeholder: t("booking.commentPlaceholder"), value: comment, onChange: (event) => setComment(event.target.value), rows: 3, style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)", resize: "vertical" } }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontFamily: "var(--bw-font-family)", fontSize: "13px", color: "var(--bw-text-muted)" }, children: [jsx("input", { type: "checkbox", checked: acceptTerms, onChange: (event) => setAcceptTerms(event.target.checked) }), t("booking.acceptTerms"), " ", t("booking.terms")] })] })] }), jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("summary.title") }), jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "14px" }, children: [jsx("span", { children: t("booking.duration") }), jsx("span", { children: t("booking.steps", { count: billedDurationUnits }) })] }), jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "14px", marginTop: "6px" }, children: [jsx("span", { children: t("booking.price") }), jsxs("span", { children: [formatCurrency(basePrice), " x ", requestedUnits] })] }), jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-color)", fontFamily: "var(--bw-font-family)", fontSize: "20px", fontWeight: 700, marginTop: "10px" }, children: [jsx("span", { children: t("summary.totalAmount") }), jsx("span", { children: formatCurrency(totalPrice) })] })] })] })) : (jsxs(Fragment, { children: [jsx(HoldCountdown, {}), jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("summary.payment") }), !systemConfig?.paymentProvider && (jsx("p", { style: { margin: 0, color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: t("booking.paymentUnavailable") })), systemConfig?.paymentProvider === "mollie" && (jsx(MolliePaymentForm, { config: config, eventDetails: eventDetailsForPayment, formData: paymentFormData, totalAmount: totalPrice, discountCode: null, giftCards: [], onSuccess: onSuccess, onError: onError, mollieProfileId: systemConfig?.mollieProfileId, mollieTestmode: systemConfig?.mollieTestmode, freeformSelection: freeformSelection })), systemConfig?.paymentProvider === "stripe" && (jsx(StripePaymentForm, { config: config, eventDetails: eventDetailsForPayment, formData: paymentFormData, totalAmount: totalPrice, discountCode: null, giftCards: [], onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, freeformSelection: freeformSelection }))] })] })) }) }));
15907
+ }
15908
+
15176
15909
  const getThemeConfig = (theme = "generic") => {
15177
15910
  switch (theme) {
15178
15911
  case "christmas":
@@ -15655,6 +16388,7 @@ let buffer = [];
15655
16388
  let flushTimer = null;
15656
16389
  let currentApiBaseUrl = "";
15657
16390
  let currentOrganizationId = "";
16391
+ let currentPartnerContractId = null;
15658
16392
  const FLUSH_INTERVAL_MS = 3000;
15659
16393
  function flush() {
15660
16394
  if (buffer.length === 0)
@@ -15696,9 +16430,10 @@ function scheduleFlush() {
15696
16430
  /**
15697
16431
  * Initialise the analytics module. Must be called once before `trackEvent`.
15698
16432
  */
15699
- function initAnalytics(apiBaseUrl, organizationId) {
16433
+ function initAnalytics(apiBaseUrl, organizationId, options) {
15700
16434
  currentApiBaseUrl = apiBaseUrl;
15701
16435
  currentOrganizationId = organizationId;
16436
+ currentPartnerContractId = options?.partnerContractId?.trim() || null;
15702
16437
  if (typeof window !== "undefined") {
15703
16438
  window.addEventListener("pagehide", flush);
15704
16439
  window.addEventListener("visibilitychange", () => {
@@ -15715,9 +16450,12 @@ function trackEvent(event, properties = {}) {
15715
16450
  return;
15716
16451
  if (!currentOrganizationId)
15717
16452
  return;
16453
+ const mergedProperties = currentPartnerContractId
16454
+ ? { ...properties, partnerContractId: currentPartnerContractId }
16455
+ : properties;
15718
16456
  buffer.push({
15719
16457
  event,
15720
- properties,
16458
+ properties: mergedProperties,
15721
16459
  domain: window.location.hostname,
15722
16460
  url: window.location.href,
15723
16461
  referrer: document.referrer,
@@ -15793,7 +16531,6 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15793
16531
  const [sidebarOpen, setSidebarOpen] = useState(false);
15794
16532
  // Booking flow state
15795
16533
  const [eventDetails, setEventDetails] = useState(null);
15796
- const [stripePromise, setStripePromise] = useState(null);
15797
16534
  // SEPARATED LOADING STATES
15798
16535
  const [isLoading, setIsLoading] = useState(true);
15799
16536
  const [isLoadingEventInstances, setIsLoadingEventInstances] = useState(false);
@@ -15809,6 +16546,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15809
16546
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = useState(false);
15810
16547
  const [shouldRenderUpsells, setShouldRenderUpsells] = useState(false);
15811
16548
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = useState(false);
16549
+ const [shouldRenderFreeformSelection, setShouldRenderFreeformSelection] = useState(false);
15812
16550
  // Google Ads config (received from API, set once from the first API response)
15813
16551
  const [googleAdsConfig, setGoogleAdsConfig] = useState(null);
15814
16552
  const extractGoogleAdsConfig = (data) => {
@@ -15822,6 +16560,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15822
16560
  // Voucher purchase state
15823
16561
  const [voucherConfig, setVoucherConfig] = useState(null);
15824
16562
  const [voucherEventTypes, setVoucherEventTypes] = useState([]);
16563
+ const [voucherCategories, setVoucherCategories] = useState([]);
15825
16564
  const [isLoadingVoucherConfig, setIsLoadingVoucherConfig] = useState(false);
15826
16565
  const [isVoucherFormOpen, setIsVoucherFormOpen] = useState(false);
15827
16566
  const [voucherPurchaseResult, setVoucherPurchaseResult] = useState(null);
@@ -15901,6 +16640,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15901
16640
  setVoucherConfig(mergedConfig);
15902
16641
  extractGoogleAdsConfig(data);
15903
16642
  setVoucherEventTypes(data.eventTypes || []);
16643
+ setVoucherCategories(data.categories || []);
15904
16644
  // Set system config for payment processing
15905
16645
  if (data.paymentProvider) {
15906
16646
  setSystemConfig({
@@ -15928,7 +16668,9 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15928
16668
  useEffect(() => {
15929
16669
  if (!analyticsInitRef.current && config.organizationId) {
15930
16670
  analyticsInitRef.current = true;
15931
- initAnalytics(config.apiBaseUrl, config.organizationId);
16671
+ initAnalytics(config.apiBaseUrl, config.organizationId, {
16672
+ ...(config.partnerContractId ? { partnerContractId: config.partnerContractId } : {}),
16673
+ });
15932
16674
  trackEvent("widget_loaded", {
15933
16675
  viewMode,
15934
16676
  eventTypeId: config.eventTypeId,
@@ -15937,7 +16679,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15937
16679
  isStandaloneVoucherMode,
15938
16680
  });
15939
16681
  }
15940
- }, [config.organizationId, config.apiBaseUrl]);
16682
+ }, [config.organizationId, config.apiBaseUrl, config.partnerContractId, viewMode, config.eventTypeId, config.categoryId, config.eventInstanceId, isStandaloneVoucherMode]);
15941
16683
  // Fire widget pageview once when Google Ads config is received from API
15942
16684
  const pageviewFiredRef = useRef(false);
15943
16685
  useEffect(() => {
@@ -15951,6 +16693,23 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15951
16693
  const initializeWidget = async () => {
15952
16694
  try {
15953
16695
  setIsLoading(true);
16696
+ if (config.partnerContractId) {
16697
+ const validationResponse = await fetch(getApiUrl(config.apiBaseUrl, `/booking/validate-partner?organizationId=${encodeURIComponent(config.organizationId)}&contractId=${config.partnerContractId}`), {
16698
+ method: "GET",
16699
+ headers: createApiHeaders(config, locale),
16700
+ });
16701
+ if (!validationResponse.ok) {
16702
+ setError("Invalid partner configuration");
16703
+ config.onError?.("Invalid partner configuration");
16704
+ return;
16705
+ }
16706
+ const validationData = (await validationResponse.json());
16707
+ if (!validationData.valid) {
16708
+ setError("Invalid partner configuration");
16709
+ config.onError?.("Invalid partner configuration");
16710
+ return;
16711
+ }
16712
+ }
15954
16713
  // Load voucher config in parallel if voucher mode is requested or there's event selection
15955
16714
  if (isVoucherModeRequested || hasEventSelection) {
15956
16715
  void loadVoucherConfig();
@@ -16086,11 +16845,18 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16086
16845
  setVoucherPurchaseResult(voucherData.voucherResult);
16087
16846
  setIsSuccess(true);
16088
16847
  setSuccessPaymentId(null);
16848
+ return;
16089
16849
  }
16090
16850
  }
16091
16851
  catch {
16092
- // Ignore lookup errors; webhook eventual consistency may delay data.
16093
- }
16852
+ // Fall back to booking success flow if voucher lookup fails.
16853
+ }
16854
+ // Not a voucher purchase: treat as a booking return and open the
16855
+ // booking confirmation, mirroring the Stripe redirect handler.
16856
+ trackEvent("booking_completed", { paymentIntentId: mollieReturn.molliePaymentId, source: "mollie_redirect" });
16857
+ setVoucherPurchaseResult(null);
16858
+ setSuccessPaymentId(mollieReturn.molliePaymentId);
16859
+ setIsSuccess(true);
16094
16860
  };
16095
16861
  if (!isLoading) {
16096
16862
  void handleMollieVoucherReturn();
@@ -16279,27 +17045,6 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16279
17045
  widgetPaymentMode: data.widgetPaymentMode ?? "all",
16280
17046
  });
16281
17047
  }
16282
- if (data.stripePublishableKey) {
16283
- const stripeOptions = {
16284
- locale: locale,
16285
- };
16286
- if (data.connectedAccountId) {
16287
- stripeOptions.stripeAccount = data.connectedAccountId;
16288
- }
16289
- if (typeof window !== "undefined" && window.document) {
16290
- try {
16291
- void (window.parent === window || window.parent.location.href);
16292
- if (!stripeOptions.betas) {
16293
- stripeOptions.betas = [];
16294
- }
16295
- }
16296
- catch {
16297
- console.warn("[WIDGET] Detected restricted environment, adjusting Stripe options");
16298
- stripeOptions.betas = [];
16299
- }
16300
- }
16301
- setStripePromise(loadStripe(data.stripePublishableKey, stripeOptions));
16302
- }
16303
17048
  if (!skipInstanceAutoSelectRef.current && data.eventInstances.length === 1) {
16304
17049
  setSelectedEventInstance(data.eventInstances[0]);
16305
17050
  setCurrentStep("booking");
@@ -16339,29 +17084,6 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16339
17084
  roundPricesEnabled: data.roundPricesEnabled ?? true,
16340
17085
  widgetPaymentMode: data.widgetPaymentMode ?? "all",
16341
17086
  });
16342
- if (!stripePromise && data.stripePublishableKey) {
16343
- const stripeOptions = {
16344
- locale: locale,
16345
- };
16346
- if (data.connectedAccountId) {
16347
- stripeOptions.stripeAccount = data.connectedAccountId;
16348
- }
16349
- if (typeof window !== "undefined" && window.document) {
16350
- try {
16351
- // Check if we have parent access (not in a cross-origin iframe)
16352
- void (window.parent === window || window.parent.location.href);
16353
- stripeOptions.apiVersion = "2025-02-24.acacia";
16354
- if (!stripeOptions.betas) {
16355
- stripeOptions.betas = [];
16356
- }
16357
- }
16358
- catch {
16359
- console.warn("[WIDGET] Detected restricted environment, adjusting Stripe options");
16360
- stripeOptions.betas = [];
16361
- }
16362
- }
16363
- setStripePromise(loadStripe(data.stripePublishableKey, stripeOptions));
16364
- }
16365
17087
  }
16366
17088
  else {
16367
17089
  const errorMessage = data.error || t("error.loadEventDetails");
@@ -16426,6 +17148,18 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16426
17148
  const handleEventTypeSelect = async (eventType) => {
16427
17149
  trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16428
17150
  setSelectedEventType(eventType);
17151
+ if (eventType.bookingMode === "freeform") {
17152
+ setCurrentStep("freeform");
17153
+ setShouldRenderFreeformSelection(true);
17154
+ setIsLoadingEventInstances(true);
17155
+ try {
17156
+ await loadEventInstances(eventType.id);
17157
+ }
17158
+ finally {
17159
+ setIsLoadingEventInstances(false);
17160
+ }
17161
+ return;
17162
+ }
16429
17163
  setCurrentStep("eventInstances");
16430
17164
  setShouldRenderInstanceSelection(true);
16431
17165
  setIsLoadingEventInstances(true);
@@ -16516,6 +17250,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16516
17250
  setSuccessPaymentId(result.paymentIntent.id);
16517
17251
  setSidebarOpen(false);
16518
17252
  setShouldRenderBookingForm(false);
17253
+ setShouldRenderFreeformSelection(false);
16519
17254
  setBookingPersistedState(null);
16520
17255
  config.onSuccess?.(result);
16521
17256
  };
@@ -16630,12 +17365,12 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16630
17365
  startTime: cardPreviewItem.startTime,
16631
17366
  endTime: cardPreviewItem.startTime,
16632
17367
  price: cardPreviewItem.price,
16633
- maxParticipants: cardPreviewItem.availableSpots + 1,
16634
- participantCount: 1,
17368
+ maxParticipants: Math.max(cardPreviewItem.availableSpots, 0),
17369
+ participantCount: 0,
16635
17370
  availableSpots: cardPreviewItem.availableSpots,
16636
17371
  durationDays: 1,
16637
17372
  durationPerDay: 1,
16638
- bookingOpen: true,
17373
+ bookingOpen: cardPreviewItem.bookingOpen && cardPreviewItem.availableSpots > 0,
16639
17374
  }
16640
17375
  : null;
16641
17376
  if (eventInstance) {
@@ -16823,7 +17558,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16823
17558
  }
16824
17559
  // Main view based on view mode
16825
17560
  if (viewMode === "next-events" && showingPreview) {
16826
- return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [jsx(NextEventsPreview, { events: upcomingEvents, onEventSelect: handleUpcomingEventSelect, onShowAll: handleShowAllEvents, showAllButtonText: nextEventsSettings.showAllButtonText, showAllButton: nextEventsSettings.showAllButton, isLoadingEventDetails: isLoadingEventDetails, isLoadingShowAll: isLoadingShowAll, isLoading: isLoading, count: nextEventsSettings.count }), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17561
+ return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [jsx(NextEventsPreview, { events: upcomingEvents, onEventSelect: handleUpcomingEventSelect, onShowAll: handleShowAllEvents, showAllButtonText: nextEventsSettings.showAllButtonText, showAllButton: nextEventsSettings.showAllButton, isLoadingEventDetails: isLoadingEventDetails, isLoadingShowAll: isLoadingShowAll, isLoading: isLoading, count: nextEventsSettings.count }), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
16827
17562
  setIsSuccess(false);
16828
17563
  setCurrentStep("eventTypes");
16829
17564
  setShowingPreview(true);
@@ -16831,6 +17566,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16831
17566
  setShouldRenderInstanceSelection(false);
16832
17567
  setShouldRenderUpsells(false);
16833
17568
  setShouldRenderBookingForm(false);
17569
+ setShouldRenderFreeformSelection(false);
16834
17570
  setSelectedUpsells([]);
16835
17571
  setUpsells([]);
16836
17572
  setBookingPersistedState(null);
@@ -16844,7 +17580,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16844
17580
  }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16845
17581
  }
16846
17582
  if (viewMode === "specials" && showingPreview) {
16847
- return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [jsx(SpecialsView, { specials: specials, onEventSelect: handleUpcomingEventSelect, isLoading: isLoadingSpecials, showSavingsAmount: config.specialsSettings?.showSavingsAmount ?? true, showSavingsPercent: config.specialsSettings?.showSavingsPercent ?? false, emptyStateText: config.specialsSettings?.emptyStateText }), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17583
+ return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [jsx(SpecialsView, { specials: specials, onEventSelect: handleUpcomingEventSelect, isLoading: isLoadingSpecials, showSavingsAmount: config.specialsSettings?.showSavingsAmount ?? true, showSavingsPercent: config.specialsSettings?.showSavingsPercent ?? false, emptyStateText: config.specialsSettings?.emptyStateText }), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
16848
17584
  setIsSuccess(false);
16849
17585
  setCurrentStep("eventTypes");
16850
17586
  setShowingPreview(true);
@@ -16852,6 +17588,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16852
17588
  setShouldRenderInstanceSelection(false);
16853
17589
  setShouldRenderUpsells(false);
16854
17590
  setShouldRenderBookingForm(false);
17591
+ setShouldRenderFreeformSelection(false);
16855
17592
  setSelectedUpsells([]);
16856
17593
  setUpsells([]);
16857
17594
  setBookingPersistedState(null);
@@ -16864,13 +17601,14 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16864
17601
  }, isOpen: currentStep === "eventInstances", onClose: () => {
16865
17602
  setShowingPreview(true);
16866
17603
  setCurrentStep("eventTypes");
16867
- }, isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17604
+ }, isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
16868
17605
  setIsSuccess(false);
16869
17606
  setCurrentStep("eventTypes");
16870
17607
  setShowingPreview(true);
16871
17608
  setSuccessPaymentId(null);
16872
17609
  setShouldRenderInstanceSelection(false);
16873
17610
  setShouldRenderBookingForm(false);
17611
+ setShouldRenderFreeformSelection(false);
16874
17612
  const url = new URL(window.location.href);
16875
17613
  url.searchParams.delete("payment_intent");
16876
17614
  url.searchParams.delete("payment_intent_client_secret");
@@ -16914,13 +17652,23 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16914
17652
  setCurrentStep("booking");
16915
17653
  setShouldRenderBookingForm(true);
16916
17654
  }
17655
+ else if (selectedEventType?.bookingMode === "freeform") {
17656
+ setCurrentStep("freeform");
17657
+ setShouldRenderFreeformSelection(true);
17658
+ }
16917
17659
  else {
16918
17660
  setCurrentStep("eventInstances");
16919
17661
  setSidebarOpen(true);
16920
17662
  setShouldRenderInstanceSelection(true);
16921
17663
  }
16922
17664
  }, children: [isButtonBusy && jsx(Spinner, { size: 20, borderColor: "var(--bw-button-text-color, #ffffff)" }), config.buttonText ||
16923
- (isDirectInstanceMode ? t("button.bookNow") : t("button.viewDates"))] }), shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => setSidebarOpen(false), isOpen: sidebarOpen && currentStep === "eventInstances", onClose: () => setSidebarOpen(false), isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17665
+ (isDirectInstanceMode || selectedEventType?.bookingMode === "freeform"
17666
+ ? t("button.bookNow")
17667
+ : t("button.viewDates"))] }), shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => setSidebarOpen(false), isOpen: sidebarOpen && currentStep === "eventInstances", onClose: () => setSidebarOpen(false), isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), shouldRenderFreeformSelection && selectedEventType?.bookingMode === "freeform" && (jsx(FreeformSelection, { config: config, eventType: selectedEventType, isOpen: currentStep === "freeform", onClose: () => {
17668
+ setCurrentStep("eventTypes");
17669
+ setSidebarOpen(false);
17670
+ setShouldRenderFreeformSelection(false);
17671
+ }, onSuccess: handleBookingSuccess, onError: handleBookingError, systemConfig: systemConfig })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
16924
17672
  setIsSuccess(false);
16925
17673
  setCurrentStep("eventTypes");
16926
17674
  setSidebarOpen(false);
@@ -16928,6 +17676,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16928
17676
  setShouldRenderInstanceSelection(false);
16929
17677
  setShouldRenderUpsells(false);
16930
17678
  setShouldRenderBookingForm(false);
17679
+ setShouldRenderFreeformSelection(false);
16931
17680
  setSelectedUpsells([]);
16932
17681
  setUpsells([]);
16933
17682
  const url = new URL(window.location.href);
@@ -16940,7 +17689,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16940
17689
  }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16941
17690
  }
16942
17691
  // Cards mode (default) - show event type selection with optional voucher card
16943
- const cardsView = (jsxs(Fragment, { children: [hasEventSelection && (jsx(EventTypeSelection, { eventTypes: eventTypes, onEventTypeSelect: handleEventTypeSelect, onInstancePreview: (instanceId, eventTypeId) => void handleUpcomingEventSelect(instanceId, eventTypeId), isLoading: isLoading, skeletonCount: getSkeletonCount(), showVoucherAttachment: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled && !isStandaloneVoucherMode), onVoucherClick: handleVoucherAttachmentClick })), isStandaloneVoucherMode && (jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: false, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: null, voucherPurchaseResult: null, isSuccess: false, showStandaloneCard: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled), onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => { } })), isStandaloneVoucherMode && isLoading && !voucherConfig && (jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsx("div", { style: {
17692
+ const cardsView = (jsxs(Fragment, { children: [hasEventSelection && (jsx(EventTypeSelection, { eventTypes: eventTypes, onEventTypeSelect: handleEventTypeSelect, onInstancePreview: (instanceId, eventTypeId) => void handleUpcomingEventSelect(instanceId, eventTypeId), isLoading: isLoading, skeletonCount: getSkeletonCount(), showVoucherAttachment: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled && !isStandaloneVoucherMode), onVoucherClick: handleVoucherAttachmentClick })), isStandaloneVoucherMode && (jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, categories: voucherCategories, systemConfig: systemConfig, isFormOpen: false, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: null, voucherPurchaseResult: null, isSuccess: false, showStandaloneCard: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled), onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => { } })), isStandaloneVoucherMode && isLoading && !voucherConfig && (jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsx("div", { style: {
16944
17693
  display: "inline-block",
16945
17694
  width: "32px",
16946
17695
  height: "32px",
@@ -16981,13 +17730,17 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16981
17730
  };
16982
17731
  };
16983
17732
  const backHandlers = getBackHandlers();
16984
- return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [cardsView, shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: handleBackToEventTypes, isOpen: currentStep === "eventInstances", onClose: handleBackToEventTypes, isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: backHandlers.onClose, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess && !voucherPurchaseResult, onClose: () => {
17733
+ return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [cardsView, shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: handleBackToEventTypes, isOpen: currentStep === "eventInstances", onClose: handleBackToEventTypes, isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: backHandlers.onClose, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), shouldRenderFreeformSelection && selectedEventType?.bookingMode === "freeform" && (jsx(FreeformSelection, { config: config, eventType: selectedEventType, isOpen: currentStep === "freeform", onClose: () => {
17734
+ setCurrentStep("eventTypes");
17735
+ setShouldRenderFreeformSelection(false);
17736
+ }, onSuccess: handleBookingSuccess, onError: handleBookingError, systemConfig: systemConfig })), jsx(BookingSuccessModal, { isOpen: isSuccess && !voucherPurchaseResult, onClose: () => {
16985
17737
  setIsSuccess(false);
16986
17738
  setCurrentStep("eventTypes");
16987
17739
  setSuccessPaymentId(null);
16988
17740
  setShouldRenderInstanceSelection(false);
16989
17741
  setShouldRenderUpsells(false);
16990
17742
  setShouldRenderBookingForm(false);
17743
+ setShouldRenderFreeformSelection(false);
16991
17744
  setSelectedUpsells([]);
16992
17745
  setUpsells([]);
16993
17746
  const url = new URL(window.location.href);
@@ -16997,7 +17750,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16997
17750
  url.searchParams.delete("mollie_payment_id");
16998
17751
  url.searchParams.delete("mollie_status");
16999
17752
  window.history.replaceState({}, "", url.toString());
17000
- }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: isVoucherFormOpen, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: preselectedVoucherEventTypeId, voucherPurchaseResult: voucherPurchaseResult, isSuccess: isSuccess, showStandaloneCard: false, onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => {
17753
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, categories: voucherCategories, systemConfig: systemConfig, isFormOpen: isVoucherFormOpen, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: preselectedVoucherEventTypeId, voucherPurchaseResult: voucherPurchaseResult, isSuccess: isSuccess, showStandaloneCard: false, onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => {
17001
17754
  setIsSuccess(false);
17002
17755
  setVoucherPurchaseResult(null);
17003
17756
  const url = new URL(window.location.href);
@@ -17045,7 +17798,7 @@ function styleInject(css, ref) {
17045
17798
  }
17046
17799
  }
17047
17800
 
17048
- var css_248z = ".booking-widget-container{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box;color:var(--bw-text-color,#1e293b);direction:ltr;display:block;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;position:relative;text-align:left}.booking-widget-container *,.booking-widget-container :after,.booking-widget-container :before{box-sizing:border-box;margin:0;padding:0}.booking-widget-container input,.booking-widget-container select,.booking-widget-container textarea{font-family:inherit;font-size:inherit;line-height:inherit}.booking-widget-container button{background:none;border:none;cursor:pointer;font-family:inherit;font-size:inherit}.booking-widget-container a{color:inherit;text-decoration:none}.booking-widget-container img{display:block;height:auto;max-width:100%;vertical-align:middle}.booking-widget-container ol,.booking-widget-container ul{list-style:none}.booking-widget-container h1,.booking-widget-container h2,.booking-widget-container h3,.booking-widget-container h4,.booking-widget-container h5,.booking-widget-container h6{font-size:inherit;font-weight:inherit}#booking-widget-portal{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);direction:ltr;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;text-align:left}#booking-widget-portal *,#booking-widget-portal :after,#booking-widget-portal :before{box-sizing:border-box}#booking-widget-portal-root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);line-height:1.5}:root{--bw-highlight-color:#00b1aa;--bw-highlight-color-rgb:0,177,170;--bw-background-color:#f8fdfe;--bw-surface-color:#fff;--bw-text-color:#0e7490;--bw-text-muted:rgba(14,116,144,.7);--bw-border-color:#bae6fd;--bw-success-color:#38bdf8;--bw-warning-color:#fbbf24;--bw-error-color:#f43f5e;--bw-border-radius:18px;--bw-border-radius-small:calc(var(--bw-border-radius)*0.8);--bw-spacing:16px;--bw-spacing-large:24px;--bw-font-family:\"Inter\",system-ui,sans-serif;--bw-font-size:14px;--bw-font-size-large:18px;--bw-font-size-small:12px;--bw-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);--bw-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);--bw-highlight-muted:rgba(0,177,170,.1);--bw-highlight-subtle:rgba(0,177,170,.05);--bw-text-subtle:rgba(14,116,144,.4)}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes shimmer{0%{transform:translateX(-100%)}to{transform:translateX(100%)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes slide-in-right{0%{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(100%)}}@keyframes slide-in-up{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-spin{animation:spin 1s linear infinite}.animate-shimmer{animation:shimmer 2s infinite}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-fade-in{animation:fade-in .2s ease-out}.animate-slide-in-up{animation:slide-in-up .3s ease-out}.animate-scale-in{animation:scale-in .2s ease-out}.skeleton-shimmer{overflow:hidden;position:relative}.skeleton-shimmer:after{animation:shimmer 1.5s infinite;background:linear-gradient(90deg,transparent,hsla(0,0%,100%,.3),transparent);content:\"\";height:100%;left:0;position:absolute;top:0;width:100%}.bw-btn{letter-spacing:var(--bw-letter-spacing,normal);text-transform:var(--bw-text-transform,none);transition:all .2s ease!important}.bw-btn:hover:not(:disabled):not([disabled]){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.bw-btn:active:not(:disabled):not([disabled]){box-shadow:0 2px 4px rgba(0,0,0,.1)}.bw-btn-primary:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);filter:brightness(1.1)}.bw-btn-secondary:hover:not(:disabled):not([disabled]){background-color:var(--bw-surface-color);border-color:var(--bw-highlight-color);color:var(--bw-highlight-color)}.bw-btn-ghost:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-muted)}.bw-btn-outline:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);color:var(--bw-button-text-color,#fff)}.bw-btn:disabled,.bw-btn[disabled]{cursor:not-allowed!important;opacity:.5!important}button[class*=bw-btn],button[style*=transition]{transition:all .2s ease!important}button[data-variant=primary]:hover:not(:disabled),button[style*=\"--bw-highlight-color\"]:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);filter:brightness(1.1);transform:translateY(-1px)}button[data-variant=secondary]:hover:not(:disabled){border-color:var(--bw-highlight-color)!important;color:var(--bw-highlight-color)!important}button[data-variant=ghost]:hover:not(:disabled){background-color:var(--bw-highlight-muted)!important}button[data-variant=outline]:hover:not(:disabled){background-color:var(--bw-highlight-color)!important;color:var(--bw-button-text-color,#fff)!important}.bw-button-hover:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.bw-button-hover:active:not(:disabled){box-shadow:0 2px 4px rgba(0,0,0,.1);transform:translateY(0)}@media (max-width:768px){.sidebar-mobile{border-radius:0!important;max-width:100%!important;width:100%!important}}@media (max-width:600px){.event-type-list{gap:12px!important;padding:8px!important}.event-type-card{flex:1 1 100%!important;max-width:100%!important;padding:0!important}.event-type-img{height:160px!important}.event-type-title{font-size:1.1rem!important}.event-type-desc{font-size:.8rem!important;max-height:100px!important;min-height:100px!important}.event-type-content{padding:16px 24px!important}}.event-type-markdown{overflow:visible!important}.event-type-markdown p{color:var(--bw-text-muted);font-family:var(--bw-font-family);line-height:1.6;margin:0 0 8px}.event-type-markdown p:last-child{margin-bottom:0}.event-type-markdown h2{font-size:18px!important;font-weight:700!important;margin:12px 0 6px!important}.event-type-markdown h2,.event-type-markdown h3{color:var(--bw-text-color)!important;line-height:1.3!important}.event-type-markdown h3{font-size:16px!important;font-weight:600!important;margin:10px 0 4px!important}.event-type-markdown strong{color:var(--bw-text-color);font-weight:600}.event-type-markdown em{font-style:italic}.event-type-markdown u{text-decoration:underline}.event-type-markdown ul{list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ul li{color:var(--bw-text-muted)!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ul li:before{color:var(--bw-text-color)!important;content:\"•\"!important;font-weight:700!important;left:-16px!important;position:absolute!important;top:0!important}.event-type-markdown ol{counter-reset:list-counter!important;list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ol li{color:var(--bw-text-muted)!important;counter-increment:list-counter!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ol li:before{color:var(--bw-text-color)!important;content:counter(list-counter) \".\"!important;font-weight:700!important;left:-20px!important;position:absolute!important;top:0!important}.event-type-markdown blockquote{border-left:2px solid var(--bw-border-color);color:var(--bw-text-muted);font-style:italic;margin:4px 0;padding-left:12px}.event-type-markdown a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{color:var(--bw-text-color);font-weight:600;margin-bottom:.5em}.markdown-content h1{font-size:1.5em}.markdown-content h2{font-size:1.25em}.markdown-content h3{font-size:1.1em}.markdown-content p{line-height:1.6;margin-bottom:1em}.markdown-content ol,.markdown-content ul{margin-bottom:1em;padding-left:1.5em}.markdown-content ul{list-style-type:disc}.markdown-content ol{list-style-type:decimal}.markdown-content li{margin-bottom:.25em}.markdown-content a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content a:hover{opacity:.8}.markdown-content strong{font-weight:600}.markdown-content em{font-style:italic}.markdown-content code{background:var(--bw-highlight-subtle);border-radius:4px;font-family:monospace;font-size:.9em;padding:.125em .25em}.markdown-content blockquote{border-left:3px solid var(--bw-highlight-color);color:var(--bw-text-muted);margin:1em 0;padding-left:1em}.print-only{display:none}.print-hidden{display:block}@media print{.print-only{display:block}.print-hidden{display:none!important}.print-booking-header{border-bottom:2px solid #000;display:block;margin-bottom:24px;padding-bottom:16px;text-align:center}.print-booking-header h1{font-size:24px;margin:0 0 8px}.print-booking-header .subtitle{color:#666;font-size:14px}.print-booking-card{border:1px solid #ccc;border-radius:8px;margin-bottom:16px;padding:16px;page-break-inside:avoid}.print-section-title{border-bottom:1px solid #ddd;display:block;font-size:16px;font-weight:600;margin-bottom:12px;padding-bottom:8px}.print-detail-grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.print-detail-item{margin-bottom:8px}.print-detail-label{color:#666;font-size:12px;margin-bottom:4px}.print-detail-value{font-size:14px;font-weight:600}.print-status-badge{border-radius:9999px;display:inline-block;font-size:12px;font-weight:600;padding:4px 12px}.print-status-paid{background-color:#dcfce7;color:#166534;display:inline-block}.print-participant{align-items:center;background-color:#f9fafb;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:8px;padding:8px}.print-participant-name{font-weight:600}.print-participant-age{color:#666;font-size:12px}.print-payment-summary{display:block}.print-payment-row{border-bottom:1px solid #eee;display:flex;justify-content:space-between;padding:4px 0}.print-payment-row:last-child{border-bottom:none;font-weight:600}.print-footer{border-top:1px solid #ddd;color:#666;display:block;font-size:12px;margin-top:24px;padding-top:16px;text-align:center}.print-footer p{margin:4px 0}}";
17801
+ var css_248z = ".booking-widget-container{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box;color:var(--bw-text-color,#1e293b);direction:ltr;display:block;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;position:relative;text-align:left}.booking-widget-container *,.booking-widget-container :after,.booking-widget-container :before{box-sizing:border-box;margin:0;padding:0}.booking-widget-container input,.booking-widget-container select,.booking-widget-container textarea{font-family:inherit;font-size:inherit;line-height:inherit}.booking-widget-container button{background:none;border:none;cursor:pointer;font-family:inherit;font-size:inherit}.booking-widget-container a{color:inherit;text-decoration:none}.booking-widget-container img{display:block;height:auto;max-width:100%;vertical-align:middle}.booking-widget-container ol,.booking-widget-container ul{list-style:none}.booking-widget-container h1,.booking-widget-container h2,.booking-widget-container h3,.booking-widget-container h4,.booking-widget-container h5,.booking-widget-container h6{font-size:inherit;font-weight:inherit}#booking-widget-portal{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);direction:ltr;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;text-align:left}#booking-widget-portal *,#booking-widget-portal :after,#booking-widget-portal :before{box-sizing:border-box}#booking-widget-portal-root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);line-height:1.5}:root{--bw-highlight-color:#00b1aa;--bw-highlight-color-rgb:0,177,170;--bw-background-color:#f8fdfe;--bw-surface-color:#fff;--bw-text-color:#0e7490;--bw-text-muted:rgba(14,116,144,.7);--bw-border-color:#bae6fd;--bw-success-color:#38bdf8;--bw-warning-color:#fbbf24;--bw-error-color:#f43f5e;--bw-border-radius:18px;--bw-border-radius-small:calc(var(--bw-border-radius)*0.8);--bw-spacing:16px;--bw-spacing-large:24px;--bw-font-family:\"Inter\",system-ui,sans-serif;--bw-font-size:14px;--bw-font-size-large:18px;--bw-font-size-small:12px;--bw-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);--bw-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);--bw-highlight-muted:rgba(0,177,170,.1);--bw-highlight-subtle:rgba(0,177,170,.05);--bw-text-subtle:rgba(14,116,144,.4)}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes shimmer{0%{transform:translateX(-100%)}to{transform:translateX(100%)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes slide-in-right{0%{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(100%)}}@keyframes slide-in-up{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-spin{animation:spin 1s linear infinite}.animate-shimmer{animation:shimmer 2s infinite}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-fade-in{animation:fade-in .2s ease-out}.animate-slide-in-up{animation:slide-in-up .3s ease-out}.animate-scale-in{animation:scale-in .2s ease-out}.skeleton-shimmer{overflow:hidden;position:relative}.skeleton-shimmer:after{animation:shimmer 1.5s infinite;background:linear-gradient(90deg,transparent,hsla(0,0%,100%,.3),transparent);content:\"\";height:100%;left:0;position:absolute;top:0;width:100%}.booking-widget-container .bw-btn{letter-spacing:var(--bw-letter-spacing,normal);text-transform:var(--bw-text-transform,none);transition:all .2s ease!important}.booking-widget-container .bw-btn:hover:not(:disabled):not([disabled]){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.booking-widget-container .bw-btn:active:not(:disabled):not([disabled]){box-shadow:0 2px 4px rgba(0,0,0,.1)}.booking-widget-container .bw-btn-primary:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);filter:brightness(1.1)}.booking-widget-container .bw-btn-secondary:hover:not(:disabled):not([disabled]){background-color:var(--bw-surface-color);border-color:var(--bw-highlight-color);color:var(--bw-highlight-color)}.booking-widget-container .bw-btn-ghost:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-muted)}.booking-widget-container .bw-btn-outline:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);color:var(--bw-button-text-color,#fff)}.booking-widget-container .bw-btn:disabled,.booking-widget-container .bw-btn[disabled]{cursor:not-allowed!important;opacity:.5!important}.booking-widget-container button[class*=bw-btn],.booking-widget-container button[style*=transition]{transition:all .2s ease!important}.booking-widget-container button[data-variant=primary]:hover:not(:disabled),.booking-widget-container button[style*=\"--bw-highlight-color\"]:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);filter:brightness(1.1);transform:translateY(-1px)}.booking-widget-container button[data-variant=secondary]:hover:not(:disabled){border-color:var(--bw-highlight-color)!important;color:var(--bw-highlight-color)!important}.booking-widget-container button[data-variant=ghost]:hover:not(:disabled){background-color:var(--bw-highlight-muted)!important}.booking-widget-container button[data-variant=outline]:hover:not(:disabled){background-color:var(--bw-highlight-color)!important;color:var(--bw-button-text-color,#fff)!important}.booking-widget-container .bw-button-hover:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.booking-widget-container .bw-button-hover:active:not(:disabled){box-shadow:0 2px 4px rgba(0,0,0,.1);transform:translateY(0)}@media (max-width:768px){.sidebar-mobile{border-radius:0!important;max-width:100%!important;width:100%!important}}@media (max-width:600px){.event-type-list{gap:12px!important;padding:8px!important}.event-type-card{flex:1 1 100%!important;max-width:100%!important;padding:0!important}.event-type-img{height:160px!important}.event-type-title{font-size:1.1rem!important}.event-type-desc{font-size:.8rem!important;max-height:100px!important;min-height:100px!important}.event-type-content{padding:16px 24px!important}}.event-type-markdown{overflow:visible!important}.event-type-markdown p{color:var(--bw-text-muted);font-family:var(--bw-font-family);line-height:1.6;margin:0 0 8px}.event-type-markdown p:last-child{margin-bottom:0}.event-type-markdown h2{font-size:18px!important;font-weight:700!important;margin:12px 0 6px!important}.event-type-markdown h2,.event-type-markdown h3{color:var(--bw-text-color)!important;line-height:1.3!important}.event-type-markdown h3{font-size:16px!important;font-weight:600!important;margin:10px 0 4px!important}.event-type-markdown strong{color:var(--bw-text-color);font-weight:600}.event-type-markdown em{font-style:italic}.event-type-markdown u{text-decoration:underline}.event-type-markdown ul{list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ul li{color:var(--bw-text-muted)!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ul li:before{color:var(--bw-text-color)!important;content:\"•\"!important;font-weight:700!important;left:-16px!important;position:absolute!important;top:0!important}.event-type-markdown ol{counter-reset:list-counter!important;list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ol li{color:var(--bw-text-muted)!important;counter-increment:list-counter!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ol li:before{color:var(--bw-text-color)!important;content:counter(list-counter) \".\"!important;font-weight:700!important;left:-20px!important;position:absolute!important;top:0!important}.event-type-markdown blockquote{border-left:2px solid var(--bw-border-color);color:var(--bw-text-muted);font-style:italic;margin:4px 0;padding-left:12px}.event-type-markdown a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{color:var(--bw-text-color);font-weight:600;margin-bottom:.5em}.markdown-content h1{font-size:1.5em}.markdown-content h2{font-size:1.25em}.markdown-content h3{font-size:1.1em}.markdown-content p{line-height:1.6;margin-bottom:1em}.markdown-content ol,.markdown-content ul{margin-bottom:1em;padding-left:1.5em}.markdown-content ul{list-style-type:disc}.markdown-content ol{list-style-type:decimal}.markdown-content li{margin-bottom:.25em}.markdown-content a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content a:hover{opacity:.8}.markdown-content strong{font-weight:600}.markdown-content em{font-style:italic}.markdown-content code{background:var(--bw-highlight-subtle);border-radius:4px;font-family:monospace;font-size:.9em;padding:.125em .25em}.markdown-content blockquote{border-left:3px solid var(--bw-highlight-color);color:var(--bw-text-muted);margin:1em 0;padding-left:1em}.print-only{display:none}.print-hidden{display:block}@media print{.print-only{display:block}.print-hidden{display:none!important}.print-booking-header{border-bottom:2px solid #000;display:block;margin-bottom:24px;padding-bottom:16px;text-align:center}.print-booking-header h1{font-size:24px;margin:0 0 8px}.print-booking-header .subtitle{color:#666;font-size:14px}.print-booking-card{border:1px solid #ccc;border-radius:8px;margin-bottom:16px;padding:16px;page-break-inside:avoid}.print-section-title{border-bottom:1px solid #ddd;display:block;font-size:16px;font-weight:600;margin-bottom:12px;padding-bottom:8px}.print-detail-grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.print-detail-item{margin-bottom:8px}.print-detail-label{color:#666;font-size:12px;margin-bottom:4px}.print-detail-value{font-size:14px;font-weight:600}.print-status-badge{border-radius:9999px;display:inline-block;font-size:12px;font-weight:600;padding:4px 12px}.print-status-paid{background-color:#dcfce7;color:#166534;display:inline-block}.print-participant{align-items:center;background-color:#f9fafb;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:8px;padding:8px}.print-participant-name{font-weight:600}.print-participant-age{color:#666;font-size:12px}.print-payment-summary{display:block}.print-payment-row{border-bottom:1px solid #eee;display:flex;justify-content:space-between;padding:4px 0}.print-payment-row:last-child{border-bottom:none;font-weight:600}.print-footer{border-top:1px solid #ddd;color:#666;display:block;font-size:12px;margin-top:24px;padding-top:16px;text-align:center}.print-footer p{margin:4px 0}}";
17049
17802
  styleInject(css_248z);
17050
17803
 
17051
17804
  // Export init function for vanilla JS usage