@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.
- package/dist/booking-widget.js +625 -74
- package/dist/booking-widget.js.map +1 -1
- package/dist/components/UniversalBookingWidget.d.ts +18 -0
- package/dist/components/UniversalBookingWidget.d.ts.map +1 -1
- package/dist/components/booking/MolliePaymentForm.d.ts +11 -1
- package/dist/components/booking/MolliePaymentForm.d.ts.map +1 -1
- package/dist/components/booking/StripePaymentForm.d.ts +11 -1
- package/dist/components/booking/StripePaymentForm.d.ts.map +1 -1
- package/dist/components/events/EventTypeSelection.d.ts +15 -0
- package/dist/components/events/EventTypeSelection.d.ts.map +1 -1
- package/dist/components/events/FreeformSelection.d.ts +49 -0
- package/dist/components/events/FreeformSelection.d.ts.map +1 -0
- package/dist/components/events/index.d.ts +1 -0
- package/dist/components/events/index.d.ts.map +1 -1
- package/dist/components/voucher/VoucherIntegration.d.ts +3 -2
- package/dist/components/voucher/VoucherIntegration.d.ts.map +1 -1
- package/dist/components/voucher/VoucherPurchaseForm.d.ts +8 -2
- package/dist/components/voucher/VoucherPurchaseForm.d.ts.map +1 -1
- package/dist/components/voucher/index.d.ts +1 -1
- package/dist/components/voucher/index.d.ts.map +1 -1
- package/dist/components/voucher/useVoucherConfig.d.ts +6 -0
- package/dist/components/voucher/useVoucherConfig.d.ts.map +1 -1
- package/dist/i18n/locales/de.d.ts.map +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/es.d.ts.map +1 -1
- package/dist/i18n/locales/pt.d.ts.map +1 -1
- package/dist/i18n/locales/sv.d.ts.map +1 -1
- package/dist/index.cjs +625 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +625 -74
- package/dist/index.esm.js.map +1 -1
- 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 =
|
|
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
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
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,
|
|
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,
|
|
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
|
|
6896
|
-
const
|
|
6897
|
-
|
|
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
|
-
?
|
|
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
|
|
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 (
|
|
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
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
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 (
|
|
7136
|
+
if (requestData.amount === undefined || requestData.amount < 0) {
|
|
7028
7137
|
throw new Error("Valid amount is required");
|
|
7029
7138
|
}
|
|
7030
|
-
if (!requestData
|
|
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,
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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") })] }))
|
|
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.
|
|
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
|
-
:
|
|
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
|
|
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(
|
|
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
|