@bigz-app/booking-widget 1.4.5 → 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 (32) hide show
  1. package/dist/booking-widget.js +625 -74
  2. package/dist/booking-widget.js.map +1 -1
  3. package/dist/components/UniversalBookingWidget.d.ts +18 -0
  4. package/dist/components/UniversalBookingWidget.d.ts.map +1 -1
  5. package/dist/components/booking/MolliePaymentForm.d.ts +11 -1
  6. package/dist/components/booking/MolliePaymentForm.d.ts.map +1 -1
  7. package/dist/components/booking/StripePaymentForm.d.ts +11 -1
  8. package/dist/components/booking/StripePaymentForm.d.ts.map +1 -1
  9. package/dist/components/events/EventTypeSelection.d.ts +15 -0
  10. package/dist/components/events/EventTypeSelection.d.ts.map +1 -1
  11. package/dist/components/events/FreeformSelection.d.ts +49 -0
  12. package/dist/components/events/FreeformSelection.d.ts.map +1 -0
  13. package/dist/components/events/index.d.ts +1 -0
  14. package/dist/components/events/index.d.ts.map +1 -1
  15. package/dist/components/voucher/VoucherIntegration.d.ts +3 -2
  16. package/dist/components/voucher/VoucherIntegration.d.ts.map +1 -1
  17. package/dist/components/voucher/VoucherPurchaseForm.d.ts +8 -2
  18. package/dist/components/voucher/VoucherPurchaseForm.d.ts.map +1 -1
  19. package/dist/components/voucher/index.d.ts +1 -1
  20. package/dist/components/voucher/index.d.ts.map +1 -1
  21. package/dist/components/voucher/useVoucherConfig.d.ts +6 -0
  22. package/dist/components/voucher/useVoucherConfig.d.ts.map +1 -1
  23. package/dist/i18n/locales/de.d.ts.map +1 -1
  24. package/dist/i18n/locales/en.d.ts.map +1 -1
  25. package/dist/i18n/locales/es.d.ts.map +1 -1
  26. package/dist/i18n/locales/pt.d.ts.map +1 -1
  27. package/dist/i18n/locales/sv.d.ts.map +1 -1
  28. package/dist/index.cjs +625 -74
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.esm.js +625 -74
  31. package/dist/index.esm.js.map +1 -1
  32. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -169,6 +169,10 @@ const de$1 = {
169
169
  "booking.stepUpsells": "Extras",
170
170
  "booking.stepDetails": "Angaben",
171
171
  "booking.stepPayment": "Zahlung",
172
+ "booking.steps": "{{count}} Schritt(e)",
173
+ "booking.freeformUnitsAvailable": "{{count}} Einheiten verfuegbar",
174
+ "booking.freeformNotAvailable": "Das gewaehlte Zeitfenster ist nicht verfuegbar",
175
+ "booking.freeformMissingConfig": "Die Freeform-Konfiguration fuer diesen Event-Typ fehlt.",
172
176
  // Booking summary
173
177
  "summary.title": "Buchungszusammenfassung",
174
178
  "summary.subtotal": "Zwischensumme:",
@@ -224,6 +228,13 @@ const de$1 = {
224
228
  "voucher.monetaryTypeDesc": "Beliebigen Betrag wählen",
225
229
  "voucher.eventType": "Erlebnisgutschein",
226
230
  "voucher.eventTypeDesc": "Für ein bestimmtes Erlebnis",
231
+ "voucher.categoryType": "Kategoriegutschein",
232
+ "voucher.categoryTypeDesc": "Für ein beliebiges Erlebnis einer Kategorie",
233
+ "voucher.categoryVoucher": "Kategoriegutschein",
234
+ "voucher.selectCategory": "Kategorie wählen",
235
+ "voucher.categoryLabel": "Kategorie",
236
+ "voucher.selectCategoryPlaceholder": "Kategorie auswählen...",
237
+ "voucher.categoryAmount": "Gutscheinwert",
227
238
  "voucher.selectAmount": "Betrag wählen",
228
239
  "voucher.customAmount": "Oder eigenen Betrag eingeben",
229
240
  "voucher.amountRange": "Betrag muss zwischen {{min}} und {{max}} liegen",
@@ -477,6 +488,10 @@ const en = {
477
488
  "booking.stepUpsells": "Extras",
478
489
  "booking.stepDetails": "Details",
479
490
  "booking.stepPayment": "Payment",
491
+ "booking.steps": "{{count}} step(s)",
492
+ "booking.freeformUnitsAvailable": "{{count}} units available",
493
+ "booking.freeformNotAvailable": "Selected window is not available",
494
+ "booking.freeformMissingConfig": "Missing freeform configuration for this event type.",
480
495
  // Booking summary
481
496
  "summary.title": "Booking Summary",
482
497
  "summary.subtotal": "Subtotal:",
@@ -532,6 +547,13 @@ const en = {
532
547
  "voucher.monetaryTypeDesc": "Choose any amount",
533
548
  "voucher.eventType": "Event Voucher",
534
549
  "voucher.eventTypeDesc": "For a specific experience",
550
+ "voucher.categoryType": "Category Voucher",
551
+ "voucher.categoryTypeDesc": "For any experience in a category",
552
+ "voucher.categoryVoucher": "Category Voucher",
553
+ "voucher.selectCategory": "Select Category",
554
+ "voucher.categoryLabel": "Category",
555
+ "voucher.selectCategoryPlaceholder": "Choose a category...",
556
+ "voucher.categoryAmount": "Voucher Value",
535
557
  "voucher.selectAmount": "Select Amount",
536
558
  "voucher.customAmount": "Or enter custom amount",
537
559
  "voucher.amountRange": "Amount must be between {{min}} and {{max}}",
@@ -822,6 +844,13 @@ const es = {
822
844
  "voucher.monetaryTypeDesc": "Elige cualquier cantidad",
823
845
  "voucher.eventType": "Vale de experiencia",
824
846
  "voucher.eventTypeDesc": "Para una experiencia específica",
847
+ "voucher.categoryType": "Vale de categoría",
848
+ "voucher.categoryTypeDesc": "Para cualquier experiencia de una categoría",
849
+ "voucher.categoryVoucher": "Vale de categoría",
850
+ "voucher.selectCategory": "Seleccionar categoría",
851
+ "voucher.categoryLabel": "Categoría",
852
+ "voucher.selectCategoryPlaceholder": "Elige una categoría...",
853
+ "voucher.categoryAmount": "Valor del vale",
825
854
  "voucher.selectAmount": "Selecciona el importe",
826
855
  "voucher.customAmount": "O introduce un importe personalizado",
827
856
  "voucher.amountRange": "El importe debe estar entre {{min}} y {{max}}",
@@ -1112,6 +1141,13 @@ const pt = {
1112
1141
  "voucher.monetaryTypeDesc": "Escolha qualquer quantia",
1113
1142
  "voucher.eventType": "Vale de experiência",
1114
1143
  "voucher.eventTypeDesc": "Para uma experiência específica",
1144
+ "voucher.categoryType": "Vale de categoria",
1145
+ "voucher.categoryTypeDesc": "Para qualquer experiência de uma categoria",
1146
+ "voucher.categoryVoucher": "Vale de categoria",
1147
+ "voucher.selectCategory": "Selecionar categoria",
1148
+ "voucher.categoryLabel": "Categoria",
1149
+ "voucher.selectCategoryPlaceholder": "Escolha uma categoria...",
1150
+ "voucher.categoryAmount": "Valor do vale",
1115
1151
  "voucher.selectAmount": "Selecione o valor",
1116
1152
  "voucher.customAmount": "Ou introduza um valor personalizado",
1117
1153
  "voucher.amountRange": "O valor deve estar entre {{min}} e {{max}}",
@@ -1402,6 +1438,13 @@ const sv = {
1402
1438
  "voucher.monetaryTypeDesc": "Välj valfritt belopp",
1403
1439
  "voucher.eventType": "Upplevelsebevis",
1404
1440
  "voucher.eventTypeDesc": "För en specifik upplevelse",
1441
+ "voucher.categoryType": "Kategoripresent",
1442
+ "voucher.categoryTypeDesc": "För alla upplevelser i en kategori",
1443
+ "voucher.categoryVoucher": "Kategoripresent",
1444
+ "voucher.selectCategory": "Välj kategori",
1445
+ "voucher.categoryLabel": "Kategori",
1446
+ "voucher.selectCategoryPlaceholder": "Välj en kategori...",
1447
+ "voucher.categoryAmount": "Presentkortsvärde",
1405
1448
  "voucher.selectAmount": "Välj belopp",
1406
1449
  "voucher.customAmount": "Eller ange eget belopp",
1407
1450
  "voucher.amountRange": "Beloppet måste vara mellan {{min}} och {{max}}",
@@ -6372,7 +6415,7 @@ function loadMollieScript() {
6372
6415
  document.head.appendChild(script);
6373
6416
  });
6374
6417
  }
6375
- function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess: _onSuccess, onError, upsellSelections = [], mollieProfileId, mollieTestmode, }) {
6418
+ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess: _onSuccess, onError, upsellSelections = [], mollieProfileId, mollieTestmode, freeformSelection, }) {
6376
6419
  const t = useTranslations();
6377
6420
  const { locale } = useLocale();
6378
6421
  const [isLoading, setIsLoading] = React.useState(false);
@@ -6386,7 +6429,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6386
6429
  const cardFormRef = React.useRef(null);
6387
6430
  const cardContainerRef = React.useRef(null);
6388
6431
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
6389
- const isFullyCoveredByGiftCards = isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
6432
+ const isFullyCoveredByGiftCards = freeformSelection
6433
+ ? false
6434
+ : isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
6390
6435
  const isCreditCard = selectedMethod === "creditcard";
6391
6436
  React.useEffect(() => {
6392
6437
  if (isFullyCoveredByGiftCards)
@@ -6496,6 +6541,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6496
6541
  return (jsxRuntime.jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards, onSuccess: _onSuccess, onError: onError, upsellSelections: upsellSelections }));
6497
6542
  }
6498
6543
  const validateBeforePayment = () => {
6544
+ if (freeformSelection) {
6545
+ return null;
6546
+ }
6499
6547
  const participantCount = formData.participants.filter((p) => p.name?.trim()).length;
6500
6548
  const availableSpots = eventDetails.availableSpots || 0;
6501
6549
  if (participantCount > availableSpots) {
@@ -6507,21 +6555,41 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6507
6555
  return null;
6508
6556
  };
6509
6557
  const buildRequestData = (cardToken) => ({
6510
- eventInstanceId: config.eventInstanceId || eventDetails.id,
6511
- organizationId: config.organizationId,
6512
- amount: Math.round(totalAmount),
6513
- currency: "EUR",
6514
- participants: formData.participants.filter((p) => p.name?.trim()),
6515
- discountCode: discountCode?.code,
6516
- giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6517
- customerName: formData.customerName?.trim(),
6518
- customerEmail: formData.customerEmail?.trim(),
6519
- customerPhone: formData.customerPhone?.trim(),
6520
- comment: formData.comment?.trim(),
6521
- ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6522
- ...(cardToken && { cardToken }),
6523
- ...(selectedMethod && !cardToken && { method: selectedMethod }),
6524
- ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6558
+ ...(freeformSelection
6559
+ ? {
6560
+ eventTypeId: freeformSelection.eventTypeId,
6561
+ organizationId: config.organizationId,
6562
+ start: freeformSelection.start,
6563
+ end: freeformSelection.end,
6564
+ requestedUnits: freeformSelection.requestedUnits,
6565
+ amount: Math.round(totalAmount),
6566
+ currency: "EUR",
6567
+ customerName: freeformSelection.customerName.trim(),
6568
+ customerEmail: freeformSelection.customerEmail.trim(),
6569
+ customerPhone: freeformSelection.customerPhone?.trim(),
6570
+ comment: freeformSelection.comment?.trim(),
6571
+ locale,
6572
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6573
+ ...(cardToken && { cardToken }),
6574
+ ...(selectedMethod && !cardToken && { method: selectedMethod }),
6575
+ }
6576
+ : {
6577
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
6578
+ organizationId: config.organizationId,
6579
+ amount: Math.round(totalAmount),
6580
+ currency: "EUR",
6581
+ participants: formData.participants.filter((p) => p.name?.trim()),
6582
+ discountCode: discountCode?.code,
6583
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6584
+ customerName: formData.customerName?.trim(),
6585
+ customerEmail: formData.customerEmail?.trim(),
6586
+ customerPhone: formData.customerPhone?.trim(),
6587
+ comment: formData.comment?.trim(),
6588
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6589
+ ...(cardToken && { cardToken }),
6590
+ ...(selectedMethod && !cardToken && { method: selectedMethod }),
6591
+ ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6592
+ }),
6525
6593
  });
6526
6594
  const handleCardPayment = async () => {
6527
6595
  if (!mollieRef.current)
@@ -6539,7 +6607,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6539
6607
  setPaymentError(tokenError?.message || t("error.createPayment"));
6540
6608
  return;
6541
6609
  }
6542
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-mollie-payment"), {
6610
+ const response = await fetch(getApiUrl(config.apiBaseUrl, freeformSelection
6611
+ ? "/booking/create-freeform-mollie-payment"
6612
+ : "/booking/create-mollie-payment"), {
6543
6613
  method: "POST",
6544
6614
  headers: createApiHeaders(config, locale),
6545
6615
  body: JSON.stringify(createRequestBody(config, buildRequestData(token))),
@@ -6575,7 +6645,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6575
6645
  setPaymentError(validationError);
6576
6646
  return;
6577
6647
  }
6578
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-mollie-payment"), {
6648
+ const response = await fetch(getApiUrl(config.apiBaseUrl, freeformSelection
6649
+ ? "/booking/create-freeform-mollie-payment"
6650
+ : "/booking/create-mollie-payment"), {
6579
6651
  method: "POST",
6580
6652
  headers: createApiHeaders(config, locale),
6581
6653
  body: JSON.stringify(createRequestBody(config, buildRequestData())),
@@ -6603,7 +6675,7 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6603
6675
  ? t("button.processingPayment")
6604
6676
  : selectedMethodData
6605
6677
  ? t("payment.payWithMethod", { method: selectedMethodData.description })
6606
- : totalAmount < eventDetails.price * formData.participants.filter((p) => p.name?.trim()).length
6678
+ : !freeformSelection && totalAmount < eventDetails.price * formData.participants.filter((p) => p.name?.trim()).length
6607
6679
  ? t("button.depositAndBook")
6608
6680
  : t("button.bookNow");
6609
6681
  const isPayDisabled = isLoading || !selectedMethod || (isCreditCard && !isMollieReady);
@@ -6910,12 +6982,18 @@ var loadStripe = function loadStripe() {
6910
6982
  };
6911
6983
 
6912
6984
  // Inner component that uses the Stripe hooks
6913
- function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onError, }) {
6985
+ function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onError, freeformSelection, }) {
6914
6986
  const t = useTranslations();
6915
- const participantCount = formData.participants.filter((p) => p.name.trim()).length;
6916
- const fullAmount = eventDetails.price * participantCount;
6917
- const submitLabel = totalAmount < fullAmount ? t("button.depositAndBook") : t("button.bookNow");
6987
+ const isFreeform = Boolean(freeformSelection);
6988
+ const participantCount = isFreeform
6989
+ ? 1
6990
+ : formData.participants.filter((p) => p.name.trim()).length;
6991
+ const fullAmount = isFreeform ? totalAmount : eventDetails.price * participantCount;
6992
+ const submitLabel = !isFreeform && totalAmount < fullAmount ? t("button.depositAndBook") : t("button.bookNow");
6918
6993
  return (jsxRuntime.jsx(StripeElementsPaymentForm, { onValidate: () => {
6994
+ if (isFreeform) {
6995
+ return null;
6996
+ }
6919
6997
  const availableSpots = eventDetails.availableSpots || 0;
6920
6998
  if (participantCount > availableSpots) {
6921
6999
  return t("payment.tooManyParticipants", { count: participantCount, available: availableSpots });
@@ -6937,7 +7015,7 @@ function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onEr
6937
7015
  },
6938
7016
  }, submitContent: jsxRuntime.jsx(jsxRuntime.Fragment, { children: submitLabel }), loadingContent: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [spinner("var(--bw-surface-color)"), " ", t("button.processingPayment")] }) }));
6939
7017
  }
6940
- function StripePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripeAppearance, upsellSelections = [], }) {
7018
+ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripeAppearance, upsellSelections = [], freeformSelection, }) {
6941
7019
  const t = useTranslations();
6942
7020
  const { locale } = useLocale();
6943
7021
  const [clientSecret, setClientSecret] = React.useState(null);
@@ -6957,7 +7035,9 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
6957
7035
  return loadStripe(systemConfig.stripePublishableKey, stripeOptions);
6958
7036
  }, [systemConfig?.stripePublishableKey, systemConfig?.connectedAccountId, locale]);
