@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.esm.js CHANGED
@@ -149,6 +149,10 @@ const de$1 = {
149
149
  "booking.stepUpsells": "Extras",
150
150
  "booking.stepDetails": "Angaben",
151
151
  "booking.stepPayment": "Zahlung",
152
+ "booking.steps": "{{count}} Schritt(e)",
153
+ "booking.freeformUnitsAvailable": "{{count}} Einheiten verfuegbar",
154
+ "booking.freeformNotAvailable": "Das gewaehlte Zeitfenster ist nicht verfuegbar",
155
+ "booking.freeformMissingConfig": "Die Freeform-Konfiguration fuer diesen Event-Typ fehlt.",
152
156
  // Booking summary
153
157
  "summary.title": "Buchungszusammenfassung",
154
158
  "summary.subtotal": "Zwischensumme:",
@@ -204,6 +208,13 @@ const de$1 = {
204
208
  "voucher.monetaryTypeDesc": "Beliebigen Betrag wählen",
205
209
  "voucher.eventType": "Erlebnisgutschein",
206
210
  "voucher.eventTypeDesc": "Für ein bestimmtes Erlebnis",
211
+ "voucher.categoryType": "Kategoriegutschein",
212
+ "voucher.categoryTypeDesc": "Für ein beliebiges Erlebnis einer Kategorie",
213
+ "voucher.categoryVoucher": "Kategoriegutschein",
214
+ "voucher.selectCategory": "Kategorie wählen",
215
+ "voucher.categoryLabel": "Kategorie",
216
+ "voucher.selectCategoryPlaceholder": "Kategorie auswählen...",
217
+ "voucher.categoryAmount": "Gutscheinwert",
207
218
  "voucher.selectAmount": "Betrag wählen",
208
219
  "voucher.customAmount": "Oder eigenen Betrag eingeben",
209
220
  "voucher.amountRange": "Betrag muss zwischen {{min}} und {{max}} liegen",
@@ -457,6 +468,10 @@ const en = {
457
468
  "booking.stepUpsells": "Extras",
458
469
  "booking.stepDetails": "Details",
459
470
  "booking.stepPayment": "Payment",
471
+ "booking.steps": "{{count}} step(s)",
472
+ "booking.freeformUnitsAvailable": "{{count}} units available",
473
+ "booking.freeformNotAvailable": "Selected window is not available",
474
+ "booking.freeformMissingConfig": "Missing freeform configuration for this event type.",
460
475
  // Booking summary
461
476
  "summary.title": "Booking Summary",
462
477
  "summary.subtotal": "Subtotal:",
@@ -512,6 +527,13 @@ const en = {
512
527
  "voucher.monetaryTypeDesc": "Choose any amount",
513
528
  "voucher.eventType": "Event Voucher",
514
529
  "voucher.eventTypeDesc": "For a specific experience",
530
+ "voucher.categoryType": "Category Voucher",
531
+ "voucher.categoryTypeDesc": "For any experience in a category",
532
+ "voucher.categoryVoucher": "Category Voucher",
533
+ "voucher.selectCategory": "Select Category",
534
+ "voucher.categoryLabel": "Category",
535
+ "voucher.selectCategoryPlaceholder": "Choose a category...",
536
+ "voucher.categoryAmount": "Voucher Value",
515
537
  "voucher.selectAmount": "Select Amount",
516
538
  "voucher.customAmount": "Or enter custom amount",
517
539
  "voucher.amountRange": "Amount must be between {{min}} and {{max}}",
@@ -802,6 +824,13 @@ const es = {
802
824
  "voucher.monetaryTypeDesc": "Elige cualquier cantidad",
803
825
  "voucher.eventType": "Vale de experiencia",
804
826
  "voucher.eventTypeDesc": "Para una experiencia específica",
827
+ "voucher.categoryType": "Vale de categoría",
828
+ "voucher.categoryTypeDesc": "Para cualquier experiencia de una categoría",
829
+ "voucher.categoryVoucher": "Vale de categoría",
830
+ "voucher.selectCategory": "Seleccionar categoría",
831
+ "voucher.categoryLabel": "Categoría",
832
+ "voucher.selectCategoryPlaceholder": "Elige una categoría...",
833
+ "voucher.categoryAmount": "Valor del vale",
805
834
  "voucher.selectAmount": "Selecciona el importe",
806
835
  "voucher.customAmount": "O introduce un importe personalizado",
807
836
  "voucher.amountRange": "El importe debe estar entre {{min}} y {{max}}",
@@ -1092,6 +1121,13 @@ const pt = {
1092
1121
  "voucher.monetaryTypeDesc": "Escolha qualquer quantia",
1093
1122
  "voucher.eventType": "Vale de experiência",
1094
1123
  "voucher.eventTypeDesc": "Para uma experiência específica",
1124
+ "voucher.categoryType": "Vale de categoria",
1125
+ "voucher.categoryTypeDesc": "Para qualquer experiência de uma categoria",
1126
+ "voucher.categoryVoucher": "Vale de categoria",
1127
+ "voucher.selectCategory": "Selecionar categoria",
1128
+ "voucher.categoryLabel": "Categoria",
1129
+ "voucher.selectCategoryPlaceholder": "Escolha uma categoria...",
1130
+ "voucher.categoryAmount": "Valor do vale",
1095
1131
  "voucher.selectAmount": "Selecione o valor",
1096
1132
  "voucher.customAmount": "Ou introduza um valor personalizado",
1097
1133
  "voucher.amountRange": "O valor deve estar entre {{min}} e {{max}}",
@@ -1382,6 +1418,13 @@ const sv = {
1382
1418
  "voucher.monetaryTypeDesc": "Välj valfritt belopp",
1383
1419
  "voucher.eventType": "Upplevelsebevis",
1384
1420
  "voucher.eventTypeDesc": "För en specifik upplevelse",
1421
+ "voucher.categoryType": "Kategoripresent",
1422
+ "voucher.categoryTypeDesc": "För alla upplevelser i en kategori",
1423
+ "voucher.categoryVoucher": "Kategoripresent",
1424
+ "voucher.selectCategory": "Välj kategori",
1425
+ "voucher.categoryLabel": "Kategori",
1426
+ "voucher.selectCategoryPlaceholder": "Välj en kategori...",
1427
+ "voucher.categoryAmount": "Presentkortsvärde",
1385
1428
  "voucher.selectAmount": "Välj belopp",
1386
1429
  "voucher.customAmount": "Eller ange eget belopp",
1387
1430
  "voucher.amountRange": "Beloppet måste vara mellan {{min}} och {{max}}",
@@ -6352,7 +6395,7 @@ function loadMollieScript() {
6352
6395
  document.head.appendChild(script);
6353
6396
  });
6354
6397
  }
6355
- function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess: _onSuccess, onError, upsellSelections = [], mollieProfileId, mollieTestmode, }) {
6398
+ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess: _onSuccess, onError, upsellSelections = [], mollieProfileId, mollieTestmode, freeformSelection, }) {
6356
6399
  const t = useTranslations();
6357
6400
  const { locale } = useLocale();
6358
6401
  const [isLoading, setIsLoading] = useState(false);
@@ -6366,7 +6409,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6366
6409
  const cardFormRef = useRef(null);
6367
6410
  const cardContainerRef = useRef(null);
6368
6411
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
6369
- const isFullyCoveredByGiftCards = isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
6412
+ const isFullyCoveredByGiftCards = freeformSelection
6413
+ ? false
6414
+ : isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
6370
6415
  const isCreditCard = selectedMethod === "creditcard";
6371
6416
  useEffect(() => {
6372
6417
  if (isFullyCoveredByGiftCards)
@@ -6476,6 +6521,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6476
6521
  return (jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards, onSuccess: _onSuccess, onError: onError, upsellSelections: upsellSelections }));
6477
6522
  }
6478
6523
  const validateBeforePayment = () => {
6524
+ if (freeformSelection) {
6525
+ return null;
6526
+ }
6479
6527
  const participantCount = formData.participants.filter((p) => p.name?.trim()).length;
6480
6528
  const availableSpots = eventDetails.availableSpots || 0;
6481
6529
  if (participantCount > availableSpots) {
@@ -6487,21 +6535,41 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6487
6535
  return null;
6488
6536
  };
6489
6537
  const buildRequestData = (cardToken) => ({
6490
- eventInstanceId: config.eventInstanceId || eventDetails.id,
6491
- organizationId: config.organizationId,
6492
- amount: Math.round(totalAmount),
6493
- currency: "EUR",
6494
- participants: formData.participants.filter((p) => p.name?.trim()),
6495
- discountCode: discountCode?.code,
6496
- giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6497
- customerName: formData.customerName?.trim(),
6498
- customerEmail: formData.customerEmail?.trim(),
6499
- customerPhone: formData.customerPhone?.trim(),
6500
- comment: formData.comment?.trim(),
6501
- ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6502
- ...(cardToken && { cardToken }),
6503
- ...(selectedMethod && !cardToken && { method: selectedMethod }),
6504
- ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6538
+ ...(freeformSelection
6539
+ ? {
6540
+ eventTypeId: freeformSelection.eventTypeId,
6541
+ organizationId: config.organizationId,
6542
+ start: freeformSelection.start,
6543
+ end: freeformSelection.end,
6544
+ requestedUnits: freeformSelection.requestedUnits,
6545
+ amount: Math.round(totalAmount),
6546
+ currency: "EUR",
6547
+ customerName: freeformSelection.customerName.trim(),
6548
+ customerEmail: freeformSelection.customerEmail.trim(),
6549
+ customerPhone: freeformSelection.customerPhone?.trim(),
6550
+ comment: freeformSelection.comment?.trim(),
6551
+ locale,
6552
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6553
+ ...(cardToken && { cardToken }),
6554
+ ...(selectedMethod && !cardToken && { method: selectedMethod }),
6555
+ }
6556
+ : {
6557
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
6558
+ organizationId: config.organizationId,
6559
+ amount: Math.round(totalAmount),
6560
+ currency: "EUR",
6561
+ participants: formData.participants.filter((p) => p.name?.trim()),
6562
+ discountCode: discountCode?.code,
6563
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
6564
+ customerName: formData.customerName?.trim(),
6565
+ customerEmail: formData.customerEmail?.trim(),
6566
+ customerPhone: formData.customerPhone?.trim(),
6567
+ comment: formData.comment?.trim(),
6568
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
6569
+ ...(cardToken && { cardToken }),
6570
+ ...(selectedMethod && !cardToken && { method: selectedMethod }),
6571
+ ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
6572
+ }),
6505
6573
  });
6506
6574
  const handleCardPayment = async () => {
6507
6575
  if (!mollieRef.current)
@@ -6519,7 +6587,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6519
6587
  setPaymentError(tokenError?.message || t("error.createPayment"));
6520
6588
  return;
6521
6589
  }
6522
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-mollie-payment"), {
6590
+ const response = await fetch(getApiUrl(config.apiBaseUrl, freeformSelection
6591
+ ? "/booking/create-freeform-mollie-payment"
6592
+ : "/booking/create-mollie-payment"), {
6523
6593
  method: "POST",
6524
6594
  headers: createApiHeaders(config, locale),
6525
6595
  body: JSON.stringify(createRequestBody(config, buildRequestData(token))),
@@ -6555,7 +6625,9 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6555
6625
  setPaymentError(validationError);
6556
6626
  return;
6557
6627
  }
6558
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-mollie-payment"), {
6628
+ const response = await fetch(getApiUrl(config.apiBaseUrl, freeformSelection
6629
+ ? "/booking/create-freeform-mollie-payment"
6630
+ : "/booking/create-mollie-payment"), {
6559
6631
  method: "POST",
6560
6632
  headers: createApiHeaders(config, locale),
6561
6633
  body: JSON.stringify(createRequestBody(config, buildRequestData())),
@@ -6583,7 +6655,7 @@ function MolliePaymentForm({ config, eventDetails, formData, totalAmount, discou
6583
6655
  ? t("button.processingPayment")
6584
6656
  : selectedMethodData
6585
6657
  ? t("payment.payWithMethod", { method: selectedMethodData.description })
6586
- : totalAmount < eventDetails.price * formData.participants.filter((p) => p.name?.trim()).length
6658
+ : !freeformSelection && totalAmount < eventDetails.price * formData.participants.filter((p) => p.name?.trim()).length
6587
6659
  ? t("button.depositAndBook")
6588
6660
  : t("button.bookNow");
6589
6661
  const isPayDisabled = isLoading || !selectedMethod || (isCreditCard && !isMollieReady);
@@ -6890,12 +6962,18 @@ var loadStripe = function loadStripe() {
6890
6962
  };
6891
6963
 
6892
6964
  // Inner component that uses the Stripe hooks
6893
- function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onError, }) {
6965
+ function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onError, freeformSelection, }) {
6894
6966
  const t = useTranslations();
6895
- const participantCount = formData.participants.filter((p) => p.name.trim()).length;
6896
- const fullAmount = eventDetails.price * participantCount;
6897
- const submitLabel = totalAmount < fullAmount ? t("button.depositAndBook") : t("button.bookNow");
6967
+ const isFreeform = Boolean(freeformSelection);
6968
+ const participantCount = isFreeform
6969
+ ? 1
6970
+ : formData.participants.filter((p) => p.name.trim()).length;
6971
+ const fullAmount = isFreeform ? totalAmount : eventDetails.price * participantCount;
6972
+ const submitLabel = !isFreeform && totalAmount < fullAmount ? t("button.depositAndBook") : t("button.bookNow");
6898
6973
  return (jsx(StripeElementsPaymentForm, { onValidate: () => {
6974
+ if (isFreeform) {
6975
+ return null;
6976
+ }
6899
6977
  const availableSpots = eventDetails.availableSpots || 0;
6900
6978
  if (participantCount > availableSpots) {
6901
6979
  return t("payment.tooManyParticipants", { count: participantCount, available: availableSpots });
@@ -6917,7 +6995,7 @@ function PaymentFormInner({ eventDetails, formData, totalAmount, onSuccess, onEr
6917
6995
  },
6918
6996
  }, submitContent: jsx(Fragment, { children: submitLabel }), loadingContent: jsxs(Fragment, { children: [spinner("var(--bw-surface-color)"), " ", t("button.processingPayment")] }) }));
6919
6997
  }
6920
- function StripePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripeAppearance, upsellSelections = [], }) {
6998
+ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripeAppearance, upsellSelections = [], freeformSelection, }) {
6921
6999
  const t = useTranslations();
6922
7000
  const { locale } = useLocale();
6923
7001
  const [clientSecret, setClientSecret] = useState(null);
@@ -6937,7 +7015,9 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
6937
7015
  return loadStripe(systemConfig.stripePublishableKey, stripeOptions);
6938
7016
  }, [systemConfig?.stripePublishableKey, systemConfig?.connectedAccountId, locale]);
6939
7017
  const storageKey = typeof window !== "undefined"
6940
- ? `bw_pi_${config?.organizationId}_${config?.eventInstanceId || eventDetails?.id}`
7018
+ ? freeformSelection
7019
+ ? `bw_pi_freeform_${config?.organizationId}_${freeformSelection.eventTypeId}_${freeformSelection.start}_${freeformSelection.end}_${freeformSelection.requestedUnits}`
7020
+ : `bw_pi_${config?.organizationId}_${config?.eventInstanceId || eventDetails?.id}`
6941
7021
  : "";
6942
7022
  const PAYMENT_INTENT_TTL = 24 * 60 * 60 * 1000;
6943
7023
  function loadPersistedPaymentIntent() {
@@ -6992,48 +7072,82 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
6992
7072
  }, [paymentIntentId]);
6993
7073
  useEffect(() => {
6994
7074
  const createStripePayment = async () => {
6995
- if (!systemConfig || !eventDetails || !formData.participants?.length) {
7075
+ if (!systemConfig || !eventDetails) {
7076
+ return;
7077
+ }
7078
+ if (freeformSelection) {
7079
+ if (!freeformSelection.customerEmail?.trim() ||
7080
+ !freeformSelection.customerName?.trim() ||
7081
+ !freeformSelection.start ||
7082
+ !freeformSelection.end) {
7083
+ return;
7084
+ }
7085
+ }
7086
+ else if (!formData.participants?.length) {
6996
7087
  return;
6997
7088
  }
6998
7089
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
6999
- if (participantCount === 0 || !formData.customerEmail?.trim() || !formData.customerName?.trim()) {
7090
+ if (!freeformSelection &&
7091
+ (participantCount === 0 || !formData.customerEmail?.trim() || !formData.customerName?.trim())) {
7000
7092
  return;
7001
7093
  }
7002
7094
  setIsCreatingPaymentIntent(true);
7003
7095
  setPaymentError(null);
7004
7096
  try {
7005
- const requestData = {
7006
- eventInstanceId: config.eventInstanceId || eventDetails.id,
7007
- organizationId: config.organizationId,
7008
- amount: Math.round(totalAmount),
7009
- currency: "eur",
7010
- participants: formData.participants.filter((p) => p.name?.trim()),
7011
- discountCode: discountCode?.code,
7012
- giftCardCodes: giftCards?.map((gc) => gc.code) || [],
7013
- customerName: formData.customerName?.trim(),
7014
- customerEmail: formData.customerEmail?.trim(),
7015
- customerPhone: formData.customerPhone?.trim(),
7016
- comment: formData.comment?.trim(),
7017
- ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7018
- ...(paymentIntentId && { paymentIntentId }),
7019
- ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
7020
- };
7021
- if (!requestData.eventInstanceId) {
7022
- throw new Error("Event instance ID is required");
7023
- }
7097
+ const endpoint = freeformSelection
7098
+ ? "/booking/create-freeform-stripe-payment"
7099
+ : "/booking/create-stripe-payment";
7100
+ const requestData = freeformSelection
7101
+ ? {
7102
+ eventTypeId: freeformSelection.eventTypeId,
7103
+ organizationId: config.organizationId,
7104
+ start: freeformSelection.start,
7105
+ end: freeformSelection.end,
7106
+ requestedUnits: freeformSelection.requestedUnits,
7107
+ amount: Math.round(totalAmount),
7108
+ currency: "eur",
7109
+ customerName: freeformSelection.customerName.trim(),
7110
+ customerEmail: freeformSelection.customerEmail.trim(),
7111
+ customerPhone: freeformSelection.customerPhone?.trim(),
7112
+ comment: freeformSelection.comment?.trim(),
7113
+ locale,
7114
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7115
+ ...(paymentIntentId && { paymentIntentId }),
7116
+ }
7117
+ : {
7118
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
7119
+ organizationId: config.organizationId,
7120
+ amount: Math.round(totalAmount),
7121
+ currency: "eur",
7122
+ participants: formData.participants.filter((p) => p.name?.trim()),
7123
+ discountCode: discountCode?.code,
7124
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
7125
+ customerName: formData.customerName?.trim(),
7126
+ customerEmail: formData.customerEmail?.trim(),
7127
+ customerPhone: formData.customerPhone?.trim(),
7128
+ comment: formData.comment?.trim(),
7129
+ ...(config.partnerContractId && { partnerContractId: config.partnerContractId }),
7130
+ ...(paymentIntentId && { paymentIntentId }),
7131
+ ...(upsellSelections && upsellSelections.length > 0 && { upsellSelections }),
7132
+ };
7024
7133
  if (!requestData.organizationId) {
7025
7134
  throw new Error("Organization ID is required");
7026
7135
  }
7027
- if (!requestData.amount || requestData.amount <= 0) {
7136
+ if (requestData.amount === undefined || requestData.amount < 0) {
7028
7137
  throw new Error("Valid amount is required");
7029
7138
  }
7030
- if (!requestData.participants || requestData.participants.length === 0) {
7139
+ if (!freeformSelection && "eventInstanceId" in requestData && !requestData.eventInstanceId) {
7140
+ throw new Error("Event instance ID is required");
7141
+ }
7142
+ if (!freeformSelection &&
7143
+ "participants" in requestData &&
7144
+ (!requestData.participants || requestData.participants.length === 0)) {
7031
7145
  throw new Error("At least one participant is required");
7032
7146
  }
7033
7147
  if (!requestData.customerEmail) {
7034
7148
  throw new Error("Customer email is required");
7035
7149
  }
7036
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-stripe-payment"), {
7150
+ const response = await fetch(getApiUrl(config.apiBaseUrl, endpoint), {
7037
7151
  method: "POST",
7038
7152
  headers: createApiHeaders(config, locale),
7039
7153
  body: JSON.stringify(createRequestBody(config, requestData)),
@@ -7078,9 +7192,13 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7078
7192
  giftCards,
7079
7193
  config,
7080
7194
  upsellSelections,
7195
+ freeformSelection,
7196
+ locale,
7081
7197
  ]);
7082
7198
  const participantCount = formData.participants?.filter((p) => p.name?.trim()).length || 0;
7083
- const isFullyCoveredByGiftCards = isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
7199
+ const isFullyCoveredByGiftCards = freeformSelection
7200
+ ? false
7201
+ : isGiftCardFullyCovered(giftCards, eventDetails?.price || 0, participantCount, discountCode?.discountAmount || 0);
7084
7202
  if (isFullyCoveredByGiftCards && totalAmount <= 0) {
7085
7203
  return (jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards, onSuccess: onSuccess, onError: onError, upsellSelections: upsellSelections }));
7086
7204
  }
@@ -7119,7 +7237,7 @@ function StripePaymentForm({ config, eventDetails, formData, totalAmount, discou
7119
7237
  setPaymentIntentId(null);
7120
7238
  setClientSecret(null);
7121
7239
  onSuccess(result);
7122
- }, onError: onError }) }));
7240
+ }, onError: onError, ...(freeformSelection ? { freeformSelection } : {}) }) }));
7123
7241
  }