6959
7037
  const storageKey = typeof window !== "undefined"
6960
- ? `bw_pi_${config?.organizationId}_${config?.eventInstanceId || eventDetails?.id}`
7038
+ ? freeformSelection
7039
+ ? `bw_pi_freeform_${config?.organizationId}_${freeformSelection.eventTypeId}_${freeformSelection.start}_${freeformSelection.end}_${freeformSelection.requestedUnits}`
7040
+ : `bw_pi_${config?.organizationId}_${config?.eventInstanceId || eventDetails?.id}`
6961
7041
  : "";
6962
7042
  const PAYMENT_INTENT_TTL = 24 * 60 * 60 * 1000;
6963
7043
  function loadPersistedPaymentIntent() {
@@ -7012,48 +7092,82 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7012
7092
  }, [paymentIntentId]);
7013
7093
  React.useEffect(() => {
7014
7094
  const createStripePayment = async () => {
7015
- if (!systemConfig || !eventDetails || !formData.participants?.length) {
7095
+ if (!systemConfig || !eventDetails) {
7096
+ return;
7097
+ }
7098
+ if (freeformSelection) {
7099
+ if (!freeformSelection.customerEmail?.trim() ||
7100
+ !freeformSelection.customerName?.trim() ||
7101
+ !freeformSelection.start ||
7102
+ !freeformSelection.end) {
7103
+ return;
7104
+ }
7105
+ }
7106
+ else if (!formData.participants?.length) {
7016
7107
  return;
7017
7108
  }
7018
7109
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
7019
- if (participantCount === 0 || !formData.customerEmail?.trim() || !formData.customerName?.trim()) {
7110
+ if (!freeformSelection &&
7111
+ (participantCount === 0 || !formData.customerEmail?.trim() || !formData.customerName?.trim())) {
7020
7112
  return;
7021
7113
  }
7022
7114
  setIsCreatingPaymentIntent(true);
7023
7115
  setPaymentError(null);
7024
7116
  try {
7025
- const requestData = {
7026
- eventInstanceId: config.eventInstanceId || eventDetails.id,
7027
- organizationId: config.organizationId,
7028
- amount: Math.round(totalAmount),
7029
- currency: "eur",
7030
- participants: formData.participants.filter((p) => p.name?.trim()),
7031
- discountCode: discountCode?.code,
7032
- giftCardCodes: giftCards?.map((gc) => gc.code) || [],
7033
- customerName: formData.customerName?.trim(),
7034
- customerEmail: formData.customerEmail?.trim(),
7035
- customerPhone: formData.customerPhone?.trim(),
7036
- comment: formData.comment?.trim(),
7037
- ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7038
- ...(paymentIntentId && { paymentIntentId }),
7039
- ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
7040
- };
7041
- if (!requestData.eventInstanceId) {
7042
- throw new Error("Event instance ID is required");
7043
- }
7117
+ const endpoint = freeformSelection
7118
+ ? "/booking/create-freeform-stripe-payment"
7119
+ : "/booking/create-stripe-payment";
7120
+ const requestData = freeformSelection
7121
+ ? {
7122
+ eventTypeId: freeformSelection.eventTypeId,
7123
+ organizationId: config.organizationId,
7124
+ start: freeformSelection.start,
7125
+ end: freeformSelection.end,
7126
+ requestedUnits: freeformSelection.requestedUnits,
7127
+ amount: Math.round(totalAmount),
7128
+ currency: "eur",
7129
+ customerName: freeformSelection.customerName.trim(),
7130
+ customerEmail: freeformSelection.customerEmail.trim(),
7131
+ customerPhone: freeformSelection.customerPhone?.trim(),
7132
+ comment: freeformSelection.comment?.trim(),
7133
+ locale,
7134
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7135
+ ...(paymentIntentId && { paymentIntentId }),
7136
+ }
7137
+ : {
7138
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
7139
+ organizationId: config.organizationId,
7140
+ amount: Math.round(totalAmount),
7141
+ currency: "eur",
7142
+ participants: formData.participants.filter((p) => p.name?.trim()),
7143
+ discountCode: discountCode?.code,
7144
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
7145
+ customerName: formData.customerName?.trim(),
7146
+ customerEmail: formData.customerEmail?.trim(),
7147
+ customerPhone: formData.customerPhone?.trim(),
7148
+ comment: formData.comment?.trim(),
7149
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7150
+ ...(paymentIntentId && { paymentIntentId }),
7151
+ ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
7152
+ };
7044
7153
  if (!requestData.organizationId) {
7045
7154
  throw new Error("Organization ID is required");
7046
7155
  }
7047
- if (!requestData.amount || requestData.amount <= 0) {
7156
+ if (requestData.amount === undefined || requestData.amount < 0) {
7048
7157
  throw new Error("Valid amount is required");
7049
7158
  }
7050
- if (!requestData.participants || requestData.participants.length === 0) {
7159
+ if (!freeformSelection && "eventInstanceId" in requestData && !requestData.eventInstanceId) {
7160
+ throw new Error("Event instance ID is required");
7161
+ }
7162
+ if (!freeformSelection &&
7163
+ "participants" in requestData &&
7164
+ (!requestData.participants || requestData.participants.length === 0)) {
7051
7165
  throw new Error("At least one participant is required");
7052
7166
  }
7053
7167
  if (!requestData.customerEmail) {
7054
7168
  throw new Error("Customer email is required");
7055
7169
  }
7056
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-stripe-payment"), {
7170
+ const response = await fetch(getApiUrl(config.apiBaseUrl, endpoint), {
7057
7171
  method: "POST",
7058
7172
  headers: createApiHeaders(config, locale),
7059
7173
  body: JSON.stringify(createRequestBody(config, requestData)),
@@ -7098,9 +7212,13 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7098
7212
  giftCards,
7099
7213
  config,
7100
7214
  upsellSelections,
7215
+ freeformSelection,
7216
+ locale,
7101
7217
  ]);
7102
7218
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
7103
- const isFullyCoveredByGiftCards = isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
7219
+ const isFullyCoveredByGiftCards = freeformSelection
7220
+ ? false
7221
+ : isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
7104
7222
  if (isFullyCoveredByGiftCards && totalAmount <= 0) {
7105
7223
  return (jsxRuntime.jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards, onSuccess: onSuccess, onError: onError, upsellSelections: upsellSelections }));
7106
7224
  }
@@ -7139,7 +7257,7 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7139
7257
  setPaymentIntentId(null);
7140
7258
  setClientSecret(null);
7141
7259
  onSuccess(result);
7142
- }, onError: onError }) }));
7260
+ }, onError: onError, ...(freeformSelection ? { freeformSelection } : {}) }) }));
7143
7261
  }
7144
7262
 
7145
7263
  const HOLD_DURATION_SECONDS = 15 * 60;
@@ -13399,20 +13517,22 @@ const sectionHeaderStyles = sectionStyles.header;
13399
13517
  const labelStyles = formStyles.label;
13400
13518
  const inputStyles = formStyles.input;
13401
13519
  const errorTextStyles = formStyles.error;
13402
- function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClose, onSuccess, onError, systemConfig, preselectedEventTypeId, isLoadingEventTypes = false, }) {
13520
+ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, categories, isOpen, onClose, onSuccess, onError, systemConfig, preselectedEventTypeId, isLoadingEventTypes = false, }) {
13403
13521
  const t = useTranslations();
13404
13522
  const { locale } = useLocale();
13405
13523
  const paymentProvider = systemConfig?.paymentProvider ?? "stripe";
13406
13524
  // Form state
13407
- // Show voucher type selection if both monetary and event vouchers are allowed
13408
- // The event type selection will handle the case when no event types are available
13409
13525
  const canUseMonetaryVoucherType = voucherConfig.allowMonetaryVouchers !== false;
13410
13526
  const canUseEventVoucherType = voucherConfig.allowEventVouchers;
13411
- const showVoucherTypeSelection = canUseMonetaryVoucherType && canUseEventVoucherType && !isLoadingEventTypes;
13527
+ const canUseCategoryVoucherType = canUseEventVoucherType && categories.length > 0;
13528
+ const availableTypeCount = [canUseMonetaryVoucherType, canUseEventVoucherType, canUseCategoryVoucherType].filter(Boolean).length;
13529
+ const showVoucherTypeSelection = availableTypeCount > 1 && !isLoadingEventTypes;
13412
13530
  const [voucherType, setVoucherType] = React.useState(canUseMonetaryVoucherType ? "monetary" : "event");
13413
13531
  const [monetaryAmount, setMonetaryAmount] = React.useState(voucherConfig.monetaryPresets[2] || 10000);
13414
13532
  const [selectedEventTypeId, setSelectedEventTypeId] = React.useState(null);
13415
13533
  const [eventQuantity, setEventQuantity] = React.useState(1);
13534
+ const [selectedCategoryId, setSelectedCategoryId] = React.useState(null);
13535
+ const [categoryAmount, setCategoryAmount] = React.useState(10000);
13416
13536
  const [recipientName, setRecipientName] = React.useState("");
13417
13537
  const [message, setMessage] = React.useState("");
13418
13538
  const [customerName, setCustomerName] = React.useState("");
@@ -13472,6 +13592,9 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13472
13592
  if (voucherType === "monetary") {
13473
13593
  return monetaryAmount;
13474
13594
  }
13595
+ else if (voucherType === "category") {
13596
+ return categoryAmount;
13597
+ }
13475
13598
  else {
13476
13599
  const eventType = eventTypes.find((et) => et.id === selectedEventTypeId);
13477
13600
  if (eventType) {
@@ -13479,7 +13602,7 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13479
13602
  }
13480
13603
  return 0;
13481
13604
  }
13482
- }, [voucherType, monetaryAmount, selectedEventTypeId, eventQuantity, eventTypes]);
13605
+ }, [voucherType, monetaryAmount, selectedEventTypeId, eventQuantity, eventTypes, categoryAmount]);
13483
13606
  const selectedEventType = React.useMemo(() => {
13484
13607
  return eventTypes.find((et) => et.id === selectedEventTypeId);
13485
13608
  }, [eventTypes, selectedEventTypeId]);