7124
7242
 
7125
7243
  const HOLD_DURATION_SECONDS = 15 * 60;
@@ -13379,20 +13497,22 @@ const sectionHeaderStyles = sectionStyles.header;
13379
13497
  const labelStyles = formStyles.label;
13380
13498
  const inputStyles = formStyles.input;
13381
13499
  const errorTextStyles = formStyles.error;
13382
- function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClose, onSuccess, onError, systemConfig, preselectedEventTypeId, isLoadingEventTypes = false, }) {
13500
+ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, categories, isOpen, onClose, onSuccess, onError, systemConfig, preselectedEventTypeId, isLoadingEventTypes = false, }) {
13383
13501
  const t = useTranslations();
13384
13502
  const { locale } = useLocale();
13385
13503
  const paymentProvider = systemConfig?.paymentProvider ?? "stripe";
13386
13504
  // Form state
13387
- // Show voucher type selection if both monetary and event vouchers are allowed
13388
- // The event type selection will handle the case when no event types are available
13389
13505
  const canUseMonetaryVoucherType = voucherConfig.allowMonetaryVouchers !== false;
13390
13506
  const canUseEventVoucherType = voucherConfig.allowEventVouchers;
13391
- const showVoucherTypeSelection = canUseMonetaryVoucherType && canUseEventVoucherType && !isLoadingEventTypes;
13507
+ const canUseCategoryVoucherType = canUseEventVoucherType && categories.length > 0;
13508
+ const availableTypeCount = [canUseMonetaryVoucherType, canUseEventVoucherType, canUseCategoryVoucherType].filter(Boolean).length;
13509
+ const showVoucherTypeSelection = availableTypeCount > 1 && !isLoadingEventTypes;
13392
13510
  const [voucherType, setVoucherType] = useState(canUseMonetaryVoucherType ? "monetary" : "event");
13393
13511
  const [monetaryAmount, setMonetaryAmount] = useState(voucherConfig.monetaryPresets[2] || 10000);
13394
13512
  const [selectedEventTypeId, setSelectedEventTypeId] = useState(null);
13395
13513
  const [eventQuantity, setEventQuantity] = useState(1);
13514
+ const [selectedCategoryId, setSelectedCategoryId] = useState(null);
13515
+ const [categoryAmount, setCategoryAmount] = useState(10000);
13396
13516
  const [recipientName, setRecipientName] = useState("");
13397
13517
  const [message, setMessage] = useState("");
13398
13518
  const [customerName, setCustomerName] = useState("");
@@ -13452,6 +13572,9 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13452
13572
  if (voucherType === "monetary") {
13453
13573
  return monetaryAmount;
13454
13574
  }
13575
+ else if (voucherType === "category") {
13576
+ return categoryAmount;
13577
+ }
13455
13578
  else {
13456
13579
  const eventType = eventTypes.find((et) => et.id === selectedEventTypeId);
13457
13580
  if (eventType) {
@@ -13459,7 +13582,7 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13459
13582
  }
13460
13583
  return 0;
13461
13584
  }
13462
- }, [voucherType, monetaryAmount, selectedEventTypeId, eventQuantity, eventTypes]);
13585
+ }, [voucherType, monetaryAmount, selectedEventTypeId, eventQuantity, eventTypes, categoryAmount]);
13463
13586
  const selectedEventType = useMemo(() => {
13464
13587
  return eventTypes.find((et) => et.id === selectedEventTypeId);
13465
13588
  }, [eventTypes, selectedEventTypeId]);