@@ -13488,6 +13611,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13488
13611
  voucherType,
13489
13612
  selectedEventTypeId,
13490
13613
  eventQuantity,
13614
+ selectedCategoryId,
13615
+ categoryAmount,
13491
13616
  recipientName: recipientName.trim(),
13492
13617
  message: message.trim(),
13493
13618
  customerName: customerName.trim(),
@@ -13497,6 +13622,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13497
13622
  voucherType,
13498
13623
  selectedEventTypeId,
13499
13624
  eventQuantity,
13625
+ selectedCategoryId,
13626
+ categoryAmount,
13500
13627
  recipientName,
13501
13628
  message,
13502
13629
  customerName,
@@ -13515,24 +13642,37 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13515
13642
  return false;
13516
13643
  if (voucherType === "event" && !selectedEventTypeId)
13517
13644
  return false;
13645
+ if (voucherType === "category" && !selectedCategoryId)
13646
+ return false;
13518
13647
  return true;
13519
- }, [customerName, customerEmail, acceptTerms, totalAmount, voucherConfig, voucherType, selectedEventTypeId]);
13648
+ }, [customerName, customerEmail, acceptTerms, totalAmount, voucherType, selectedEventTypeId, selectedCategoryId]);
13520
13649
  const defaultMonetaryAmount = React.useMemo(() => voucherConfig.monetaryPresets[2] || voucherConfig.monetaryPresets[0] || 1000, [voucherConfig.monetaryPresets]);
13521
13650
  const handleVoucherTypeChange = React.useCallback((nextType) => {
13522
13651
  setVoucherType(nextType);
13523
13652
  setPaymentError(null);
13524
13653
  if (nextType === "event") {
13525
- // Keep voucher type mutually exclusive: entering event flow resets amount selection state.
13526
13654
  setMonetaryAmount(defaultMonetaryAmount);
13655
+ setSelectedCategoryId(null);
13656
+ setCategoryAmount(10000);
13527
13657
  if (eventTypes.length === 1) {
13528
13658
  setSelectedEventTypeId(eventTypes[0]?.id ?? null);
13529
13659
  }
13530
13660
  return;
13531
13661
  }
13532
- // Keep voucher type mutually exclusive: entering amount flow clears event selection.
13662
+ if (nextType === "category") {
13663
+ setMonetaryAmount(defaultMonetaryAmount);
13664
+ setSelectedEventTypeId(null);
13665
+ setEventQuantity(1);
13666
+ if (categories.length === 1) {
13667
+ setSelectedCategoryId(categories[0]?.id ?? null);
13668
+ }
13669
+ return;
13670
+ }
13533
13671
  setSelectedEventTypeId(null);
13534
13672
  setEventQuantity(1);
13535
- }, [defaultMonetaryAmount, eventTypes]);
13673
+ setSelectedCategoryId(null);
13674
+ setCategoryAmount(10000);
13675
+ }, [defaultMonetaryAmount, eventTypes, categories]);
13536
13676
  React.useEffect(() => {
13537
13677
  if (!canUseMonetaryVoucherType && voucherType === "monetary") {
13538
13678
  setVoucherType("event");
@@ -13573,9 +13713,12 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13573
13713
  organizationId: config.organizationId,
13574
13714
  amount: totalAmount,
13575
13715
  currency: "eur",
13576
- voucherType,
13577
- monetaryValue: voucherType === "monetary" ? totalAmount : undefined,
13716
+ voucherType: voucherType === "category" ? "monetary" : voucherType,
13717
+ monetaryValue: voucherType === "monetary" ? totalAmount
13718
+ : voucherType === "category" ? categoryAmount
13719
+ : undefined,
13578
13720
  eventTypeId: voucherType === "event" ? selectedEventTypeId : undefined,
13721
+ eventCategoryId: voucherType === "category" ? selectedCategoryId : undefined,
13579
13722
  eventAmount: voucherType === "event" ? eventQuantity : undefined,
13580
13723
  recipientName: recipientName.trim() || undefined,
13581
13724
  message: message.trim() || undefined,
@@ -13766,7 +13909,30 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13766
13909
  fontSize: "13px",
13767
13910
  color: "var(--bw-text-muted)",
13768
13911
  marginTop: "4px",
13769
- }, children: t("voucher.eventTypeDesc") })] }))] })] })), voucherType === "monetary" && (jsxRuntime.jsxs("div", { style: cardStyles, children: [jsxRuntime.jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectAmount") }), jsxRuntime.jsx("div", { style: {
13912
+ }, children: t("voucher.eventTypeDesc") })] })), canUseCategoryVoucherType && (jsxRuntime.jsxs("button", { type: "button", onClick: () => handleVoucherTypeChange("category"), style: {
13913
+ flex: 1,
13914
+ padding: "16px",
13915
+ borderRadius: "var(--bw-border-radius)",
13916
+ border: voucherType === "category"
13917
+ ? "2px solid var(--bw-highlight-color)"
13918
+ : "1px solid var(--bw-border-color)",
13919
+ backgroundColor: voucherType === "category"
13920
+ ? "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)"
13921
+ : "var(--bw-surface-color)",
13922
+ cursor: "pointer",
13923
+ fontFamily: "var(--bw-font-family)",
13924
+ transition: "all 0.2s ease",
13925
+ textAlign: "center",
13926
+ }, children: [jsxRuntime.jsx("div", { style: {
13927
+ fontWeight: 600,
13928
+ color: voucherType === "category"
13929
+ ? "var(--bw-highlight-color)"
13930
+ : "var(--bw-text-color)",
13931
+ }, children: t("voucher.categoryType") }), jsxRuntime.jsx("div", { style: {
13932
+ fontSize: "13px",
13933
+ color: "var(--bw-text-muted)",
13934
+ marginTop: "4px",
13935
+ }, children: t("voucher.categoryTypeDesc") })] }))] })] })), voucherType === "monetary" && (jsxRuntime.jsxs("div", { style: cardStyles, children: [jsxRuntime.jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectAmount") }), jsxRuntime.jsx("div", { style: {
13770
13936
  display: "grid",
13771
13937
  gridTemplateColumns: "repeat(3, 1fr)",
13772
13938
  gap: "8px",
@@ -13821,7 +13987,50 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13821
13987
  fontSize: "18px",
13822
13988
  color: "var(--bw-highlight-color)",
13823
13989
  fontFamily: "var(--bw-font-family)",
13824
- }, children: formatCurrency(totalAmount) })] }) }))] })), jsxRuntime.jsxs("div", { style: cardStyles, children: [jsxRuntime.jsx("h2", { style: sectionHeaderStyles, children: t("voucher.recipientSection") }), jsxRuntime.jsx("p", { style: {
13990
+ }, children: formatCurrency(totalAmount) })] }) }))] })), voucherType === "category" && (jsxRuntime.jsxs("div", { style: cardStyles, children: [jsxRuntime.jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectCategory") }), jsxRuntime.jsx("div", { style: { marginTop: "12px" }, children: jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { htmlFor: "category-select", style: labelStyles, children: t("voucher.categoryLabel") }), jsxRuntime.jsxs("select", { id: "category-select", value: selectedCategoryId || "", onChange: (e) => setSelectedCategoryId(e.target.value ? Number(e.target.value) : null), disabled: categories.length === 1, style: {
13991
+ ...inputStyles,
13992
+ cursor: categories.length === 1 ? "not-allowed" : "pointer",
13993
+ }, children: [categories.length > 1 && (jsxRuntime.jsx("option", { value: "", children: t("voucher.selectCategoryPlaceholder") })), categories.map((cat) => (jsxRuntime.jsx("option", { value: cat.id, children: cat.name }, cat.id)))] })] }), selectedCategoryId && (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { htmlFor: "category-amount", style: labelStyles, children: t("voucher.categoryAmount") }), jsxRuntime.jsx("div", { style: {
13994
+ display: "grid",
13995
+ gridTemplateColumns: "repeat(3, 1fr)",
13996
+ gap: "8px",
13997
+ marginTop: "4px",
13998
+ }, children: voucherConfig.monetaryPresets.map((preset) => (jsxRuntime.jsx("button", { type: "button", onClick: () => setCategoryAmount(preset), style: {
13999
+ padding: "10px",
14000
+ borderRadius: "var(--bw-border-radius)",
14001
+ border: categoryAmount === preset
14002
+ ? "2px solid var(--bw-highlight-color)"
14003
+ : "1px solid var(--bw-border-color)",
14004
+ backgroundColor: categoryAmount === preset
14005
+ ? "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)"
14006
+ : "var(--bw-surface-color)",
14007
+ cursor: "pointer",
14008
+ fontFamily: "var(--bw-font-family)",
14009
+ fontWeight: 600,
14010
+ fontSize: "14px",
14011
+ color: categoryAmount === preset
14012
+ ? "var(--bw-highlight-color)"
14013
+ : "var(--bw-text-color)",
14014
+ transition: "all 0.2s ease",
14015
+ }, children: formatCurrency(preset) }, preset))) })] }))] }) }), selectedCategoryId && categoryAmount > 0 && (jsxRuntime.jsx("div", { style: {
14016
+ marginTop: "16px",
14017
+ padding: "12px",
14018
+ backgroundColor: "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)",
14019
+ borderRadius: "var(--bw-border-radius)",
14020
+ border: "1px solid var(--bw-highlight-color)",
14021
+ }, children: jsxRuntime.jsxs("div", { style: {
14022
+ display: "flex",
14023
+ justifyContent: "space-between",
14024
+ alignItems: "center",
14025
+ }, children: [jsxRuntime.jsx("span", { style: {
14026
+ color: "var(--bw-text-color)",
14027
+ fontFamily: "var(--bw-font-family)",
14028
+ }, children: categories.find((c) => c.id === selectedCategoryId)?.name }), jsxRuntime.jsx("span", { style: {
14029
+ fontWeight: 700,
14030
+ fontSize: "18px",
14031
+ color: "var(--bw-highlight-color)",
14032
+ fontFamily: "var(--bw-font-family)",
14033
+ }, children: formatCurrency(categoryAmount) })] }) }))] })), jsxRuntime.jsxs("div", { style: cardStyles, children: [jsxRuntime.jsx("h2", { style: sectionHeaderStyles, children: t("voucher.recipientSection") }), jsxRuntime.jsx("p", { style: {
13825
14034
  fontSize: "13px",
13826
14035
  color: "var(--bw-text-muted)",
13827
14036
  fontFamily: "var(--bw-font-family)",
@@ -13854,7 +14063,9 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13854
14063
  fontFamily: "var(--bw-font-family)",
13855
14064
  }, children: voucherType === "monetary"
13856
14065
  ? t("voucher.monetaryVoucher")
13857
- : `${eventQuantity}x ${selectedEventType?.name || t("voucher.eventVoucher")}` }), recipientName && (jsxRuntime.jsxs("div", { style: {
14066
+ : voucherType === "category"
14067
+ ? categories.find((c) => c.id === selectedCategoryId)?.name || t("voucher.categoryVoucher")
14068
+ : `${eventQuantity}x ${selectedEventType?.name || t("voucher.eventVoucher")}` }), recipientName && (jsxRuntime.jsxs("div", { style: {
13858
14069
  fontSize: "13px",
13859
14070
  color: "var(--bw-text-muted)",
13860
14071
  fontFamily: "var(--bw-font-family)",
@@ -14130,7 +14341,7 @@ function VoucherAttachment({ onClick }) {
14130
14341
  }, children: t("voucher.buyAsGift") })] }));
14131
14342
  }
14132
14343
 
14133
- function VoucherIntegration({ config, voucherConfig, eventTypes, systemConfig, isFormOpen, isLoadingConfig, preselectedEventTypeId, voucherPurchaseResult, isSuccess, showStandaloneCard, onCardClick, onFormClose, onSuccess, onError, onSuccessModalClose, }) {
14344
+ function VoucherIntegration({ config, voucherConfig, eventTypes, categories, systemConfig, isFormOpen, isLoadingConfig, preselectedEventTypeId, voucherPurchaseResult, isSuccess, showStandaloneCard, onCardClick, onFormClose, onSuccess, onError, onSuccessModalClose, }) {
14134
14345
  if (!voucherConfig?.enabled) {
14135
14346
  return null;
14136
14347
  }
@@ -14141,7 +14352,7 @@ function VoucherIntegration({ config, voucherConfig, eventTypes, systemConfig, i
14141
14352
  justifyContent: "center",
14142
14353
  }, children: jsxRuntime.jsx(VoucherPurchaseCard, { config: voucherConfig, minEventPrice: eventTypes.length > 0
14143
14354
  ? Math.min(...eventTypes.map((et) => et.maxPrice))
14144
- : undefined, fallbackImages: eventTypes.flatMap((eventType) => eventType.images || []), onClick: onCardClick, standalone: true }) }) })), jsxRuntime.jsx(VoucherPurchaseForm, { config: config, voucherConfig: voucherConfig, eventTypes: eventTypes, isOpen: isFormOpen, onClose: onFormClose, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, preselectedEventTypeId: preselectedEventTypeId, isLoadingEventTypes: isLoadingConfig }), isSuccess && voucherPurchaseResult && (jsxRuntime.jsx(VoucherSuccessModal, { isOpen: true, onClose: onSuccessModalClose, result: voucherPurchaseResult }))] }));
14355
+ : undefined, fallbackImages: eventTypes.flatMap((eventType) => eventType.images || []), onClick: onCardClick, standalone: true }) }) })), jsxRuntime.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 && (jsxRuntime.jsx(VoucherSuccessModal, { isOpen: true, onClose: onSuccessModalClose, result: voucherPurchaseResult }))] }));
14145
14356
  }
14146
14357
 
14147
14358
  // Helper function to preprocess markdown for underline support
@@ -15409,6 +15620,312 @@ function SpecialsView({ specials, onEventSelect, isLoading = false, showSavingsA
15409
15620
  }) })] }));
15410
15621
  }
15411
15622
 