@@ -13468,6 +13591,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13468
13591
  voucherType,
13469
13592
  selectedEventTypeId,
13470
13593
  eventQuantity,
13594
+ selectedCategoryId,
13595
+ categoryAmount,
13471
13596
  recipientName: recipientName.trim(),
13472
13597
  message: message.trim(),
13473
13598
  customerName: customerName.trim(),
@@ -13477,6 +13602,8 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13477
13602
  voucherType,
13478
13603
  selectedEventTypeId,
13479
13604
  eventQuantity,
13605
+ selectedCategoryId,
13606
+ categoryAmount,
13480
13607
  recipientName,
13481
13608
  message,
13482
13609
  customerName,
@@ -13495,24 +13622,37 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13495
13622
  return false;
13496
13623
  if (voucherType === "event" && !selectedEventTypeId)
13497
13624
  return false;
13625
+ if (voucherType === "category" && !selectedCategoryId)
13626
+ return false;
13498
13627
  return true;
13499
- }, [customerName, customerEmail, acceptTerms, totalAmount, voucherConfig, voucherType, selectedEventTypeId]);
13628
+ }, [customerName, customerEmail, acceptTerms, totalAmount, voucherType, selectedEventTypeId, selectedCategoryId]);
13500
13629
  const defaultMonetaryAmount = useMemo(() => voucherConfig.monetaryPresets[2] || voucherConfig.monetaryPresets[0] || 1000, [voucherConfig.monetaryPresets]);
13501
13630
  const handleVoucherTypeChange = useCallback((nextType) => {
13502
13631
  setVoucherType(nextType);
13503
13632
  setPaymentError(null);
13504
13633
  if (nextType === "event") {
13505
- // Keep voucher type mutually exclusive: entering event flow resets amount selection state.
13506
13634
  setMonetaryAmount(defaultMonetaryAmount);
13635
+ setSelectedCategoryId(null);
13636
+ setCategoryAmount(10000);
13507
13637
  if (eventTypes.length === 1) {
13508
13638
  setSelectedEventTypeId(eventTypes[0]?.id ?? null);
13509
13639
  }
13510
13640
  return;
13511
13641
  }
13512
- // Keep voucher type mutually exclusive: entering amount flow clears event selection.
13642
+ if (nextType === "category") {
13643
+ setMonetaryAmount(defaultMonetaryAmount);
13644
+ setSelectedEventTypeId(null);
13645
+ setEventQuantity(1);
13646
+ if (categories.length === 1) {
13647
+ setSelectedCategoryId(categories[0]?.id ?? null);
13648
+ }
13649
+ return;
13650
+ }
13513
13651
  setSelectedEventTypeId(null);
13514
13652
  setEventQuantity(1);
13515
- }, [defaultMonetaryAmount, eventTypes]);
13653
+ setSelectedCategoryId(null);
13654
+ setCategoryAmount(10000);
13655
+ }, [defaultMonetaryAmount, eventTypes, categories]);
13516
13656
  useEffect(() => {
13517
13657
  if (!canUseMonetaryVoucherType && voucherType === "monetary") {
13518
13658
  setVoucherType("event");
@@ -13553,9 +13693,12 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13553
13693
  organizationId: config.organizationId,
13554
13694
  amount: totalAmount,
13555
13695
  currency: "eur",
13556
- voucherType,
13557
- monetaryValue: voucherType === "monetary" ? totalAmount : undefined,
13696
+ voucherType: voucherType === "category" ? "monetary" : voucherType,
13697
+ monetaryValue: voucherType === "monetary" ? totalAmount
13698
+ : voucherType === "category" ? categoryAmount
13699
+ : undefined,
13558
13700
  eventTypeId: voucherType === "event" ? selectedEventTypeId : undefined,
13701
+ eventCategoryId: voucherType === "category" ? selectedCategoryId : undefined,
13559
13702
  eventAmount: voucherType === "event" ? eventQuantity : undefined,
13560
13703
  recipientName: recipientName.trim() || undefined,
13561
13704
  message: message.trim() || undefined,
@@ -13746,7 +13889,30 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13746
13889
  fontSize: "13px",
13747
13890
  color: "var(--bw-text-muted)",
13748
13891
  marginTop: "4px",
13749
- }, children: t("voucher.eventTypeDesc") })] }))] })] })), voucherType === "monetary" && (jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectAmount") }), jsx("div", { style: {
13892
+ }, children: t("voucher.eventTypeDesc") })] })), canUseCategoryVoucherType && (jsxs("button", { type: "button", onClick: () => handleVoucherTypeChange("category"), style: {
13893
+ flex: 1,
13894
+ padding: "16px",
13895
+ borderRadius: "var(--bw-border-radius)",
13896
+ border: voucherType === "category"
13897
+ ? "2px solid var(--bw-highlight-color)"
13898
+ : "1px solid var(--bw-border-color)",
13899
+ backgroundColor: voucherType === "category"
13900
+ ? "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)"
13901
+ : "var(--bw-surface-color)",
13902
+ cursor: "pointer",
13903
+ fontFamily: "var(--bw-font-family)",
13904
+ transition: "all 0.2s ease",
13905
+ textAlign: "center",
13906
+ }, children: [jsx("div", { style: {
13907
+ fontWeight: 600,
13908
+ color: voucherType === "category"
13909
+ ? "var(--bw-highlight-color)"
13910
+ : "var(--bw-text-color)",
13911
+ }, children: t("voucher.categoryType") }), jsx("div", { style: {
13912
+ fontSize: "13px",
13913
+ color: "var(--bw-text-muted)",
13914
+ marginTop: "4px",
13915
+ }, children: t("voucher.categoryTypeDesc") })] }))] })] })), voucherType === "monetary" && (jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectAmount") }), jsx("div", { style: {
13750
13916
  display: "grid",
13751
13917
  gridTemplateColumns: "repeat(3, 1fr)",
13752
13918
  gap: "8px",
@@ -13801,7 +13967,50 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13801
13967
  fontSize: "18px",
13802
13968
  color: "var(--bw-highlight-color)",
13803
13969
  fontFamily: "var(--bw-font-family)",
13804
- }, children: formatCurrency(totalAmount) })] }) }))] })), jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.recipientSection") }), jsx("p", { style: {
13970
+ }, children: formatCurrency(totalAmount) })] }) }))] })), voucherType === "category" && (jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.selectCategory") }), jsx("div", { style: { marginTop: "12px" }, children: jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [jsxs("div", { children: [jsx("label", { htmlFor: "category-select", style: labelStyles, children: t("voucher.categoryLabel") }), jsxs("select", { id: "category-select", value: selectedCategoryId || "", onChange: (e) => setSelectedCategoryId(e.target.value ? Number(e.target.value) : null), disabled: categories.length === 1, style: {
13971
+ ...inputStyles,
13972
+ cursor: categories.length === 1 ? "not-allowed" : "pointer",
13973
+ }, children: [categories.length > 1 && (jsx("option", { value: "", children: t("voucher.selectCategoryPlaceholder") })), categories.map((cat) => (jsx("option", { value: cat.id, children: cat.name }, cat.id)))] })] }), selectedCategoryId && (jsxs("div", { children: [jsx("label", { htmlFor: "category-amount", style: labelStyles, children: t("voucher.categoryAmount") }), jsx("div", { style: {
13974
+ display: "grid",
13975
+ gridTemplateColumns: "repeat(3, 1fr)",
13976
+ gap: "8px",
13977
+ marginTop: "4px",
13978
+ }, children: voucherConfig.monetaryPresets.map((preset) => (jsx("button", { type: "button", onClick: () => setCategoryAmount(preset), style: {
13979
+ padding: "10px",
13980
+ borderRadius: "var(--bw-border-radius)",
13981
+ border: categoryAmount === preset
13982
+ ? "2px solid var(--bw-highlight-color)"
13983
+ : "1px solid var(--bw-border-color)",
13984
+ backgroundColor: categoryAmount === preset
13985
+ ? "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)"
13986
+ : "var(--bw-surface-color)",
13987
+ cursor: "pointer",
13988
+ fontFamily: "var(--bw-font-family)",
13989
+ fontWeight: 600,
13990
+ fontSize: "14px",
13991
+ color: categoryAmount === preset
13992
+ ? "var(--bw-highlight-color)"
13993
+ : "var(--bw-text-color)",
13994
+ transition: "all 0.2s ease",
13995
+ }, children: formatCurrency(preset) }, preset))) })] }))] }) }), selectedCategoryId && categoryAmount > 0 && (jsx("div", { style: {
13996
+ marginTop: "16px",
13997
+ padding: "12px",
13998
+ backgroundColor: "rgba(var(--bw-highlight-color-rgb, 0, 177, 170), 0.1)",
13999
+ borderRadius: "var(--bw-border-radius)",
14000
+ border: "1px solid var(--bw-highlight-color)",
14001
+ }, children: jsxs("div", { style: {
14002
+ display: "flex",
14003
+ justifyContent: "space-between",
14004
+ alignItems: "center",
14005
+ }, children: [jsx("span", { style: {
14006
+ color: "var(--bw-text-color)",
14007
+ fontFamily: "var(--bw-font-family)",
14008
+ }, children: categories.find((c) => c.id === selectedCategoryId)?.name }), jsx("span", { style: {
14009
+ fontWeight: 700,
14010
+ fontSize: "18px",
14011
+ color: "var(--bw-highlight-color)",
14012
+ fontFamily: "var(--bw-font-family)",
14013
+ }, children: formatCurrency(categoryAmount) })] }) }))] })), jsxs("div", { style: cardStyles, children: [jsx("h2", { style: sectionHeaderStyles, children: t("voucher.recipientSection") }), jsx("p", { style: {
13805
14014
  fontSize: "13px",
13806
14015
  color: "var(--bw-text-muted)",
13807
14016
  fontFamily: "var(--bw-font-family)",
@@ -13834,7 +14043,9 @@ function VoucherPurchaseForm({ config, voucherConfig, eventTypes, isOpen, onClos
13834
14043
  fontFamily: "var(--bw-font-family)",
13835
14044
  }, children: voucherType === "monetary"
13836
14045
  ? t("voucher.monetaryVoucher")
13837
- : `${eventQuantity}x ${selectedEventType?.name || t("voucher.eventVoucher")}` }), recipientName && (jsxs("div", { style: {
14046
+ : voucherType === "category"
14047
+ ? categories.find((c) => c.id === selectedCategoryId)?.name || t("voucher.categoryVoucher")
14048
+ : `${eventQuantity}x ${selectedEventType?.name || t("voucher.eventVoucher")}` }), recipientName && (jsxs("div", { style: {
13838
14049
  fontSize: "13px",
13839
14050
  color: "var(--bw-text-muted)",
13840
14051
  fontFamily: "var(--bw-font-family)",
@@ -14110,7 +14321,7 @@ function VoucherAttachment({ onClick }) {
14110
14321
  }, children: t("voucher.buyAsGift") })] }));
14111
14322
  }
14112
14323
 
14113
- function VoucherIntegration({ config, voucherConfig, eventTypes, systemConfig, isFormOpen, isLoadingConfig, preselectedEventTypeId, voucherPurchaseResult, isSuccess, showStandaloneCard, onCardClick, onFormClose, onSuccess, onError, onSuccessModalClose, }) {
14324
+ function VoucherIntegration({ config, voucherConfig, eventTypes, categories, systemConfig, isFormOpen, isLoadingConfig, preselectedEventTypeId, voucherPurchaseResult, isSuccess, showStandaloneCard, onCardClick, onFormClose, onSuccess, onError, onSuccessModalClose, }) {
14114
14325
  if (!voucherConfig?.enabled) {
14115
14326
  return null;
14116
14327
  }
@@ -14121,7 +14332,7 @@ function VoucherIntegration({ config, voucherConfig, eventTypes, systemConfig, i
14121
14332
  justifyContent: "center",
14122
14333
  }, children: jsx(VoucherPurchaseCard, { config: voucherConfig, minEventPrice: eventTypes.length > 0
14123
14334
  ? Math.min(...eventTypes.map((et) => et.maxPrice))
14124
- : undefined, fallbackImages: eventTypes.flatMap((eventType) => eventType.images || []), onClick: onCardClick, standalone: true }) }) })), jsx(VoucherPurchaseForm, { config: config, voucherConfig: voucherConfig, eventTypes: eventTypes, isOpen: isFormOpen, onClose: onFormClose, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, preselectedEventTypeId: preselectedEventTypeId, isLoadingEventTypes: isLoadingConfig }), isSuccess && voucherPurchaseResult && (jsx(VoucherSuccessModal, { isOpen: true, onClose: onSuccessModalClose, result: voucherPurchaseResult }))] }));
14335
+ : undefined, fallbackImages: eventTypes.flatMap((eventType) => eventType.images || []), onClick: onCardClick, standalone: true }) }) })), jsx(VoucherPurchaseForm, { config: config, voucherConfig: voucherConfig, eventTypes: eventTypes, categories: categories, isOpen: isFormOpen, onClose: onFormClose, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, preselectedEventTypeId: preselectedEventTypeId, isLoadingEventTypes: isLoadingConfig }), isSuccess && voucherPurchaseResult && (jsx(VoucherSuccessModal, { isOpen: true, onClose: onSuccessModalClose, result: voucherPurchaseResult }))] }));
14125
14336
  }