15623
+ function toMinutesForUnit(unit) {
15624
+ switch (unit.toLowerCase()) {
15625
+ case "minute":
15626
+ case "minutes":
15627
+ return 1;
15628
+ case "hour":
15629
+ case "hours":
15630
+ return 60;
15631
+ case "day":
15632
+ case "days":
15633
+ return 24 * 60;
15634
+ default:
15635
+ return 1;
15636
+ }
15637
+ }
15638
+ // --- Timezone helpers ------------------------------------------------------
15639
+ // The booking is evaluated server-side against the organization's local
15640
+ // operating hours. The customer's browser may be in a different timezone, so we
15641
+ // interpret the datetime-local field as a wall-clock time in the ORG timezone
15642
+ // (not the browser's) and convert to a UTC instant for the API.
15643
+ function getTzParts(date, timeZone) {
15644
+ const dtf = new Intl.DateTimeFormat("en-CA", {
15645
+ timeZone,
15646
+ hour12: false,
15647
+ year: "numeric",
15648
+ month: "2-digit",
15649
+ day: "2-digit",
15650
+ hour: "2-digit",
15651
+ minute: "2-digit",
15652
+ second: "2-digit",
15653
+ });
15654
+ const parts = {};
15655
+ for (const part of dtf.formatToParts(date)) {
15656
+ if (part.type !== "literal")
15657
+ parts[part.type] = part.value;
15658
+ }
15659
+ return parts;
15660
+ }
15661
+ function tzOffsetMs(utcMs, timeZone) {
15662
+ const p = getTzParts(new Date(utcMs), timeZone);
15663
+ const hour = p.hour === "24" ? "00" : p.hour;
15664
+ const asLocalUtc = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(hour), Number(p.minute), Number(p.second));
15665
+ return asLocalUtc - utcMs;
15666
+ }
15667
+ /** Convert an org-tz wall-clock string ("YYYY-MM-DDTHH:mm") to a UTC instant. */
15668
+ function zonedWallTimeToUtc(wallTime, timeZone) {
15669
+ const [datePart, timePart] = wallTime.split("T");
15670
+ if (!datePart || !timePart)
15671
+ return new Date(NaN);
15672
+ const [y, mo, d] = datePart.split("-").map(Number);
15673
+ const [h, mi] = timePart.split(":").map(Number);
15674
+ const naiveUtc = Date.UTC(y, (mo ?? 1) - 1, d, h ?? 0, mi ?? 0);
15675
+ // Single-pass offset correction (good enough outside the ~1h DST fold).
15676
+ const offset = tzOffsetMs(naiveUtc, timeZone);
15677
+ return new Date(naiveUtc - offset);
15678
+ }
15679
+ /** Render a UTC instant as an org-tz datetime-local input value. */
15680
+ function utcToZonedInputValue(date, timeZone) {
15681
+ const p = getTzParts(date, timeZone);
15682
+ const hour = p.hour === "24" ? "00" : p.hour;
15683
+ return `${p.year}-${p.month}-${p.day}T${hour}:${p.minute}`;
15684
+ }
15685
+ /** Format a UTC instant for display in the org timezone. */
15686
+ function formatInTimeZone(date, timeZone, locale) {
15687
+ return new Intl.DateTimeFormat(locale, {
15688
+ timeZone,
15689
+ dateStyle: "medium",
15690
+ timeStyle: "short",
15691
+ }).format(date);
15692
+ }
15693
+ function calculateTieredPrice(durationUnits, config) {
15694
+ const tiers = [...(config.tiers ?? [])]
15695
+ .filter((tier) => tier.unitCount > 0 && tier.price >= 0)
15696
+ .sort((a, b) => b.unitCount - a.unitCount);
15697
+ if (tiers.length === 0) {
15698
+ return 0;
15699
+ }
15700
+ let total = 0;
15701
+ let remaining = durationUnits;
15702
+ for (const tier of tiers) {
15703
+ const count = Math.floor(remaining / tier.unitCount);
15704
+ if (count <= 0)
15705
+ continue;
15706
+ total += count * tier.price;
15707
+ remaining -= count * tier.unitCount;
15708
+ }
15709
+ if (remaining > 0) {
15710
+ if (typeof config.perUnitPrice === "number") {
15711
+ total += remaining * config.perUnitPrice;
15712
+ }
15713
+ else {
15714
+ total += tiers[tiers.length - 1]?.price ?? 0;
15715
+ }
15716
+ }
15717
+ return total;
15718
+ }
15719
+ function FreeformSelection({ config, eventType, systemConfig, isOpen, onClose, onSuccess, onError, }) {
15720
+ const t = useTranslations();
15721
+ const { locale } = useLocale();
15722
+ const freeformConfig = eventType.freeformConfig;
15723
+ const timeZone = eventType.organizationTimezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
15724
+ const isNamedResource = eventType.resourceKind === "named";
15725
+ const maxUnits = isNamedResource ? 1 : Math.max(1, eventType.resourceQuantity ?? 1);
15726
+ const now = React.useMemo(() => {
15727
+ const next = new Date();
15728
+ next.setMinutes(next.getMinutes() + 30, 0, 0);
15729
+ return next;
15730
+ }, []);
15731
+ const [startInput, setStartInput] = React.useState(utcToZonedInputValue(now, timeZone));
15732
+ const [duration, setDuration] = React.useState(Math.max(1, freeformConfig?.minDuration ?? 1));
15733
+ const [requestedUnits, setRequestedUnits] = React.useState(1);
15734
+ const [customerName, setCustomerName] = React.useState("");
15735
+ const [customerEmail, setCustomerEmail] = React.useState("");
15736
+ const [customerPhone, setCustomerPhone] = React.useState("");
15737
+ const [comment, setComment] = React.useState("");
15738
+ const [acceptTerms, setAcceptTerms] = React.useState(false);
15739
+ const [checkoutStep, setCheckoutStep] = React.useState("details");
15740
+ const [availability, setAvailability] = React.useState(null);
15741
+ const [availabilityError, setAvailabilityError] = React.useState(null);
15742
+ const [isCheckingAvailability, setIsCheckingAvailability] = React.useState(false);
15743
+ React.useEffect(() => {
15744
+ if (!isOpen)
15745
+ return;
15746
+ setCheckoutStep("details");
15747
+ }, [isOpen]);
15748
+ React.useEffect(() => {
15749
+ if (!freeformConfig)
15750
+ return;
15751
+ setDuration(Math.max(1, freeformConfig.minDuration));
15752
+ setRequestedUnits(1);
15753
+ setAvailability(null);
15754
+ setAvailabilityError(null);
15755
+ }, [freeformConfig?.resourceId]);
15756
+ const unitMinutes = toMinutesForUnit(freeformConfig?.unit ?? "hour");
15757
+ const startDate = React.useMemo(() => zonedWallTimeToUtc(startInput, timeZone), [startInput, timeZone]);
15758
+ const endDate = React.useMemo(() => new Date(startDate.getTime() + duration * unitMinutes * 60000), [duration, startDate, unitMinutes]);
15759
+ React.useEffect(() => {
15760
+ if (!isOpen || !freeformConfig)
15761
+ return;
15762
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
15763
+ return;
15764
+ const timer = setTimeout(async () => {
15765
+ setIsCheckingAvailability(true);
15766
+ setAvailabilityError(null);
15767
+ try {
15768
+ const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/freeform-availability"), {
15769
+ method: "POST",
15770
+ headers: createApiHeaders(config, locale),
15771
+ body: JSON.stringify(createRequestBody(config, {
15772
+ eventTypeId: eventType.id,
15773
+ requestedUnits,
15774
+ start: startDate.toISOString(),
15775
+ end: endDate.toISOString(),
15776
+ })),
15777
+ });
15778
+ const data = await response.json();
15779
+ if (!response.ok) {
15780
+ setAvailability(null);
15781
+ setAvailabilityError(data.error || t("error.loadBookingData"));
15782
+ return;
15783
+ }
15784
+ setAvailability({
15785
+ available: Boolean(data.available),
15786
+ freeUnits: Number(data.freeUnits ?? 0),
15787
+ capacityUnits: Number(data.capacityUnits ?? 0),
15788
+ usedUnits: Number(data.usedUnits ?? 0),
15789
+ reservationUsedUnits: Number(data.reservationUsedUnits ?? 0),
15790
+ blockUsedUnits: Number(data.blockUsedUnits ?? 0),
15791
+ durationUnits: Number(data.durationUnits ?? 0),
15792
+ roundedDurationMinutes: Number(data.roundedDurationMinutes ?? 0),
15793
+ });
15794
+ }
15795
+ catch (_error) {
15796
+ setAvailability(null);
15797
+ setAvailabilityError(t("error.loadBookingData"));
15798
+ }
15799
+ finally {
15800
+ setIsCheckingAvailability(false);
15801
+ }
15802
+ }, 350);
15803
+ return () => clearTimeout(timer);
15804
+ }, [
15805
+ config,
15806
+ endDate,
15807
+ eventType.id,
15808
+ freeformConfig,
15809
+ isOpen,
15810
+ locale,
15811
+ requestedUnits,
15812
+ startDate,
15813
+ t,
15814
+ ]);
15815
+ const billedDurationUnits = availability?.durationUnits ?? Math.max(1, Math.ceil(duration / Math.max(1, freeformConfig?.stepDuration ?? 1)));
15816
+ const basePrice = React.useMemo(() => {
15817
+ if (!freeformConfig)
15818
+ return 0;
15819
+ if (freeformConfig.pricingModel === "perUnit") {
15820
+ return (freeformConfig.perUnitPrice ?? 0) * billedDurationUnits;
15821
+ }
15822
+ return calculateTieredPrice(billedDurationUnits, freeformConfig);
15823
+ }, [billedDurationUnits, freeformConfig]);
15824
+ const totalPrice = Math.max(0, basePrice * requestedUnits);
15825
+ const canContinueToPayment = !!freeformConfig &&
15826
+ customerName.trim().length >= 2 &&
15827
+ customerEmail.trim().length > 3 &&
15828
+ acceptTerms &&
15829
+ !!availability?.available &&
15830
+ !isCheckingAvailability;
15831
+ const freeformSelection = React.useMemo(() => ({
15832
+ eventTypeId: eventType.id,
15833
+ start: startDate.toISOString(),
15834
+ end: endDate.toISOString(),
15835
+ requestedUnits,
15836
+ customerName: customerName.trim(),
15837
+ customerEmail: customerEmail.trim(),
15838
+ customerPhone: customerPhone.trim(),
15839
+ comment: comment.trim(),
15840
+ }), [
15841
+ comment,
15842
+ customerEmail,
15843
+ customerName,
15844
+ customerPhone,
15845
+ endDate,
15846
+ eventType.id,
15847
+ requestedUnits,
15848
+ startDate,
15849
+ ]);
15850
+ const paymentFormData = React.useMemo(() => ({
15851
+ customerName: customerName.trim(),
15852
+ customerEmail: customerEmail.trim(),
15853
+ customerPhone: customerPhone.trim(),
15854
+ participants: [{ name: customerName.trim() || customerEmail.trim(), level: undefined }],
15855
+ comment: comment.trim(),
15856
+ }), [comment, customerEmail, customerName, customerPhone]);
15857
+ const eventDetailsForPayment = React.useMemo(() => ({
15858
+ id: eventType.id,
15859
+ name: eventType.name,
15860
+ startTime: startDate.toISOString(),
15861
+ endTime: endDate.toISOString(),
15862
+ price: basePrice,
15863
+ maxParticipants: availability?.capacityUnits ?? requestedUnits,
15864
+ participantCount: 0,
15865
+ availableSpots: availability?.freeUnits ?? requestedUnits,
15866
+ durationDays: 1,
15867
+ durationPerDay: availability?.roundedDurationMinutes ?? duration * unitMinutes,
15868
+ images: eventType.images ?? [],
15869
+ category: eventType.category,
15870
+ organization: {
15871
+ id: config.organizationId,
15872
+ name: "",
15873
+ },
15874
+ bookingOpen: availability?.available ?? false,
15875
+ }), [
15876
+ availability?.available,
15877
+ availability?.capacityUnits,
15878
+ availability?.freeUnits,
15879
+ availability?.roundedDurationMinutes,
15880
+ basePrice,
15881
+ config.organizationId,
15882
+ duration,
15883
+ endDate,
15884
+ eventType.category,
15885
+ eventType.id,
15886
+ eventType.images,
15887
+ eventType.name,
15888
+ requestedUnits,
15889
+ startDate,
15890
+ unitMinutes,
15891
+ ]);
15892
+ const footer = (jsxRuntime.jsxs("div", { style: { width: "100%", display: "flex", gap: "12px" }, children: [jsxRuntime.jsx("button", { type: "button", onClick: () => {
15893
+ if (checkoutStep === "payment") {
15894
+ setCheckoutStep("details");
15895
+ }
15896
+ else {
15897
+ onClose();
15898
+ }
15899
+ }, style: {
15900
+ flex: 1,
15901
+ padding: "12px 16px",
15902
+ border: "1px solid var(--bw-border-color)",
15903
+ borderRadius: "var(--bw-border-radius)",
15904
+ backgroundColor: "var(--bw-surface-color)",
15905
+ color: "var(--bw-text-color)",
15906
+ fontFamily: "var(--bw-font-family)",
15907
+ fontWeight: 600,
15908
+ cursor: "pointer",
15909
+ }, children: checkoutStep === "payment" ? t("button.backToDetails") : t("common.back") }), checkoutStep === "details" && (jsxRuntime.jsx("button", { type: "button", onClick: () => setCheckoutStep("payment"), disabled: !canContinueToPayment, style: {
15910
+ flex: 1,
15911
+ padding: "12px 16px",
15912
+ border: "none",
15913
+ borderRadius: "var(--bw-border-radius)",
15914
+ backgroundColor: "var(--bw-highlight-color)",
15915
+ color: "var(--bw-button-text-color, #ffffff)",
15916
+ fontFamily: "var(--bw-font-family)",
15917
+ fontWeight: 700,
15918
+ cursor: canContinueToPayment ? "pointer" : "not-allowed",
15919
+ opacity: canContinueToPayment ? 1 : 0.6,
15920
+ }, children: t("button.continueToPayment") }))] }));
15921
+ if (!freeformConfig) {
15922
+ return (jsxRuntime.jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: eventType.name, children: jsxRuntime.jsx("div", { style: { padding: "16px", color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: t("booking.freeformMissingConfig") }) }));
15923
+ }
15924
+ return (jsxRuntime.jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: eventType.name, footer: footer, children: jsxRuntime.jsx("div", { style: { padding: "16px", display: "flex", flexDirection: "column", gap: "16px" }, children: checkoutStep === "details" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsxRuntime.jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("booking.eventDetails") }), jsxRuntime.jsxs("div", { style: { display: "grid", gap: "10px" }, children: [jsxRuntime.jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("booking.date"), jsxRuntime.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)" } })] }), jsxRuntime.jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("booking.duration"), jsxRuntime.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 && (jsxRuntime.jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("voucher.quantity"), jsxRuntime.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)" } })] }))] }), jsxRuntime.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 && (jsxRuntime.jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: t("common.loading") })), availabilityError && (jsxRuntime.jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: availabilityError })), !availabilityError && availability && (jsxRuntime.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
15925
+ ? t("booking.freeformUnitsAvailable", { count: availability.freeUnits })
15926
+ : t("booking.freeformNotAvailable") }))] }), jsxRuntime.jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsxRuntime.jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("booking.contactInfo") }), jsxRuntime.jsxs("div", { style: { display: "grid", gap: "10px" }, children: [jsxRuntime.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)" } }), jsxRuntime.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)" } }), jsxRuntime.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)" } }), jsxRuntime.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" } }), jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontFamily: "var(--bw-font-family)", fontSize: "13px", color: "var(--bw-text-muted)" }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: acceptTerms, onChange: (event) => setAcceptTerms(event.target.checked) }), t("booking.acceptTerms"), " ", t("booking.terms")] })] })] }), jsxRuntime.jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsxRuntime.jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("summary.title") }), jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "14px" }, children: [jsxRuntime.jsx("span", { children: t("booking.duration") }), jsxRuntime.jsx("span", { children: t("booking.steps", { count: billedDurationUnits }) })] }), jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "14px", marginTop: "6px" }, children: [jsxRuntime.jsx("span", { children: t("booking.price") }), jsxRuntime.jsxs("span", { children: [formatCurrency(basePrice), " x ", requestedUnits] })] }), jsxRuntime.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: [jsxRuntime.jsx("span", { children: t("summary.totalAmount") }), jsxRuntime.jsx("span", { children: formatCurrency(totalPrice) })] })] })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(HoldCountdown, {}), jsxRuntime.jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsxRuntime.jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("summary.payment") }), !systemConfig?.paymentProvider && (jsxRuntime.jsx("p", { style: { margin: 0, color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: t("booking.paymentUnavailable") })), systemConfig?.paymentProvider === "mollie" && (jsxRuntime.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" && (jsxRuntime.jsx(StripePaymentForm, { config: config, eventDetails: eventDetailsForPayment, formData: paymentFormData, totalAmount: totalPrice, discountCode: null, giftCards: [], onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, freeformSelection: freeformSelection }))] })] })) }) }));
15927
+ }
15928
+
15412
15929
  const getThemeConfig = (theme = "generic") => {
15413
15930
  switch (theme) {
15414
15931
  case "christmas":
@@ -16049,6 +16566,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16049
16566
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = React.useState(false);
16050
16567
  const [shouldRenderUpsells, setShouldRenderUpsells] = React.useState(false);
16051
16568
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = React.useState(false);
16569
+ const [shouldRenderFreeformSelection, setShouldRenderFreeformSelection] = React.useState(false);
16052
16570
  // Google Ads config (received from API, set once from the first API response)
16053
16571
  const [googleAdsConfig, setGoogleAdsConfig] = React.useState(null);
16054
16572
  const extractGoogleAdsConfig = (data) => {
@@ -16062,6 +16580,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16062
16580
  // Voucher purchase state
16063
16581
  const [voucherConfig, setVoucherConfig] = React.useState(null);
16064
16582
  const [voucherEventTypes, setVoucherEventTypes] = React.useState([]);
16583
+ const [voucherCategories, setVoucherCategories] = React.useState([]);
16065
16584
  const [isLoadingVoucherConfig, setIsLoadingVoucherConfig] = React.useState(false);
16066
16585
  const [isVoucherFormOpen, setIsVoucherFormOpen] = React.useState(false);
16067
16586
  const [voucherPurchaseResult, setVoucherPurchaseResult] = React.useState(null);
@@ -16141,6 +16660,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16141
16660
  setVoucherConfig(mergedConfig);
16142
16661
  extractGoogleAdsConfig(data);
16143
16662
  setVoucherEventTypes(data.eventTypes || []);
16663
+ setVoucherCategories(data.categories || []);
16144
16664
  // Set system config for payment processing
16145
16665
  if (data.paymentProvider) {
16146
16666
  setSystemConfig({
@@ -16169,7 +16689,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16169
16689
  if (!analyticsInitRef.current && config.organizationId) {
16170
16690
  analyticsInitRef.current = true;
16171
16691
  initAnalytics(config.apiBaseUrl, config.organizationId, {
16172
- partnerContractId: config.partnerContractId,
16692
+ ...(config.partnerContractId ? { partnerContractId: config.partnerContractId } : {}),
16173
16693
  });
16174
16694
  trackEvent("widget_loaded", {
16175
16695
  viewMode,
@@ -16648,6 +17168,18 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16648
17168
  const handleEventTypeSelect = async (eventType) => {
16649
17169
  trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16650
17170
  setSelectedEventType(eventType);
17171
+ if (eventType.bookingMode === "freeform") {
17172
+ setCurrentStep("freeform");
17173
+ setShouldRenderFreeformSelection(true);
17174
+ setIsLoadingEventInstances(true);
17175
+ try {
17176
+ await loadEventInstances(eventType.id);
17177
+ }
17178
+ finally {
17179
+ setIsLoadingEventInstances(false);
17180
+ }
17181
+ return;
17182
+ }
16651
17183
  setCurrentStep("eventInstances");
16652
17184
  setShouldRenderInstanceSelection(true);
16653
17185
  setIsLoadingEventInstances(true);
@@ -16738,6 +17270,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16738
17270
  setSuccessPaymentId(result.paymentIntent.id);
16739
17271
  setSidebarOpen(false);
16740
17272
  setShouldRenderBookingForm(false);
17273
+ setShouldRenderFreeformSelection(false);
16741
17274
  setBookingPersistedState(null);
16742
17275
  config.onSuccess?.(result);
16743
17276
  };
@@ -17053,6 +17586,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17053
17586
  setShouldRenderInstanceSelection(false);
17054
17587
  setShouldRenderUpsells(false);
17055
17588
  setShouldRenderBookingForm(false);
17589
+ setShouldRenderFreeformSelection(false);
17056
17590
  setSelectedUpsells([]);
17057
17591
  setUpsells([]);
17058
17592
  setBookingPersistedState(null);
@@ -17074,6 +17608,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17074
17608
  setShouldRenderInstanceSelection(false);
17075
17609
  setShouldRenderUpsells(false);
17076
17610
  setShouldRenderBookingForm(false);
17611
+ setShouldRenderFreeformSelection(false);
17077
17612
  setSelectedUpsells([]);
17078
17613
  setUpsells([]);
17079
17614
  setBookingPersistedState(null);
@@ -17093,6 +17628,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17093
17628
  setSuccessPaymentId(null);
17094
17629
  setShouldRenderInstanceSelection(false);
17095
17630
  setShouldRenderBookingForm(false);
17631
+ setShouldRenderFreeformSelection(false);
17096
17632
  const url = new URL(window.location.href);
17097
17633
  url.searchParams.delete("payment_intent");
17098
17634
  url.searchParams.delete("payment_intent_client_secret");
@@ -17136,13 +17672,23 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17136
17672
  setCurrentStep("booking");
17137
17673
  setShouldRenderBookingForm(true);
17138
17674
  }
17675
+ else if (selectedEventType?.bookingMode === "freeform") {
17676
+ setCurrentStep("freeform");
17677
+ setShouldRenderFreeformSelection(true);
17678
+ }
17139
17679
  else {
17140
17680
  setCurrentStep("eventInstances");
17141
17681
  setSidebarOpen(true);
17142
17682
  setShouldRenderInstanceSelection(true);
17143
17683
  }
17144
17684
  }, children: [isButtonBusy && jsxRuntime.jsx(Spinner, { size: 20, borderColor: "var(--bw-button-text-color, #ffffff)" }), config.buttonText ||
17145
- (isDirectInstanceMode ? t("button.bookNow") : t("button.viewDates"))] }), shouldRenderInstanceSelection && (jsxRuntime.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 && (jsxRuntime.jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsxRuntime.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 })), jsxRuntime.jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17685
+ (isDirectInstanceMode || selectedEventType?.bookingMode === "freeform"
17686
+ ? t("button.bookNow")
17687
+ : t("button.viewDates"))] }), shouldRenderInstanceSelection && (jsxRuntime.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 && (jsxRuntime.jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsxRuntime.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" && (jsxRuntime.jsx(FreeformSelection, { config: config, eventType: selectedEventType, isOpen: currentStep === "freeform", onClose: () => {
17688
+ setCurrentStep("eventTypes");
17689
+ setSidebarOpen(false);
17690
+ setShouldRenderFreeformSelection(false);
17691
+ }, onSuccess: handleBookingSuccess, onError: handleBookingError, systemConfig: systemConfig })), jsxRuntime.jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17146
17692
  setIsSuccess(false);