14126
14337
 
14127
14338
  // Helper function to preprocess markdown for underline support
@@ -15389,6 +15600,312 @@ function SpecialsView({ specials, onEventSelect, isLoading = false, showSavingsA
15389
15600
  }) })] }));
15390
15601
  }
15391
15602
 
15603
+ function toMinutesForUnit(unit) {
15604
+ switch (unit.toLowerCase()) {
15605
+ case "minute":
15606
+ case "minutes":
15607
+ return 1;
15608
+ case "hour":
15609
+ case "hours":
15610
+ return 60;
15611
+ case "day":
15612
+ case "days":
15613
+ return 24 * 60;
15614
+ default:
15615
+ return 1;
15616
+ }
15617
+ }
15618
+ // --- Timezone helpers ------------------------------------------------------
15619
+ // The booking is evaluated server-side against the organization's local
15620
+ // operating hours. The customer's browser may be in a different timezone, so we
15621
+ // interpret the datetime-local field as a wall-clock time in the ORG timezone
15622
+ // (not the browser's) and convert to a UTC instant for the API.
15623
+ function getTzParts(date, timeZone) {
15624
+ const dtf = new Intl.DateTimeFormat("en-CA", {
15625
+ timeZone,
15626
+ hour12: false,
15627
+ year: "numeric",
15628
+ month: "2-digit",
15629
+ day: "2-digit",
15630
+ hour: "2-digit",
15631
+ minute: "2-digit",
15632
+ second: "2-digit",
15633
+ });
15634
+ const parts = {};
15635
+ for (const part of dtf.formatToParts(date)) {
15636
+ if (part.type !== "literal")
15637
+ parts[part.type] = part.value;
15638
+ }
15639
+ return parts;
15640
+ }
15641
+ function tzOffsetMs(utcMs, timeZone) {
15642
+ const p = getTzParts(new Date(utcMs), timeZone);
15643
+ const hour = p.hour === "24" ? "00" : p.hour;
15644
+ const asLocalUtc = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(hour), Number(p.minute), Number(p.second));
15645
+ return asLocalUtc - utcMs;
15646
+ }
15647
+ /** Convert an org-tz wall-clock string ("YYYY-MM-DDTHH:mm") to a UTC instant. */
15648
+ function zonedWallTimeToUtc(wallTime, timeZone) {
15649
+ const [datePart, timePart] = wallTime.split("T");
15650
+ if (!datePart || !timePart)
15651
+ return new Date(NaN);
15652
+ const [y, mo, d] = datePart.split("-").map(Number);
15653
+ const [h, mi] = timePart.split(":").map(Number);
15654
+ const naiveUtc = Date.UTC(y, (mo ?? 1) - 1, d, h ?? 0, mi ?? 0);
15655
+ // Single-pass offset correction (good enough outside the ~1h DST fold).
15656
+ const offset = tzOffsetMs(naiveUtc, timeZone);
15657
+ return new Date(naiveUtc - offset);
15658
+ }
15659
+ /** Render a UTC instant as an org-tz datetime-local input value. */
15660
+ function utcToZonedInputValue(date, timeZone) {
15661
+ const p = getTzParts(date, timeZone);
15662
+ const hour = p.hour === "24" ? "00" : p.hour;
15663
+ return `${p.year}-${p.month}-${p.day}T${hour}:${p.minute}`;
15664
+ }
15665
+ /** Format a UTC instant for display in the org timezone. */
15666
+ function formatInTimeZone(date, timeZone, locale) {
15667
+ return new Intl.DateTimeFormat(locale, {
15668
+ timeZone,
15669
+ dateStyle: "medium",
15670
+ timeStyle: "short",
15671
+ }).format(date);
15672
+ }
15673
+ function calculateTieredPrice(durationUnits, config) {
15674
+ const tiers = [...(config.tiers ?? [])]
15675
+ .filter((tier) => tier.unitCount > 0 && tier.price >= 0)
15676
+ .sort((a, b) => b.unitCount - a.unitCount);
15677
+ if (tiers.length === 0) {
15678
+ return 0;
15679
+ }
15680
+ let total = 0;
15681
+ let remaining = durationUnits;
15682
+ for (const tier of tiers) {
15683
+ const count = Math.floor(remaining / tier.unitCount);
15684
+ if (count <= 0)
15685
+ continue;
15686
+ total += count * tier.price;
15687
+ remaining -= count * tier.unitCount;
15688
+ }
15689
+ if (remaining > 0) {
15690
+ if (typeof config.perUnitPrice === "number") {
15691
+ total += remaining * config.perUnitPrice;
15692
+ }
15693
+ else {
15694
+ total += tiers[tiers.length - 1]?.price ?? 0;
15695
+ }
15696
+ }
15697
+ return total;
15698
+ }
15699
+ function FreeformSelection({ config, eventType, systemConfig, isOpen, onClose, onSuccess, onError, }) {
15700
+ const t = useTranslations();
15701
+ const { locale } = useLocale();
15702
+ const freeformConfig = eventType.freeformConfig;
15703
+ const timeZone = eventType.organizationTimezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
15704
+ const isNamedResource = eventType.resourceKind === "named";
15705
+ const maxUnits = isNamedResource ? 1 : Math.max(1, eventType.resourceQuantity ?? 1);
15706
+ const now = useMemo(() => {
15707
+ const next = new Date();
15708
+ next.setMinutes(next.getMinutes() + 30, 0, 0);
15709
+ return next;
15710
+ }, []);
15711
+ const [startInput, setStartInput] = useState(utcToZonedInputValue(now, timeZone));
15712
+ const [duration, setDuration] = useState(Math.max(1, freeformConfig?.minDuration ?? 1));
15713
+ const [requestedUnits, setRequestedUnits] = useState(1);
15714
+ const [customerName, setCustomerName] = useState("");
15715
+ const [customerEmail, setCustomerEmail] = useState("");
15716
+ const [customerPhone, setCustomerPhone] = useState("");
15717
+ const [comment, setComment] = useState("");
15718
+ const [acceptTerms, setAcceptTerms] = useState(false);
15719
+ const [checkoutStep, setCheckoutStep] = useState("details");
15720
+ const [availability, setAvailability] = useState(null);
15721
+ const [availabilityError, setAvailabilityError] = useState(null);
15722
+ const [isCheckingAvailability, setIsCheckingAvailability] = useState(false);
15723
+ useEffect(() => {
15724
+ if (!isOpen)
15725
+ return;
15726
+ setCheckoutStep("details");
15727
+ }, [isOpen]);
15728
+ useEffect(() => {
15729
+ if (!freeformConfig)
15730
+ return;
15731
+ setDuration(Math.max(1, freeformConfig.minDuration));
15732
+ setRequestedUnits(1);
15733
+ setAvailability(null);
15734
+ setAvailabilityError(null);
15735
+ }, [freeformConfig?.resourceId]);
15736
+ const unitMinutes = toMinutesForUnit(freeformConfig?.unit ?? "hour");
15737
+ const startDate = useMemo(() => zonedWallTimeToUtc(startInput, timeZone), [startInput, timeZone]);
15738
+ const endDate = useMemo(() => new Date(startDate.getTime() + duration * unitMinutes * 60000), [duration, startDate, unitMinutes]);
15739
+ useEffect(() => {
15740
+ if (!isOpen || !freeformConfig)
15741
+ return;
15742
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
15743
+ return;
15744
+ const timer = setTimeout(async () => {
15745
+ setIsCheckingAvailability(true);
15746
+ setAvailabilityError(null);
15747
+ try {
15748
+ const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/freeform-availability"), {
15749
+ method: "POST",
15750
+ headers: createApiHeaders(config, locale),
15751
+ body: JSON.stringify(createRequestBody(config, {
15752
+ eventTypeId: eventType.id,
15753
+ requestedUnits,
15754
+ start: startDate.toISOString(),
15755
+ end: endDate.toISOString(),
15756
+ })),
15757
+ });
15758
+ const data = await response.json();
15759
+ if (!response.ok) {
15760
+ setAvailability(null);
15761
+ setAvailabilityError(data.error || t("error.loadBookingData"));
15762
+ return;
15763
+ }
15764
+ setAvailability({
15765
+ available: Boolean(data.available),
15766
+ freeUnits: Number(data.freeUnits ?? 0),
15767
+ capacityUnits: Number(data.capacityUnits ?? 0),
15768
+ usedUnits: Number(data.usedUnits ?? 0),
15769
+ reservationUsedUnits: Number(data.reservationUsedUnits ?? 0),
15770
+ blockUsedUnits: Number(data.blockUsedUnits ?? 0),
15771
+ durationUnits: Number(data.durationUnits ?? 0),
15772
+ roundedDurationMinutes: Number(data.roundedDurationMinutes ?? 0),
15773
+ });
15774
+ }
15775
+ catch (_error) {
15776
+ setAvailability(null);
15777
+ setAvailabilityError(t("error.loadBookingData"));
15778
+ }
15779
+ finally {
15780
+ setIsCheckingAvailability(false);
15781
+ }
15782
+ }, 350);
15783
+ return () => clearTimeout(timer);
15784
+ }, [
15785
+ config,
15786
+ endDate,
15787
+ eventType.id,
15788
+ freeformConfig,
15789
+ isOpen,
15790
+ locale,
15791
+ requestedUnits,
15792
+ startDate,
15793
+ t,
15794
+ ]);
15795
+ const billedDurationUnits = availability?.durationUnits ?? Math.max(1, Math.ceil(duration / Math.max(1, freeformConfig?.stepDuration ?? 1)));
15796
+ const basePrice = useMemo(() => {
15797
+ if (!freeformConfig)
15798
+ return 0;
15799
+ if (freeformConfig.pricingModel === "perUnit") {
15800
+ return (freeformConfig.perUnitPrice ?? 0) * billedDurationUnits;
15801
+ }
15802
+ return calculateTieredPrice(billedDurationUnits, freeformConfig);
15803
+ }, [billedDurationUnits, freeformConfig]);
15804
+ const totalPrice = Math.max(0, basePrice * requestedUnits);
15805
+ const canContinueToPayment = !!freeformConfig &&
15806
+ customerName.trim().length >= 2 &&
15807
+ customerEmail.trim().length > 3 &&
15808
+ acceptTerms &&
15809
+ !!availability?.available &&
15810
+ !isCheckingAvailability;
15811
+ const freeformSelection = useMemo(() => ({
15812
+ eventTypeId: eventType.id,
15813
+ start: startDate.toISOString(),
15814
+ end: endDate.toISOString(),
15815
+ requestedUnits,
15816
+ customerName: customerName.trim(),
15817
+ customerEmail: customerEmail.trim(),
15818
+ customerPhone: customerPhone.trim(),
15819
+ comment: comment.trim(),
15820
+ }), [
15821
+ comment,
15822
+ customerEmail,
15823
+ customerName,
15824
+ customerPhone,
15825
+ endDate,
15826
+ eventType.id,
15827
+ requestedUnits,
15828
+ startDate,
15829
+ ]);
15830
+ const paymentFormData = useMemo(() => ({
15831
+ customerName: customerName.trim(),
15832
+ customerEmail: customerEmail.trim(),
15833
+ customerPhone: customerPhone.trim(),
15834
+ participants: [{ name: customerName.trim() || customerEmail.trim(), level: undefined }],
15835
+ comment: comment.trim(),
15836
+ }), [comment, customerEmail, customerName, customerPhone]);
15837
+ const eventDetailsForPayment = useMemo(() => ({
15838
+ id: eventType.id,
15839
+ name: eventType.name,
15840
+ startTime: startDate.toISOString(),
15841
+ endTime: endDate.toISOString(),
15842
+ price: basePrice,
15843
+ maxParticipants: availability?.capacityUnits ?? requestedUnits,
15844
+ participantCount: 0,
15845
+ availableSpots: availability?.freeUnits ?? requestedUnits,
15846
+ durationDays: 1,
15847
+ durationPerDay: availability?.roundedDurationMinutes ?? duration * unitMinutes,
15848
+ images: eventType.images ?? [],
15849
+ category: eventType.category,
15850
+ organization: {
15851
+ id: config.organizationId,
15852
+ name: "",
15853
+ },
15854
+ bookingOpen: availability?.available ?? false,
15855
+ }), [
15856
+ availability?.available,
15857
+ availability?.capacityUnits,
15858
+ availability?.freeUnits,
15859
+ availability?.roundedDurationMinutes,
15860
+ basePrice,
15861
+ config.organizationId,
15862
+ duration,
15863
+ endDate,
15864
+ eventType.category,
15865
+ eventType.id,
15866
+ eventType.images,
15867
+ eventType.name,
15868
+ requestedUnits,
15869
+ startDate,
15870
+ unitMinutes,
15871
+ ]);
15872
+ const footer = (jsxs("div", { style: { width: "100%", display: "flex", gap: "12px" }, children: [jsx("button", { type: "button", onClick: () => {
15873
+ if (checkoutStep === "payment") {
15874
+ setCheckoutStep("details");
15875
+ }
15876
+ else {
15877
+ onClose();
15878
+ }
15879
+ }, style: {
15880
+ flex: 1,
15881
+ padding: "12px 16px",
15882
+ border: "1px solid var(--bw-border-color)",
15883
+ borderRadius: "var(--bw-border-radius)",
15884
+ backgroundColor: "var(--bw-surface-color)",
15885
+ color: "var(--bw-text-color)",
15886
+ fontFamily: "var(--bw-font-family)",
15887
+ fontWeight: 600,
15888
+ cursor: "pointer",
15889
+ }, children: checkoutStep === "payment" ? t("button.backToDetails") : t("common.back") }), checkoutStep === "details" && (jsx("button", { type: "button", onClick: () => setCheckoutStep("payment"), disabled: !canContinueToPayment, style: {
15890
+ flex: 1,
15891
+ padding: "12px 16px",
15892
+ border: "none",
15893
+ borderRadius: "var(--bw-border-radius)",
15894
+ backgroundColor: "var(--bw-highlight-color)",
15895
+ color: "var(--bw-button-text-color, #ffffff)",
15896
+ fontFamily: "var(--bw-font-family)",
15897
+ fontWeight: 700,
15898
+ cursor: canContinueToPayment ? "pointer" : "not-allowed",
15899
+ opacity: canContinueToPayment ? 1 : 0.6,
15900
+ }, children: t("button.continueToPayment") }))] }));
15901
+ if (!freeformConfig) {
15902
+ return (jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: eventType.name, children: jsx("div", { style: { padding: "16px", color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: t("booking.freeformMissingConfig") }) }));
15903
+ }
15904
+ return (jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: eventType.name, footer: footer, children: jsx("div", { style: { padding: "16px", display: "flex", flexDirection: "column", gap: "16px" }, children: checkoutStep === "details" ? (jsxs(Fragment, { children: [jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("booking.eventDetails") }), jsxs("div", { style: { display: "grid", gap: "10px" }, children: [jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("booking.date"), jsx("input", { type: "datetime-local", value: startInput, onChange: (event) => setStartInput(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } })] }), jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("booking.duration"), jsx("input", { type: "number", min: freeformConfig.minDuration, max: freeformConfig.maxDuration, step: freeformConfig.stepDuration, value: duration, onChange: (event) => setDuration(Number(event.target.value)), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } })] }), !isNamedResource && (jsxs("label", { style: { display: "grid", gap: "6px", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-muted)" }, children: [t("voucher.quantity"), jsx("input", { type: "number", min: 1, max: maxUnits, value: requestedUnits, onChange: (event) => setRequestedUnits(Math.min(maxUnits, Math.max(1, Number(event.target.value)))), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } })] }))] }), jsxs("div", { style: { marginTop: "12px", fontSize: "13px", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: [formatInTimeZone(startDate, timeZone, locale), " - ", formatInTimeZone(endDate, timeZone, locale), eventType.organizationTimezone ? ` (${timeZone})` : ""] }), isCheckingAvailability && (jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)" }, children: t("common.loading") })), availabilityError && (jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: availabilityError })), !availabilityError && availability && (jsx("div", { style: { marginTop: "8px", fontSize: "13px", color: availability.available ? "var(--bw-success-color)" : "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: availability.available
15905
+ ? t("booking.freeformUnitsAvailable", { count: availability.freeUnits })
15906
+ : t("booking.freeformNotAvailable") }))] }), jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("booking.contactInfo") }), jsxs("div", { style: { display: "grid", gap: "10px" }, children: [jsx("input", { type: "text", placeholder: t("booking.namePlaceholder"), value: customerName, onChange: (event) => setCustomerName(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } }), jsx("input", { type: "email", placeholder: t("booking.emailPlaceholder"), value: customerEmail, onChange: (event) => setCustomerEmail(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } }), jsx("input", { type: "tel", placeholder: t("booking.phonePlaceholder"), value: customerPhone, onChange: (event) => setCustomerPhone(event.target.value), style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)" } }), jsx("textarea", { placeholder: t("booking.commentPlaceholder"), value: comment, onChange: (event) => setComment(event.target.value), rows: 3, style: { padding: "10px", borderRadius: "var(--bw-border-radius-small)", border: "1px solid var(--bw-border-color)", backgroundColor: "var(--bw-background-color)", color: "var(--bw-text-color)", resize: "vertical" } }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontFamily: "var(--bw-font-family)", fontSize: "13px", color: "var(--bw-text-muted)" }, children: [jsx("input", { type: "checkbox", checked: acceptTerms, onChange: (event) => setAcceptTerms(event.target.checked) }), t("booking.acceptTerms"), " ", t("booking.terms")] })] })] }), jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("summary.title") }), jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "14px" }, children: [jsx("span", { children: t("booking.duration") }), jsx("span", { children: t("booking.steps", { count: billedDurationUnits }) })] }), jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "14px", marginTop: "6px" }, children: [jsx("span", { children: t("booking.price") }), jsxs("span", { children: [formatCurrency(basePrice), " x ", requestedUnits] })] }), jsxs("div", { style: { display: "flex", justifyContent: "space-between", color: "var(--bw-text-color)", fontFamily: "var(--bw-font-family)", fontSize: "20px", fontWeight: 700, marginTop: "10px" }, children: [jsx("span", { children: t("summary.totalAmount") }), jsx("span", { children: formatCurrency(totalPrice) })] })] })] })) : (jsxs(Fragment, { children: [jsx(HoldCountdown, {}), jsxs("div", { style: { border: "1px solid var(--bw-border-color)", borderRadius: "var(--bw-border-radius)", padding: "14px", backgroundColor: "var(--bw-surface-color)" }, children: [jsx("h3", { style: { margin: "0 0 12px 0", fontFamily: "var(--bw-font-family)", color: "var(--bw-text-color)" }, children: t("summary.payment") }), !systemConfig?.paymentProvider && (jsx("p", { style: { margin: 0, color: "var(--bw-error-color)", fontFamily: "var(--bw-font-family)" }, children: t("booking.paymentUnavailable") })), systemConfig?.paymentProvider === "mollie" && (jsx(MolliePaymentForm, { config: config, eventDetails: eventDetailsForPayment, formData: paymentFormData, totalAmount: totalPrice, discountCode: null, giftCards: [], onSuccess: onSuccess, onError: onError, mollieProfileId: systemConfig?.mollieProfileId, mollieTestmode: systemConfig?.mollieTestmode, freeformSelection: freeformSelection })), systemConfig?.paymentProvider === "stripe" && (jsx(StripePaymentForm, { config: config, eventDetails: eventDetailsForPayment, formData: paymentFormData, totalAmount: totalPrice, discountCode: null, giftCards: [], onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, freeformSelection: freeformSelection }))] })] })) }) }));
15907
+ }
15908
+
15392
15909
  const getThemeConfig = (theme = "generic") => {
15393
15910
  switch (theme) {
15394
15911
  case "christmas":
@@ -16029,6 +16546,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16029
16546
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = useState(false);
16030
16547
  const [shouldRenderUpsells, setShouldRenderUpsells] = useState(false);
16031
16548
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = useState(false);
16549
+ const [shouldRenderFreeformSelection, setShouldRenderFreeformSelection] = useState(false);
16032
16550
  // Google Ads config (received from API, set once from the first API response)
16033
16551
  const [googleAdsConfig, setGoogleAdsConfig] = useState(null);
16034
16552
  const extractGoogleAdsConfig = (data) => {
@@ -16042,6 +16560,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16042
16560
  // Voucher purchase state
16043
16561
  const [voucherConfig, setVoucherConfig] = useState(null);
16044
16562
  const [voucherEventTypes, setVoucherEventTypes] = useState([]);
16563
+ const [voucherCategories, setVoucherCategories] = useState([]);
16045
16564
  const [isLoadingVoucherConfig, setIsLoadingVoucherConfig] = useState(false);
16046
16565
  const [isVoucherFormOpen, setIsVoucherFormOpen] = useState(false);
16047
16566
  const [voucherPurchaseResult, setVoucherPurchaseResult] = useState(null);
@@ -16121,6 +16640,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16121
16640
  setVoucherConfig(mergedConfig);
16122
16641
  extractGoogleAdsConfig(data);
16123
16642
  setVoucherEventTypes(data.eventTypes || []);
16643
+ setVoucherCategories(data.categories || []);
16124
16644
  // Set system config for payment processing
16125
16645
  if (data.paymentProvider) {
16126
16646
  setSystemConfig({
@@ -16149,7 +16669,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16149
16669
  if (!analyticsInitRef.current && config.organizationId) {
16150
16670
  analyticsInitRef.current = true;
16151
16671
  initAnalytics(config.apiBaseUrl, config.organizationId, {
16152
- partnerContractId: config.partnerContractId,
16672
+ ...(config.partnerContractId ? { partnerContractId: config.partnerContractId } : {}),
16153
16673
  });
16154
16674
  trackEvent("widget_loaded", {
16155
16675
  viewMode,
@@ -16628,6 +17148,18 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16628
17148
  const handleEventTypeSelect = async (eventType) => {
16629
17149
  trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16630
17150
  setSelectedEventType(eventType);
17151
+ if (eventType.bookingMode === "freeform") {
17152
+ setCurrentStep("freeform");
17153
+ setShouldRenderFreeformSelection(true);
17154
+ setIsLoadingEventInstances(true);
17155
+ try {
17156
+ await loadEventInstances(eventType.id);
17157
+ }
17158
+ finally {
17159
+ setIsLoadingEventInstances(false);
17160
+ }
17161
+ return;
17162
+ }
16631
17163
  setCurrentStep("eventInstances");
16632
17164
  setShouldRenderInstanceSelection(true);
16633
17165
  setIsLoadingEventInstances(true);
@@ -16718,6 +17250,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16718
17250
  setSuccessPaymentId(result.paymentIntent.id);
16719
17251
  setSidebarOpen(false);
16720
17252
  setShouldRenderBookingForm(false);
17253
+ setShouldRenderFreeformSelection(false);
16721
17254
  setBookingPersistedState(null);
16722
17255
  config.onSuccess?.(result);
16723
17256
  };
@@ -17033,6 +17566,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17033
17566
  setShouldRenderInstanceSelection(false);
17034
17567
  setShouldRenderUpsells(false);
17035
17568
  setShouldRenderBookingForm(false);
17569
+ setShouldRenderFreeformSelection(false);
17036
17570
  setSelectedUpsells([]);
17037
17571
  setUpsells([]);
17038
17572
  setBookingPersistedState(null);
@@ -17054,6 +17588,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17054
17588
  setShouldRenderInstanceSelection(false);
17055
17589
  setShouldRenderUpsells(false);
17056
17590
  setShouldRenderBookingForm(false);
17591
+ setShouldRenderFreeformSelection(false);
17057
17592
  setSelectedUpsells([]);
17058
17593
  setUpsells([]);
17059
17594
  setBookingPersistedState(null);
@@ -17073,6 +17608,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17073
17608
  setSuccessPaymentId(null);
17074
17609
  setShouldRenderInstanceSelection(false);
17075
17610
  setShouldRenderBookingForm(false);
17611
+ setShouldRenderFreeformSelection(false);
17076
17612
  const url = new URL(window.location.href);
17077
17613
  url.searchParams.delete("payment_intent");
17078
17614
  url.searchParams.delete("payment_intent_client_secret");
@@ -17116,13 +17652,23 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17116
17652
  setCurrentStep("booking");
17117
17653
  setShouldRenderBookingForm(true);
17118
17654
  }
17655
+ else if (selectedEventType?.bookingMode === "freeform") {
17656
+ setCurrentStep("freeform");
17657
+ setShouldRenderFreeformSelection(true);
17658
+ }
17119
17659
  else {
17120
17660
  setCurrentStep("eventInstances");
17121
17661
  setSidebarOpen(true);
17122
17662
  setShouldRenderInstanceSelection(true);
17123
17663
  }
17124
17664
  }, children: [isButtonBusy && jsx(Spinner, { size: 20, borderColor: "var(--bw-button-text-color, #ffffff)" }), config.buttonText ||
17125
- (isDirectInstanceMode ? t("button.bookNow") : t("button.viewDates"))] }), shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => setSidebarOpen(false), isOpen: sidebarOpen && currentStep === "eventInstances", onClose: () => setSidebarOpen(false), isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17665
+ (isDirectInstanceMode || selectedEventType?.bookingMode === "freeform"
17666
+ ? t("button.bookNow")
17667
+ : t("button.viewDates"))] }), shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => setSidebarOpen(false), isOpen: sidebarOpen && currentStep === "eventInstances", onClose: () => setSidebarOpen(false), isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: handleBackFromBooking, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), shouldRenderFreeformSelection && selectedEventType?.bookingMode === "freeform" && (jsx(FreeformSelection, { config: config, eventType: selectedEventType, isOpen: currentStep === "freeform", onClose: () => {
17668
+ setCurrentStep("eventTypes");
17669
+ setSidebarOpen(false);
17670
+ setShouldRenderFreeformSelection(false);
17671
+ }, onSuccess: handleBookingSuccess, onError: handleBookingError, systemConfig: systemConfig })), jsx(BookingSuccessModal, { isOpen: isSuccess, onClose: () => {
17126
17672
  setIsSuccess(false);
17127
17673
  setCurrentStep("eventTypes");
17128
17674
  setSidebarOpen(false);
@@ -17130,6 +17676,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17130
17676
  setShouldRenderInstanceSelection(false);
17131
17677
  setShouldRenderUpsells(false);
17132
17678
  setShouldRenderBookingForm(false);
17679
+ setShouldRenderFreeformSelection(false);
17133
17680
  setSelectedUpsells([]);
17134
17681
  setUpsells([]);
17135
17682
  const url = new URL(window.location.href);
@@ -17142,7 +17689,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17142
17689
  }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
17143
17690
  }
17144
17691
  // Cards mode (default) - show event type selection with optional voucher card
17145
- const cardsView = (jsxs(Fragment, { children: [hasEventSelection && (jsx(EventTypeSelection, { eventTypes: eventTypes, onEventTypeSelect: handleEventTypeSelect, onInstancePreview: (instanceId, eventTypeId) => void handleUpcomingEventSelect(instanceId, eventTypeId), isLoading: isLoading, skeletonCount: getSkeletonCount(), showVoucherAttachment: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled && !isStandaloneVoucherMode), onVoucherClick: handleVoucherAttachmentClick })), isStandaloneVoucherMode && (jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: false, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: null, voucherPurchaseResult: null, isSuccess: false, showStandaloneCard: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled), onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => { } })), isStandaloneVoucherMode && isLoading && !voucherConfig && (jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsx("div", { style: {
17692
+ const cardsView = (jsxs(Fragment, { children: [hasEventSelection && (jsx(EventTypeSelection, { eventTypes: eventTypes, onEventTypeSelect: handleEventTypeSelect, onInstancePreview: (instanceId, eventTypeId) => void handleUpcomingEventSelect(instanceId, eventTypeId), isLoading: isLoading, skeletonCount: getSkeletonCount(), showVoucherAttachment: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled && !isStandaloneVoucherMode), onVoucherClick: handleVoucherAttachmentClick })), isStandaloneVoucherMode && (jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, categories: voucherCategories, systemConfig: systemConfig, isFormOpen: false, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: null, voucherPurchaseResult: null, isSuccess: false, showStandaloneCard: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled), onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => { } })), isStandaloneVoucherMode && isLoading && !voucherConfig && (jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsx("div", { style: {
17146
17693
  display: "inline-block",
17147
17694
  width: "32px",
17148
17695
  height: "32px",
@@ -17183,13 +17730,17 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17183
17730
  };
17184
17731
  };
17185
17732
  const backHandlers = getBackHandlers();
17186
- return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [cardsView, shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: handleBackToEventTypes, isOpen: currentStep === "eventInstances", onClose: handleBackToEventTypes, isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: backHandlers.onClose, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), jsx(BookingSuccessModal, { isOpen: isSuccess && !voucherPurchaseResult, onClose: () => {
17733
+ return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [cardsView, shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: handleBackToEventTypes, isOpen: currentStep === "eventInstances", onClose: handleBackToEventTypes, isLoadingEventInstances: isLoadingEventInstances, isLoadingEventDetails: isLoadingEventDetails, hasUpsellsStep: hasUpsellsFlowStep, apiBaseUrl: config.apiBaseUrl, organizationId: config.organizationId })), shouldRenderUpsells && (jsx(UpsellsStep, { upsells: upsells, selectedUpsells: selectedUpsells, participantCount: tempParticipantCount, isLoading: isLoadingUpsells, isOpen: currentStep === "upsells", onClose: () => setCurrentStep("eventInstances"), onSelect: handleUpsellsSelect, onContinue: handleUpsellsContinue, onBack: handleUpsellsBack })), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, onSuccess: handleBookingSuccess, onError: handleBookingError, isOpen: currentStep === "booking" && !!eventDetails, onClose: backHandlers.onClose, systemConfig: systemConfig, selectedUpsells: selectedUpsells, upsells: upsells, persistedState: bookingPersistedState, onPersistedStateChange: setBookingPersistedState })), shouldRenderFreeformSelection && selectedEventType?.bookingMode === "freeform" && (jsx(FreeformSelection, { config: config, eventType: selectedEventType, isOpen: currentStep === "freeform", onClose: () => {
17734
+ setCurrentStep("eventTypes");
17735
+ setShouldRenderFreeformSelection(false);
17736
+ }, onSuccess: handleBookingSuccess, onError: handleBookingError, systemConfig: systemConfig })), jsx(BookingSuccessModal, { isOpen: isSuccess && !voucherPurchaseResult, onClose: () => {
17187
17737
  setIsSuccess(false);
17188
17738
  setCurrentStep("eventTypes");
17189
17739
  setSuccessPaymentId(null);
17190
17740
  setShouldRenderInstanceSelection(false);
17191
17741
  setShouldRenderUpsells(false);
17192
17742
  setShouldRenderBookingForm(false);
17743
+ setShouldRenderFreeformSelection(false);
17193
17744
  setSelectedUpsells([]);
17194
17745
  setUpsells([]);
17195
17746
  const url = new URL(window.location.href);
@@ -17199,7 +17750,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
17199
17750
  url.searchParams.delete("mollie_payment_id");
17200
17751
  url.searchParams.delete("mollie_status");
17201
17752
  window.history.replaceState({}, "", url.toString());
17202
- }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: isVoucherFormOpen, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: preselectedVoucherEventTypeId, voucherPurchaseResult: voucherPurchaseResult, isSuccess: isSuccess, showStandaloneCard: false, onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => {
17753
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsx(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, categories: voucherCategories, systemConfig: systemConfig, isFormOpen: isVoucherFormOpen, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: preselectedVoucherEventTypeId, voucherPurchaseResult: voucherPurchaseResult, isSuccess: isSuccess, showStandaloneCard: false, onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => {
17203
17754
  setIsSuccess(false);
17204
17755
  setVoucherPurchaseResult(null);
17205
17756
  const url = new URL(window.location.href);
@@ -17247,7 +17798,7 @@ function styleInject(css, ref) {
17247
17798
  }
17248
17799
  }
17249
17800
 
17250
- var css_248z = ".booking-widget-container{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box;color:var(--bw-text-color,#1e293b);direction:ltr;display:block;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;position:relative;text-align:left}.booking-widget-container *,.booking-widget-container :after,.booking-widget-container :before{box-sizing:border-box;margin:0;padding:0}.booking-widget-container input,.booking-widget-container select,.booking-widget-container textarea{font-family:inherit;font-size:inherit;line-height:inherit}.booking-widget-container button{background:none;border:none;cursor:pointer;font-family:inherit;font-size:inherit}.booking-widget-container a{color:inherit;text-decoration:none}.booking-widget-container img{display:block;height:auto;max-width:100%;vertical-align:middle}.booking-widget-container ol,.booking-widget-container ul{list-style:none}.booking-widget-container h1,.booking-widget-container h2,.booking-widget-container h3,.booking-widget-container h4,.booking-widget-container h5,.booking-widget-container h6{font-size:inherit;font-weight:inherit}#booking-widget-portal{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);direction:ltr;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;text-align:left}#booking-widget-portal *,#booking-widget-portal :after,#booking-widget-portal :before{box-sizing:border-box}#booking-widget-portal-root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);line-height:1.5}:root{--bw-highlight-color:#00b1aa;--bw-highlight-color-rgb:0,177,170;--bw-background-color:#f8fdfe;--bw-surface-color:#fff;--bw-text-color:#0e7490;--bw-text-muted:rgba(14,116,144,.7);--bw-border-color:#bae6fd;--bw-success-color:#38bdf8;--bw-warning-color:#fbbf24;--bw-error-color:#f43f5e;--bw-border-radius:18px;--bw-border-radius-small:calc(var(--bw-border-radius)*0.8);--bw-spacing:16px;--bw-spacing-large:24px;--bw-font-family:\"Inter\",system-ui,sans-serif;--bw-font-size:14px;--bw-font-size-large:18px;--bw-font-size-small:12px;--bw-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);--bw-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);--bw-highlight-muted:rgba(0,177,170,.1);--bw-highlight-subtle:rgba(0,177,170,.05);--bw-text-subtle:rgba(14,116,144,.4)}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes shimmer{0%{transform:translateX(-100%)}to{transform:translateX(100%)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes slide-in-right{0%{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(100%)}}@keyframes slide-in-up{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-spin{animation:spin 1s linear infinite}.animate-shimmer{animation:shimmer 2s infinite}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-fade-in{animation:fade-in .2s ease-out}.animate-slide-in-up{animation:slide-in-up .3s ease-out}.animate-scale-in{animation:scale-in .2s ease-out}.skeleton-shimmer{overflow:hidden;position:relative}.skeleton-shimmer:after{animation:shimmer 1.5s infinite;background:linear-gradient(90deg,transparent,hsla(0,0%,100%,.3),transparent);content:\"\";height:100%;left:0;position:absolute;top:0;width:100%}.bw-btn{letter-spacing:var(--bw-letter-spacing,normal);text-transform:var(--bw-text-transform,none);transition:all .2s ease!important}.bw-btn:hover:not(:disabled):not([disabled]){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.bw-btn:active:not(:disabled):not([disabled]){box-shadow:0 2px 4px rgba(0,0,0,.1)}.bw-btn-primary:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);filter:brightness(1.1)}.bw-btn-secondary:hover:not(:disabled):not([disabled]){background-color:var(--bw-surface-color);border-color:var(--bw-highlight-color);color:var(--bw-highlight-color)}.bw-btn-ghost:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-muted)}.bw-btn-outline:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);color:var(--bw-button-text-color,#fff)}.bw-btn:disabled,.bw-btn[disabled]{cursor:not-allowed!important;opacity:.5!important}button[class*=bw-btn],button[style*=transition]{transition:all .2s ease!important}button[data-variant=primary]:hover:not(:disabled),button[style*=\"--bw-highlight-color\"]:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);filter:brightness(1.1);transform:translateY(-1px)}button[data-variant=secondary]:hover:not(:disabled){border-color:var(--bw-highlight-color)!important;color:var(--bw-highlight-color)!important}button[data-variant=ghost]:hover:not(:disabled){background-color:var(--bw-highlight-muted)!important}button[data-variant=outline]:hover:not(:disabled){background-color:var(--bw-highlight-color)!important;color:var(--bw-button-text-color,#fff)!important}.bw-button-hover:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.bw-button-hover:active:not(:disabled){box-shadow:0 2px 4px rgba(0,0,0,.1);transform:translateY(0)}@media (max-width:768px){.sidebar-mobile{border-radius:0!important;max-width:100%!important;width:100%!important}}@media (max-width:600px){.event-type-list{gap:12px!important;padding:8px!important}.event-type-card{flex:1 1 100%!important;max-width:100%!important;padding:0!important}.event-type-img{height:160px!important}.event-type-title{font-size:1.1rem!important}.event-type-desc{font-size:.8rem!important;max-height:100px!important;min-height:100px!important}.event-type-content{padding:16px 24px!important}}.event-type-markdown{overflow:visible!important}.event-type-markdown p{color:var(--bw-text-muted);font-family:var(--bw-font-family);line-height:1.6;margin:0 0 8px}.event-type-markdown p:last-child{margin-bottom:0}.event-type-markdown h2{font-size:18px!important;font-weight:700!important;margin:12px 0 6px!important}.event-type-markdown h2,.event-type-markdown h3{color:var(--bw-text-color)!important;line-height:1.3!important}.event-type-markdown h3{font-size:16px!important;font-weight:600!important;margin:10px 0 4px!important}.event-type-markdown strong{color:var(--bw-text-color);font-weight:600}.event-type-markdown em{font-style:italic}.event-type-markdown u{text-decoration:underline}.event-type-markdown ul{list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ul li{color:var(--bw-text-muted)!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ul li:before{color:var(--bw-text-color)!important;content:\"•\"!important;font-weight:700!important;left:-16px!important;position:absolute!important;top:0!important}.event-type-markdown ol{counter-reset:list-counter!important;list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ol li{color:var(--bw-text-muted)!important;counter-increment:list-counter!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ol li:before{color:var(--bw-text-color)!important;content:counter(list-counter) \".\"!important;font-weight:700!important;left:-20px!important;position:absolute!important;top:0!important}.event-type-markdown blockquote{border-left:2px solid var(--bw-border-color);color:var(--bw-text-muted);font-style:italic;margin:4px 0;padding-left:12px}.event-type-markdown a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{color:var(--bw-text-color);font-weight:600;margin-bottom:.5em}.markdown-content h1{font-size:1.5em}.markdown-content h2{font-size:1.25em}.markdown-content h3{font-size:1.1em}.markdown-content p{line-height:1.6;margin-bottom:1em}.markdown-content ol,.markdown-content ul{margin-bottom:1em;padding-left:1.5em}.markdown-content ul{list-style-type:disc}.markdown-content ol{list-style-type:decimal}.markdown-content li{margin-bottom:.25em}.markdown-content a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content a:hover{opacity:.8}.markdown-content strong{font-weight:600}.markdown-content em{font-style:italic}.markdown-content code{background:var(--bw-highlight-subtle);border-radius:4px;font-family:monospace;font-size:.9em;padding:.125em .25em}.markdown-content blockquote{border-left:3px solid var(--bw-highlight-color);color:var(--bw-text-muted);margin:1em 0;padding-left:1em}.print-only{display:none}.print-hidden{display:block}@media print{.print-only{display:block}.print-hidden{display:none!important}.print-booking-header{border-bottom:2px solid #000;display:block;margin-bottom:24px;padding-bottom:16px;text-align:center}.print-booking-header h1{font-size:24px;margin:0 0 8px}.print-booking-header .subtitle{color:#666;font-size:14px}.print-booking-card{border:1px solid #ccc;border-radius:8px;margin-bottom:16px;padding:16px;page-break-inside:avoid}.print-section-title{border-bottom:1px solid #ddd;display:block;font-size:16px;font-weight:600;margin-bottom:12px;padding-bottom:8px}.print-detail-grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.print-detail-item{margin-bottom:8px}.print-detail-label{color:#666;font-size:12px;margin-bottom:4px}.print-detail-value{font-size:14px;font-weight:600}.print-status-badge{border-radius:9999px;display:inline-block;font-size:12px;font-weight:600;padding:4px 12px}.print-status-paid{background-color:#dcfce7;color:#166534;display:inline-block}.print-participant{align-items:center;background-color:#f9fafb;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:8px;padding:8px}.print-participant-name{font-weight:600}.print-participant-age{color:#666;font-size:12px}.print-payment-summary{display:block}.print-payment-row{border-bottom:1px solid #eee;display:flex;justify-content:space-between;padding:4px 0}.print-payment-row:last-child{border-bottom:none;font-weight:600}.print-footer{border-top:1px solid #ddd;color:#666;display:block;font-size:12px;margin-top:24px;padding-top:16px;text-align:center}.print-footer p{margin:4px 0}}";
17801
+ var css_248z = ".booking-widget-container{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box;color:var(--bw-text-color,#1e293b);direction:ltr;display:block;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;position:relative;text-align:left}.booking-widget-container *,.booking-widget-container :after,.booking-widget-container :before{box-sizing:border-box;margin:0;padding:0}.booking-widget-container input,.booking-widget-container select,.booking-widget-container textarea{font-family:inherit;font-size:inherit;line-height:inherit}.booking-widget-container button{background:none;border:none;cursor:pointer;font-family:inherit;font-size:inherit}.booking-widget-container a{color:inherit;text-decoration:none}.booking-widget-container img{display:block;height:auto;max-width:100%;vertical-align:middle}.booking-widget-container ol,.booking-widget-container ul{list-style:none}.booking-widget-container h1,.booking-widget-container h2,.booking-widget-container h3,.booking-widget-container h4,.booking-widget-container h5,.booking-widget-container h6{font-size:inherit;font-weight:inherit}#booking-widget-portal{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);direction:ltr;font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);isolation:isolate;line-height:1.5;text-align:left}#booking-widget-portal *,#booking-widget-portal :after,#booking-widget-portal :before{box-sizing:border-box}#booking-widget-portal-root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--bw-text-color,#1e293b);font-family:var(--bw-font-family,system-ui,-apple-system,sans-serif);font-size:var(--bw-font-size,14px);line-height:1.5}:root{--bw-highlight-color:#00b1aa;--bw-highlight-color-rgb:0,177,170;--bw-background-color:#f8fdfe;--bw-surface-color:#fff;--bw-text-color:#0e7490;--bw-text-muted:rgba(14,116,144,.7);--bw-border-color:#bae6fd;--bw-success-color:#38bdf8;--bw-warning-color:#fbbf24;--bw-error-color:#f43f5e;--bw-border-radius:18px;--bw-border-radius-small:calc(var(--bw-border-radius)*0.8);--bw-spacing:16px;--bw-spacing-large:24px;--bw-font-family:\"Inter\",system-ui,sans-serif;--bw-font-size:14px;--bw-font-size-large:18px;--bw-font-size-small:12px;--bw-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);--bw-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);--bw-highlight-muted:rgba(0,177,170,.1);--bw-highlight-subtle:rgba(0,177,170,.05);--bw-text-subtle:rgba(14,116,144,.4)}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes shimmer{0%{transform:translateX(-100%)}to{transform:translateX(100%)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes slide-in-right{0%{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(100%)}}@keyframes slide-in-up{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-spin{animation:spin 1s linear infinite}.animate-shimmer{animation:shimmer 2s infinite}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-fade-in{animation:fade-in .2s ease-out}.animate-slide-in-up{animation:slide-in-up .3s ease-out}.animate-scale-in{animation:scale-in .2s ease-out}.skeleton-shimmer{overflow:hidden;position:relative}.skeleton-shimmer:after{animation:shimmer 1.5s infinite;background:linear-gradient(90deg,transparent,hsla(0,0%,100%,.3),transparent);content:\"\";height:100%;left:0;position:absolute;top:0;width:100%}.booking-widget-container .bw-btn{letter-spacing:var(--bw-letter-spacing,normal);text-transform:var(--bw-text-transform,none);transition:all .2s ease!important}.booking-widget-container .bw-btn:hover:not(:disabled):not([disabled]){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.booking-widget-container .bw-btn:active:not(:disabled):not([disabled]){box-shadow:0 2px 4px rgba(0,0,0,.1)}.booking-widget-container .bw-btn-primary:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);filter:brightness(1.1)}.booking-widget-container .bw-btn-secondary:hover:not(:disabled):not([disabled]){background-color:var(--bw-surface-color);border-color:var(--bw-highlight-color);color:var(--bw-highlight-color)}.booking-widget-container .bw-btn-ghost:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-muted)}.booking-widget-container .bw-btn-outline:hover:not(:disabled):not([disabled]){background-color:var(--bw-highlight-color);color:var(--bw-button-text-color,#fff)}.booking-widget-container .bw-btn:disabled,.booking-widget-container .bw-btn[disabled]{cursor:not-allowed!important;opacity:.5!important}.booking-widget-container button[class*=bw-btn],.booking-widget-container button[style*=transition]{transition:all .2s ease!important}.booking-widget-container button[data-variant=primary]:hover:not(:disabled),.booking-widget-container button[style*=\"--bw-highlight-color\"]:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);filter:brightness(1.1);transform:translateY(-1px)}.booking-widget-container button[data-variant=secondary]:hover:not(:disabled){border-color:var(--bw-highlight-color)!important;color:var(--bw-highlight-color)!important}.booking-widget-container button[data-variant=ghost]:hover:not(:disabled){background-color:var(--bw-highlight-muted)!important}.booking-widget-container button[data-variant=outline]:hover:not(:disabled){background-color:var(--bw-highlight-color)!important;color:var(--bw-button-text-color,#fff)!important}.booking-widget-container .bw-button-hover:hover:not(:disabled){box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.booking-widget-container .bw-button-hover:active:not(:disabled){box-shadow:0 2px 4px rgba(0,0,0,.1);transform:translateY(0)}@media (max-width:768px){.sidebar-mobile{border-radius:0!important;max-width:100%!important;width:100%!important}}@media (max-width:600px){.event-type-list{gap:12px!important;padding:8px!important}.event-type-card{flex:1 1 100%!important;max-width:100%!important;padding:0!important}.event-type-img{height:160px!important}.event-type-title{font-size:1.1rem!important}.event-type-desc{font-size:.8rem!important;max-height:100px!important;min-height:100px!important}.event-type-content{padding:16px 24px!important}}.event-type-markdown{overflow:visible!important}.event-type-markdown p{color:var(--bw-text-muted);font-family:var(--bw-font-family);line-height:1.6;margin:0 0 8px}.event-type-markdown p:last-child{margin-bottom:0}.event-type-markdown h2{font-size:18px!important;font-weight:700!important;margin:12px 0 6px!important}.event-type-markdown h2,.event-type-markdown h3{color:var(--bw-text-color)!important;line-height:1.3!important}.event-type-markdown h3{font-size:16px!important;font-weight:600!important;margin:10px 0 4px!important}.event-type-markdown strong{color:var(--bw-text-color);font-weight:600}.event-type-markdown em{font-style:italic}.event-type-markdown u{text-decoration:underline}.event-type-markdown ul{list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ul li{color:var(--bw-text-muted)!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ul li:before{color:var(--bw-text-color)!important;content:\"•\"!important;font-weight:700!important;left:-16px!important;position:absolute!important;top:0!important}.event-type-markdown ol{counter-reset:list-counter!important;list-style:none!important;margin:6px 0!important;padding:0 0 0 24px!important;position:relative!important}.event-type-markdown ol li{color:var(--bw-text-muted)!important;counter-increment:list-counter!important;font-family:var(--bw-font-family)!important;margin-bottom:2px!important;padding-left:0!important;position:relative!important}.event-type-markdown ol li:before{color:var(--bw-text-color)!important;content:counter(list-counter) \".\"!important;font-weight:700!important;left:-20px!important;position:absolute!important;top:0!important}.event-type-markdown blockquote{border-left:2px solid var(--bw-border-color);color:var(--bw-text-muted);font-style:italic;margin:4px 0;padding-left:12px}.event-type-markdown a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{color:var(--bw-text-color);font-weight:600;margin-bottom:.5em}.markdown-content h1{font-size:1.5em}.markdown-content h2{font-size:1.25em}.markdown-content h3{font-size:1.1em}.markdown-content p{line-height:1.6;margin-bottom:1em}.markdown-content ol,.markdown-content ul{margin-bottom:1em;padding-left:1.5em}.markdown-content ul{list-style-type:disc}.markdown-content ol{list-style-type:decimal}.markdown-content li{margin-bottom:.25em}.markdown-content a{color:var(--bw-highlight-color);text-decoration:underline}.markdown-content a:hover{opacity:.8}.markdown-content strong{font-weight:600}.markdown-content em{font-style:italic}.markdown-content code{background:var(--bw-highlight-subtle);border-radius:4px;font-family:monospace;font-size:.9em;padding:.125em .25em}.markdown-content blockquote{border-left:3px solid var(--bw-highlight-color);color:var(--bw-text-muted);margin:1em 0;padding-left:1em}.print-only{display:none}.print-hidden{display:block}@media print{.print-only{display:block}.print-hidden{display:none!important}.print-booking-header{border-bottom:2px solid #000;display:block;margin-bottom:24px;padding-bottom:16px;text-align:center}.print-booking-header h1{font-size:24px;margin:0 0 8px}.print-booking-header .subtitle{color:#666;font-size:14px}.print-booking-card{border:1px solid #ccc;border-radius:8px;margin-bottom:16px;padding:16px;page-break-inside:avoid}.print-section-title{border-bottom:1px solid #ddd;display:block;font-size:16px;font-weight:600;margin-bottom:12px;padding-bottom:8px}.print-detail-grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.print-detail-item{margin-bottom:8px}.print-detail-label{color:#666;font-size:12px;margin-bottom:4px}.print-detail-value{font-size:14px;font-weight:600}.print-status-badge{border-radius:9999px;display:inline-block;font-size:12px;font-weight:600;padding:4px 12px}.print-status-paid{background-color:#dcfce7;color:#166534;display:inline-block}.print-participant{align-items:center;background-color:#f9fafb;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:8px;padding:8px}.print-participant-name{font-weight:600}.print-participant-age{color:#666;font-size:12px}.print-payment-summary{display:block}.print-payment-row{border-bottom:1px solid #eee;display:flex;justify-content:space-between;padding:4px 0}.print-payment-row:last-child{border-bottom:none;font-weight:600}.print-footer{border-top:1px solid #ddd;color:#666;display:block;font-size:12px;margin-top:24px;padding-top:16px;text-align:center}.print-footer p{margin:4px 0}}";
17251
17802
  styleInject(css_248z);
17252
17803
 
17253
17804
  // Export init function for vanilla JS usage