17147
17693
  setCurrentStep("eventTypes");
17148
17694
  setSidebarOpen(false);
@@ -17150,6 +17696,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17150
17696
  setShouldRenderInstanceSelection(false);
17151
17697
  setShouldRenderUpsells(false);
17152
17698
  setShouldRenderBookingForm(false);
17699
+ setShouldRenderFreeformSelection(false);
17153
17700
  setSelectedUpsells([]);
17154
17701
  setUpsells([]);
17155
17702
  const url = new URL(window.location.href);
@@ -17162,7 +17709,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17162
17709
  }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
17163
17710
  }
17164
17711
  // Cards mode (default) - show event type selection with optional voucher card
17165
- const cardsView = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasEventSelection && (jsxRuntime.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 && (jsxRuntime.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 && (jsxRuntime.jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsxRuntime.jsx("div", { style: {
17712
+ const cardsView = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasEventSelection && (jsxRuntime.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 && (jsxRuntime.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 && (jsxRuntime.jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsxRuntime.jsx("div", { style: {
17166
17713
  display: "inline-block",
17167
17714
  width: "32px",
17168
17715
  height: "32px",
@@ -17203,13 +17750,17 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17203
17750
  };
17204
17751
  };
17205
17752
  const backHandlers = getBackHandlers();
17206
- return (jsxRuntime.jsxs(StyleProvider, { config: config, children: [jsxRuntime.jsxs("div", { ref: setWidgetContainerRef, children: [cardsView, shouldRenderInstanceSelection && (jsxRuntime.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 && (jsxRuntime.jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsxRuntime.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 })), jsxRuntime.jsx(BookingSuccessModal, { isOpen: isSuccess && !voucherPurchaseResult, onClose: () => {
17753
+ return (jsxRuntime.jsxs(StyleProvider, { config: config, children: [jsxRuntime.jsxs("div", { ref: setWidgetContainerRef, children: [cardsView, shouldRenderInstanceSelection && (jsxRuntime.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 && (jsxRuntime.jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsxRuntime.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" && (jsxRuntime.jsx(FreeformSelection, { config: config, eventType: selectedEventType, isOpen: currentStep === "freeform", onClose: () => {
17754
+ setCurrentStep("eventTypes");
17755
+ setShouldRenderFreeformSelection(false);
17756
+ }, onSuccess: handleBookingSuccess, onError: handleBookingError, systemConfig: systemConfig })), jsxRuntime.jsx(BookingSuccessModal, { isOpen: isSuccess && !voucherPurchaseResult, onClose: () => {
17207
17757
  setIsSuccess(false);
17208
17758
  setCurrentStep("eventTypes");
17209
17759
  setSuccessPaymentId(null);
17210
17760
  setShouldRenderInstanceSelection(false);
17211
17761
  setShouldRenderUpsells(false);
17212
17762
  setShouldRenderBookingForm(false);
17763
+ setShouldRenderFreeformSelection(false);
17213
17764
  setSelectedUpsells([]);
17214
17765
  setUpsells([]);
17215
17766
  const url = new URL(window.location.href);
@@ -17219,7 +17770,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17219
17770
  url.searchParams.delete("mollie_payment_id");
17220
17771
  url.searchParams.delete("mollie_status");
17221
17772
  window.history.replaceState({}, "", url.toString());
17222
- }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsxRuntime.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: () => {
17773
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsxRuntime.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: () => {
17223
17774
  setIsSuccess(false);
17224
17775
  setVoucherPurchaseResult(null);
17225
17776
  const url = new URL(window.location.href);
@@ -17267,7 +17818,7 @@ function styleInject(css, ref) {
17267
17818
  }
17268
17819
  }
17269
17820
 
17270
- 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}}";
17821
+ 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}}";
17271
17822
  styleInject(css_248z);
17272
17823
 
17273
17824
  // Export init function for vanilla JS usage