@bigz-app/booking-widget 1.3.0 → 1.3.2

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/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import React__default, { createContext, useState, useEffect, useCallback, useMemo, useContext, forwardRef, useRef, Fragment as Fragment$1 } from 'react';
2
+ import React__default, { createContext, useState, useCallback, useMemo, useContext, useEffect, forwardRef, useRef, Fragment as Fragment$1 } from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import ReactDOM, { createPortal } from 'react-dom';
@@ -280,6 +280,8 @@ const de$1 = {
280
280
  "booking.participantName": "Name *",
281
281
  "booking.participantNamePlaceholder": "Teilnehmername",
282
282
  "booking.participantAge": "Alter",
283
+ "booking.participantLevel": "Level",
284
+ "booking.participantLevelPlaceholder": "Level wählen...",
283
285
  "booking.addParticipant": "{{number}}. Teilnehmer hinzufügen",
284
286
  "booking.maxParticipants": "Maximale Anzahl an Teilnehmern erreicht. Es sind nur noch {{count}} Plätze verfügbar.",
285
287
  "booking.maxSpotsReached": "Maximal {{count}} Plätze verfügbar.",
@@ -428,7 +430,11 @@ const de$1 = {
428
430
  "validation.emailInvalid": "Ungültiges E-Mail-Format",
429
431
  "validation.emailDomainInvalid": "Ungültige E-Mail-Domain",
430
432
  "validation.participantRequired": "Mindestens ein Teilnehmer erforderlich",
433
+ "validation.ageRequired": "Alter ist erforderlich",
434
+ "validation.levelRequired": "Bitte ein Level auswählen",
431
435
  "validation.acceptTerms": "Bitte akzeptiere die Allgemeinen Geschäftsbedingungen",
436
+ "level.beginner": "Anfänger",
437
+ "level.advanced": "Fortgeschritten",
432
438
  // Sidebar
433
439
  "sidebar.close": "Schließen",
434
440
  // Promo
@@ -550,6 +556,8 @@ const en = {
550
556
  "booking.participantName": "Name *",
551
557
  "booking.participantNamePlaceholder": "Participant name",
552
558
  "booking.participantAge": "Age",
559
+ "booking.participantLevel": "Level",
560
+ "booking.participantLevelPlaceholder": "Select level...",
553
561
  "booking.addParticipant": "Add participant {{number}}",
554
562
  "booking.maxParticipants": "Maximum number of participants reached. Only {{count}} spots are available.",
555
563
  "booking.maxSpotsReached": "Maximum {{count}} spots available.",
@@ -698,7 +706,11 @@ const en = {
698
706
  "validation.emailInvalid": "Invalid email format",
699
707
  "validation.emailDomainInvalid": "Invalid email domain",
700
708
  "validation.participantRequired": "At least one participant is required",
709
+ "validation.ageRequired": "Age is required",
710
+ "validation.levelRequired": "Please select a level",
701
711
  "validation.acceptTerms": "Please accept the terms and conditions",
712
+ "level.beginner": "Beginner",
713
+ "level.advanced": "Advanced",
702
714
  // Sidebar
703
715
  "sidebar.close": "Close",
704
716
  // Promo
@@ -820,6 +832,8 @@ const es = {
820
832
  "booking.participantName": "Nombre *",
821
833
  "booking.participantNamePlaceholder": "Nombre del participante",
822
834
  "booking.participantAge": "Edad",
835
+ "booking.participantLevel": "Nivel",
836
+ "booking.participantLevelPlaceholder": "Seleccionar nivel...",
823
837
  "booking.addParticipant": "Añadir participante {{number}}",
824
838
  "booking.maxParticipants": "Número máximo de participantes alcanzado. Solo quedan {{count}} plazas disponibles.",
825
839
  "booking.maxSpotsReached": "Máximo {{count}} plazas disponibles.",
@@ -968,7 +982,11 @@ const es = {
968
982
  "validation.emailInvalid": "Formato de correo electrónico inválido",
969
983
  "validation.emailDomainInvalid": "Dominio de correo electrónico inválido",
970
984
  "validation.participantRequired": "Se requiere al menos un participante",
985
+ "validation.ageRequired": "La edad es obligatoria",
986
+ "validation.levelRequired": "Selecciona un nivel",
971
987
  "validation.acceptTerms": "Por favor, acepta los términos y condiciones",
988
+ "level.beginner": "Principiante",
989
+ "level.advanced": "Avanzado",
972
990
  // Sidebar
973
991
  "sidebar.close": "Cerrar",
974
992
  // Promo
@@ -1090,6 +1108,8 @@ const pt = {
1090
1108
  "booking.participantName": "Nome *",
1091
1109
  "booking.participantNamePlaceholder": "Nome do participante",
1092
1110
  "booking.participantAge": "Idade",
1111
+ "booking.participantLevel": "Nível",
1112
+ "booking.participantLevelPlaceholder": "Selecionar nível...",
1093
1113
  "booking.addParticipant": "Adicionar participante {{number}}",
1094
1114
  "booking.maxParticipants": "Número máximo de participantes atingido. Apenas {{count}} lugares disponíveis.",
1095
1115
  "booking.maxSpotsReached": "Máximo {{count}} lugares disponíveis.",
@@ -1238,7 +1258,11 @@ const pt = {
1238
1258
  "validation.emailInvalid": "Formato de email inválido",
1239
1259
  "validation.emailDomainInvalid": "Domínio de email inválido",
1240
1260
  "validation.participantRequired": "É necessário pelo menos um participante",
1261
+ "validation.ageRequired": "A idade é obrigatória",
1262
+ "validation.levelRequired": "Por favor selecione um nível",
1241
1263
  "validation.acceptTerms": "Por favor, aceite os termos e condições",
1264
+ "level.beginner": "Iniciante",
1265
+ "level.advanced": "Avançado",
1242
1266
  // Sidebar
1243
1267
  "sidebar.close": "Fechar",
1244
1268
  // Promo
@@ -1360,6 +1384,8 @@ const sv = {
1360
1384
  "booking.participantName": "Namn *",
1361
1385
  "booking.participantNamePlaceholder": "Deltagarens namn",
1362
1386
  "booking.participantAge": "Ålder",
1387
+ "booking.participantLevel": "Nivå",
1388
+ "booking.participantLevelPlaceholder": "Välj nivå...",
1363
1389
  "booking.addParticipant": "Lägg till deltagare {{number}}",
1364
1390
  "booking.maxParticipants": "Maximalt antal deltagare uppnått. Bara {{count}} platser är tillgängliga.",
1365
1391
  "booking.maxSpotsReached": "Maximalt {{count}} platser tillgängliga.",
@@ -1508,7 +1534,11 @@ const sv = {
1508
1534
  "validation.emailInvalid": "Ogiltigt e-postformat",
1509
1535
  "validation.emailDomainInvalid": "Ogiltig e-postdomän",
1510
1536
  "validation.participantRequired": "Minst en deltagare krävs",
1537
+ "validation.ageRequired": "Ålder krävs",
1538
+ "validation.levelRequired": "Välj en nivå",
1511
1539
  "validation.acceptTerms": "Acceptera villkoren",
1540
+ "level.beginner": "Nybörjare",
1541
+ "level.advanced": "Avancerad",
1512
1542
  // Sidebar
1513
1543
  "sidebar.close": "Stäng",
1514
1544
  // Promo
@@ -1601,18 +1631,9 @@ function persistLocale(locale) {
1601
1631
  }
1602
1632
  const I18nContext = createContext(null);
1603
1633
  function I18nProvider({ configLocale, children }) {
1604
- // Priority: configLocale (site owner) > persisted user choice > browser language > "de"
1605
- // If configLocale is set, the site owner has locked the language - don't restore user choice.
1606
- const [overrideLocale, setOverrideLocale] = useState(() => {
1607
- if (configLocale)
1608
- return null;
1609
- return readPersistedLocale();
1610
- });
1611
- useEffect(() => {
1612
- if (configLocale) {
1613
- setOverrideLocale(null);
1614
- }
1615
- }, [configLocale]);
1634
+ // Priority: persisted user choice > configLocale (org default) > browser language > "de"
1635
+ // This keeps org locale as default, but remembers explicit user overrides across reloads.
1636
+ const [overrideLocale, setOverrideLocale] = useState(() => readPersistedLocale());
1616
1637
  const locale = overrideLocale ?? resolveLocale(configLocale);
1617
1638
  const handleSetLocale = useCallback((next) => {
1618
1639
  persistLocale(next);
@@ -1744,6 +1765,16 @@ const resolveSemanticColor = (colorValue, fallbackValue) => {
1744
1765
  // If semantic resolution fails, use fallback or return the original value
1745
1766
  return fallbackValue || colorValue;
1746
1767
  };
1768
+ // Legacy theme name redirects (old name → new name)
1769
+ const legacyThemeRedirects = {
1770
+ "light-fresh": "teal-minimal",
1771
+ "light-elegant": "blue-business",
1772
+ "light-vibrant": "orange-raw",
1773
+ "light-professional": "blue-business",
1774
+ "dark-night": "navy-night",
1775
+ "dark-modern": "navy-night",
1776
+ "dark-forest": "green-deep",
1777
+ };
1747
1778
  // Predefined themes
1748
1779
  const themes = {
1749
1780
  // --- Light Themes ---
@@ -1890,7 +1921,9 @@ const StyleProvider = ({ config, children, }) => {
1890
1921
  }, []);
1891
1922
  // PERFORMANCE OPTIMIZATION: Memoize style calculations
1892
1923
  const themedStyles = useMemo(() => {
1893
- const themeName = config.theme || "teal-minimal";
1924
+ const rawThemeName = config.theme || "teal-minimal";
1925
+ // Redirect legacy theme names to new names
1926
+ const themeName = legacyThemeRedirects[rawThemeName] || rawThemeName;
1894
1927
  const themeDefaults = themes[themeName] || themes["teal-minimal"];
1895
1928
  const getCSSValue = (value, fallback) => {
1896
1929
  if (!value)
@@ -11183,16 +11216,37 @@ const objectType = ZodObject.create;
11183
11216
  ZodUnion.create;
11184
11217
  ZodIntersection.create;
11185
11218
  ZodTuple.create;
11186
- ZodEnum.create;
11219
+ const enumType = ZodEnum.create;
11187
11220
  ZodPromise.create;
11188
11221
  ZodOptional.create;
11189
11222
  ZodNullable.create;
11223
+ const preprocessType = ZodEffects.createWithPreprocess;
11190
11224
 
11191
- const participantSchema = (t) => objectType({
11192
- name: stringType().trim().min(1, t("validation.nameRequired")),
11225
+ const DEFAULT_PARTICIPANT_FIELDS_CONFIG = {
11226
+ name: { enabled: true, required: true },
11227
+ age: { enabled: true, required: false },
11228
+ level: { enabled: false, required: false },
11229
+ };
11230
+ const participantSchema = (t, fieldsConfig) => objectType({
11231
+ name: stringType().trim().optional(),
11193
11232
  age: numberType().min(0).max(120).optional(),
11233
+ level: preprocessType((value) => (value === "" ? undefined : value), enumType(["beginner", "advanced"]).optional()),
11234
+ })
11235
+ .superRefine((value, ctx) => {
11236
+ if (fieldsConfig.name.required && (!value.name || value.name.trim().length < 1)) {
11237
+ ctx.addIssue({ code: ZodIssueCode.custom, message: t("validation.nameRequired"), path: ["name"] });
11238
+ }
11239
+ if (!fieldsConfig.name.enabled && value.name && value.name.trim().length > 0) {
11240
+ ctx.addIssue({ code: ZodIssueCode.custom, message: t("validation.nameRequired"), path: ["name"] });
11241
+ }
11242
+ if (fieldsConfig.age.required && typeof value.age !== "number") {
11243
+ ctx.addIssue({ code: ZodIssueCode.custom, message: t("validation.ageRequired"), path: ["age"] });
11244
+ }
11245
+ if (fieldsConfig.level.required && !value.level) {
11246
+ ctx.addIssue({ code: ZodIssueCode.custom, message: t("validation.levelRequired"), path: ["level"] });
11247
+ }
11194
11248
  });
11195
- function createBookingFormSchema(t) {
11249
+ function createBookingFormSchema(t, fieldsConfig = DEFAULT_PARTICIPANT_FIELDS_CONFIG) {
11196
11250
  const tr = t ?? ((key) => key);
11197
11251
  return objectType({
11198
11252
  customerName: stringType().trim().min(2, tr("validation.nameMinLength")),
@@ -11202,7 +11256,7 @@ function createBookingFormSchema(t) {
11202
11256
  .email(tr("validation.emailInvalid"))
11203
11257
  .regex(/\.[a-zA-Z]{2,}$/, tr("validation.emailDomainInvalid")),
11204
11258
  customerPhone: stringType().trim().optional(),
11205
- participants: arrayType(participantSchema(tr)).min(1, tr("validation.participantRequired")),
11259
+ participants: arrayType(participantSchema(tr, fieldsConfig)).min(1, tr("validation.participantRequired")),
11206
11260
  discountCode: stringType().trim().optional(),
11207
11261
  comment: stringType().trim().optional(),
11208
11262
  acceptTerms: booleanType().refine((val) => val === true, {
@@ -11345,7 +11399,8 @@ const participantUpsellStyles = {
11345
11399
  gap: "8px",
11346
11400
  marginTop: "10px",
11347
11401
  paddingTop: "10px",
11348
- borderTop: "1px dashed var(--bw-border-color)",
11402
+ paddingBottom: "25px",
11403
+ borderBottom: "1px dashed var(--bw-border-color)",
11349
11404
  },
11350
11405
  label: {
11351
11406
  display: "inline-flex",
@@ -11403,6 +11458,8 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11403
11458
  const { locale } = useLocale();
11404
11459
  const timezone = useTimezone();
11405
11460
  const roundEnabled = systemConfig?.roundPricesEnabled !== false;
11461
+ const participantFieldsConfig = eventDetails.participantFieldsConfig ?? DEFAULT_PARTICIPANT_FIELDS_CONFIG;
11462
+ const participantLevelOptions = eventDetails.participantLevelOptions ?? ["beginner", "advanced"];
11406
11463
  const roundDiscountUp = (minorUnits) => Math.ceil(minorUnits / 100) * 100;
11407
11464
  const calcPercentDiscountAmount = (baseAmount, basisPoints, round) => {
11408
11465
  const raw = Math.round((baseAmount * basisPoints) / 10000);
@@ -11415,18 +11472,19 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11415
11472
  // Per-participant upsell selections: participantIndex -> array of upsell package IDs
11416
11473
  const [participantUpsells, setParticipantUpsells] = useState({});
11417
11474
  const form = useForm({
11418
- resolver: t(createBookingFormSchema(t$1)),
11475
+ resolver: t(createBookingFormSchema(t$1, participantFieldsConfig)),
11419
11476
  defaultValues: {
11420
11477
  customerName: "",
11421
11478
  customerEmail: "",
11422
11479
  customerPhone: "",
11423
- participants: [{ name: "" }],
11480
+ participants: [{ name: "", level: undefined }],
11424
11481
  discountCode: "",
11425
11482
  comment: "",
11426
11483
  acceptTerms: false,
11427
11484
  },
11428
11485
  });
11429
11486
  const watchedParticipants = form.watch("participants");
11487
+ const participantCount = watchedParticipants.length;
11430
11488
  const watchedCustomerName = form.watch("customerName");
11431
11489
  const watchedCustomerEmail = form.watch("customerEmail");
11432
11490
  const watchedComment = form.watch("comment");
@@ -11468,14 +11526,13 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11468
11526
  const calculateBaseTotal = useCallback(() => {
11469
11527
  if (!eventDetails)
11470
11528
  return 0;
11471
- return eventDetails.price * watchedParticipants.filter((p) => p.name.trim()).length;
11472
- }, [eventDetails, watchedParticipants]);
11529
+ return eventDetails.price * participantCount;
11530
+ }, [eventDetails, participantCount]);
11473
11531
  // Calculate upsells total based on per-participant selections
11474
11532
  const calculateUpsellsTotal = useCallback(() => {
11475
11533
  let total = 0;
11476
- watchedParticipants.forEach((participant, index) => {
11477
- // Only count upsells for participants with names
11478
- if (participant.name.trim()) {
11534
+ watchedParticipants.forEach((_, index) => {
11535
+ if (participantCount > 0) {
11479
11536
  const participantUpsellIds = participantUpsells[index] || [];
11480
11537
  participantUpsellIds.forEach(upsellId => {
11481
11538
  const upsell = upsells.find(u => u.id === upsellId);
@@ -11486,7 +11543,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11486
11543
  }
11487
11544
  });
11488
11545
  return total;
11489
- }, [participantUpsells, upsells, watchedParticipants]);
11546
+ }, [participantUpsells, upsells, watchedParticipants, participantCount]);
11490
11547
  const calculateTotalDiscount = useCallback(() => {
11491
11548
  return appliedVouchers.reduce((total, voucher) => {
11492
11549
  if (voucher.type === "discount") {
@@ -11507,8 +11564,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11507
11564
  const calculateDeposit = () => {
11508
11565
  if (!eventDetails || !eventDetails.deposit)
11509
11566
  return 0;
11510
- const participantCount = watchedParticipants.filter((p) => p.name.trim()).length;
11511
- return eventDetails.deposit * participantCount;
11567
+ return eventDetails.deposit * watchedParticipants.length;
11512
11568
  };
11513
11569
  const baseTotal = calculateBaseTotal();
11514
11570
  const upsellsTotal = calculateUpsellsTotal();
@@ -11525,8 +11581,8 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11525
11581
  // Includes participantIndices to track which participants selected each upsell
11526
11582
  const aggregatedUpsellSelections = useCallback(() => {
11527
11583
  const upsellParticipantMap = {};
11528
- watchedParticipants.forEach((participant, index) => {
11529
- if (participant.name.trim()) {
11584
+ watchedParticipants.forEach((_, index) => {
11585
+ if (participantCount > 0) {
11530
11586
  const participantUpsellIds = participantUpsells[index] || [];
11531
11587
  participantUpsellIds.forEach(upsellId => {
11532
11588
  if (!upsellParticipantMap[upsellId]) {
@@ -11560,15 +11616,17 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11560
11616
  setAppliedVouchers((prev) => prev.filter((v) => v.code !== code));
11561
11617
  }, []);
11562
11618
  const isReadyForPayment = () => {
11563
- const participantsWithNames = watchedParticipants.filter((p) => p.name.trim()).length;
11619
+ const participantsWithNames = watchedParticipants.filter((p) => p.name?.trim()).length;
11564
11620
  const totalParticipantRows = watchedParticipants.length;
11565
- const allParticipantsHaveNames = participantsWithNames === totalParticipantRows;
11621
+ const allParticipantsHaveNames = participantFieldsConfig.name.required
11622
+ ? participantsWithNames === totalParticipantRows
11623
+ : true;
11566
11624
  const participantsWithinLimit = participantsWithNames <= (eventDetails?.availableSpots || 0);
11567
11625
  const hasValidCustomerName = watchedCustomerName && watchedCustomerName.trim().length >= 2;
11568
11626
  const hasValidCustomerEmail = watchedCustomerEmail && watchedCustomerEmail.trim().length > 0 && !customerEmailError;
11569
11627
  return allParticipantsHaveNames &&
11570
11628
  participantsWithinLimit &&
11571
- participantsWithNames > 0 &&
11629
+ (participantFieldsConfig.name.required ? participantsWithNames > 0 : totalParticipantRows > 0) &&
11572
11630
  hasValidCustomerName &&
11573
11631
  hasValidCustomerEmail &&
11574
11632
  watchedAcceptTerms;
@@ -11576,7 +11634,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11576
11634
  useEffect(() => {
11577
11635
  if (appliedVouchers.length > 0) {
11578
11636
  const newBaseTotal = eventDetails?.price
11579
- ? eventDetails.price * watchedParticipants.filter((p) => p.name.trim()).length
11637
+ ? eventDetails.price * watchedParticipants.length
11580
11638
  : 0;
11581
11639
  const currentUpsellsTotal = calculateUpsellsTotal();
11582
11640
  const orderTotal = newBaseTotal + currentUpsellsTotal;
@@ -11621,7 +11679,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11621
11679
  const currentParticipants = form.getValues("participants");
11622
11680
  const availableSpots = eventDetails?.availableSpots || 0;
11623
11681
  if (currentParticipants.length < availableSpots) {
11624
- form.setValue("participants", [...currentParticipants, { name: "" }]);
11682
+ form.setValue("participants", [...currentParticipants, { name: "", level: undefined }]);
11625
11683
  }
11626
11684
  else {
11627
11685
  alert(t$1("booking.maxParticipants", { count: availableSpots }));
@@ -11740,7 +11798,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11740
11798
  justifyContent: "space-between",
11741
11799
  alignItems: "center",
11742
11800
  marginBottom: "16px",
11743
- }, children: jsx("h2", { style: { ...sectionHeaderStyles$1, marginBottom: 0 }, children: t$1("booking.participants") }) }), jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "16px" }, children: [watchedParticipants.map((_, index) => (jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [jsxs("div", { style: { display: "flex", gap: "12px", alignItems: "center" }, children: [jsxs("div", { style: { flex: 1 }, children: [jsx("label", { htmlFor: `participant-name-${index}`, style: labelStyles$1, children: t$1("booking.participantName") }), jsx("input", { id: `participant-name-${index}`, ...form.register(`participants.${index}.name`), type: "text", style: inputStyles$1, placeholder: t$1("booking.participantNamePlaceholder") }), form.formState.errors.participants?.[index]?.name && (jsx("p", { style: errorTextStyles$1, children: form.formState.errors.participants[index]?.name?.message }))] }), jsxs("div", { style: { width: "80px" }, children: [jsx("label", { htmlFor: `participant-age-${index}`, style: labelStyles$1, children: t$1("booking.participantAge") }), jsx("input", { id: `participant-age-${index}`, ...form.register(`participants.${index}.age`, {
11801
+ }, children: jsx("h2", { style: { ...sectionHeaderStyles$1, marginBottom: 0 }, children: t$1("booking.participants") }) }), jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "16px" }, children: [watchedParticipants.map((_, index) => (jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [jsxs("div", { style: { display: "flex", gap: "12px", alignItems: "center" }, children: [participantFieldsConfig.name.enabled && (jsxs("div", { style: { flex: 1 }, children: [jsx("label", { htmlFor: `participant-name-${index}`, style: labelStyles$1, children: t$1("booking.participantName") }), jsx("input", { id: `participant-name-${index}`, ...form.register(`participants.${index}.name`), type: "text", style: inputStyles$1, placeholder: t$1("booking.participantNamePlaceholder") }), form.formState.errors.participants?.[index]?.name && (jsx("p", { style: errorTextStyles$1, children: form.formState.errors.participants[index]?.name?.message }))] })), participantFieldsConfig.age.enabled && (jsxs("div", { style: { width: "80px" }, children: [jsx("label", { htmlFor: `participant-age-${index}`, style: labelStyles$1, children: t$1("booking.participantAge") }), jsx("input", { id: `participant-age-${index}`, ...form.register(`participants.${index}.age`, {
11744
11802
  setValueAs: (value) => {
11745
11803
  if (value === "" || value === null || value === undefined) {
11746
11804
  return undefined;
@@ -11748,7 +11806,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11748
11806
  const num = Number(value);
11749
11807
  return Number.isNaN(num) ? undefined : num;
11750
11808
  },
11751
- }), type: "number", min: "0", max: "120", style: inputStyles$1, placeholder: "25" })] }), watchedParticipants.length > 1 && (jsxs("div", { children: [jsx("label", { style: { ...labelStyles$1, visibility: "hidden" }, children: "\u00A0" }), jsx("button", { type: "button", onClick: () => removeParticipant(index), style: {
11809
+ }), type: "number", min: "0", max: "120", style: inputStyles$1, placeholder: "25" })] })), watchedParticipants.length > 1 && (jsxs("div", { children: [jsx("label", { style: { ...labelStyles$1, visibility: "hidden" }, children: "\u00A0" }), jsx("button", { type: "button", onClick: () => removeParticipant(index), style: {
11752
11810
  color: "var(--bw-error-color)",
11753
11811
  backgroundColor: "var(--bw-surface-color)",
11754
11812
  border: "1px solid var(--bw-border-color)",
@@ -11764,7 +11822,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11764
11822
  fontWeight: 700,
11765
11823
  fontFamily: "var(--bw-font-family)",
11766
11824
  padding: 0,
11767
- }, children: "\u00D7" })] }))] }), upsells.length > 0 && (jsx("div", { style: participantUpsellStyles.container, children: upsells.map((upsell) => {
11825
+ }, children: "\u00D7" })] }))] }), participantFieldsConfig.level.enabled && (jsxs("div", { style: { minWidth: "140px" }, children: [jsx("label", { htmlFor: `participant-level-${index}`, style: labelStyles$1, children: t$1("booking.participantLevel") }), jsxs("select", { id: `participant-level-${index}`, ...form.register(`participants.${index}.level`), style: inputStyles$1, children: [jsx("option", { value: "", children: t$1("booking.participantLevelPlaceholder") }), participantLevelOptions.map((level) => (jsx("option", { value: level, children: t$1(`level.${level}`) }, level)))] }), form.formState.errors.participants?.[index]?.level && (jsx("p", { style: errorTextStyles$1, children: form.formState.errors.participants[index]?.level?.message }))] })), upsells.length > 0 && (jsx("div", { style: participantUpsellStyles.container, children: upsells.map((upsell) => {
11768
11826
  const isSelected = (participantUpsells[index] || []).includes(upsell.id);
11769
11827
  return (jsxs("label", { htmlFor: `upsell-${index}-${upsell.id}`, style: isSelected ? participantUpsellStyles.labelSelected : participantUpsellStyles.label, children: [jsx("input", { id: `upsell-${index}-${upsell.id}`, type: "checkbox", style: participantUpsellStyles.checkbox, checked: isSelected, onChange: () => toggleParticipantUpsell(index, upsell.id) }), jsx("span", { style: { fontWeight: 500 }, children: upsell.name }), jsxs("span", { style: { fontSize: "12px", opacity: 0.8 }, children: ["(+", formatCurrency(upsell.price), ")"] })] }, upsell.id));
11770
11828
  }) }))] }, index))), watchedParticipants.length < eventDetails.availableSpots ? (jsx("div", { style: {
@@ -11793,9 +11851,9 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11793
11851
  color: "var(--bw-text-color)",
11794
11852
  fontWeight: 500,
11795
11853
  fontFamily: "var(--bw-font-family)",
11796
- }, children: [jsxs("span", { style: { fontWeight: 200 }, children: [watchedParticipants.length > 1 ? watchedParticipants.filter((p) => p.name.trim()).length : 1, " x "] }), " ", formatCurrency(eventDetails.price)] })] }), upsellsTotal > 0 && (jsxs("div", { style: { marginTop: "8px", paddingTop: "8px", borderTop: "1px dashed var(--bw-border-color)" }, children: [jsxs("span", { style: { color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "13px", display: "block", marginBottom: "4px" }, children: [t$1("common.extras"), ":"] }), upsells.map((upsell) => {
11854
+ }, children: [jsxs("span", { style: { fontWeight: 200 }, children: [watchedParticipants.length > 1 ? watchedParticipants.length : 1, " x "] }), " ", formatCurrency(eventDetails.price)] })] }), upsellsTotal > 0 && (jsxs("div", { style: { marginTop: "8px", paddingTop: "8px", borderTop: "1px dashed var(--bw-border-color)" }, children: [jsxs("span", { style: { color: "var(--bw-text-muted)", fontFamily: "var(--bw-font-family)", fontSize: "13px", display: "block", marginBottom: "4px" }, children: [t$1("common.extras"), ":"] }), upsells.map((upsell) => {
11797
11855
  // Count how many participants have this upsell selected
11798
- const countWithUpsell = watchedParticipants.filter((p, idx) => p.name.trim() && (participantUpsells[idx] || []).includes(upsell.id)).length;
11856
+ const countWithUpsell = watchedParticipants.filter((_, idx) => (participantUpsells[idx] || []).includes(upsell.id)).length;
11799
11857
  if (countWithUpsell === 0)
11800
11858
  return null;
11801
11859
  const upsellLineTotal = upsell.price * countWithUpsell;
@@ -11896,15 +11954,17 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11896
11954
  }, children: t$1("summary.remainingOnSite", { amount: formatCurrency(totalAmount - depositAmount) }) }))] })] })] }), jsx("div", { ref: paymentSectionRef, children: (stripePromise || systemConfig?.paymentProvider === "mollie") &&
11897
11955
  (() => {
11898
11956
  if (!isReadyForPayment()) {
11899
- const participantsWithNames = watchedParticipants.filter((p) => p.name.trim()).length;
11957
+ const participantsWithNames = watchedParticipants.filter((p) => p.name?.trim()).length;
11900
11958
  const totalParticipantRows = watchedParticipants.length;
11901
11959
  const participantsWithoutNames = totalParticipantRows - participantsWithNames;
11902
11960
  const missing = [];
11903
- if (participantsWithNames === 0) {
11904
- missing.push(t$1("payment.needParticipant"));
11905
- }
11906
- else if (participantsWithoutNames > 0) {
11907
- missing.push(t$1("payment.needAllNames", { count: totalParticipantRows }));
11961
+ if (participantFieldsConfig.name.required) {
11962
+ if (participantsWithNames === 0) {
11963
+ missing.push(t$1("payment.needParticipant"));
11964
+ }
11965
+ else if (participantsWithoutNames > 0) {
11966
+ missing.push(t$1("payment.needAllNames", { count: totalParticipantRows }));
11967
+ }
11908
11968
  }
11909
11969
  if (participantsWithNames > (eventDetails?.availableSpots || 0)) {
11910
11970
  missing.push(t$1("payment.reduceParticipants", { count: eventDetails?.availableSpots || 0 }));
@@ -11948,121 +12008,145 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
11948
12008
  }
11949
12009
 
11950
12010
  /**
11951
- * Google Ads Conversion Tracking Utility
12011
+ * Google Ads Tracking Utility
11952
12012
  *
11953
- * Simplified utility that waits 1500ms, checks/initializes gtag, and sends conversion.
11954
- */
11955
- /**
11956
- * Check if gtag is available in current or parent window
12013
+ * Handles pageview tracking (widget load) and conversion tracking (successful booking).
12014
+ * Supports both direct gtag.js and GTM dataLayer setups.
11957
12015
  */
11958
12016
  function isGtagAvailable() {
11959
- if (typeof window === "undefined") {
12017
+ if (typeof window === "undefined")
11960
12018
  return false;
11961
- }
11962
- // Check current window
11963
- if (typeof window.gtag === "function") {
12019
+ if (typeof window.gtag === "function")
11964
12020
  return true;
11965
- }
11966
- // Check parent window (for iframe/widget scenarios)
11967
12021
  if (window !== window.parent) {
11968
12022
  try {
11969
- if (typeof window.parent?.gtag === "function") {
12023
+ if (typeof window.parent?.gtag === "function")
11970
12024
  return true;
11971
- }
11972
12025
  }
11973
12026
  catch {
11974
- // Cannot access parent window (cross-origin)
12027
+ // Cross-origin
11975
12028
  }
11976
12029
  }
11977
12030
  return false;
11978
12031
  }
11979
- /**
11980
- * Initialize gtag if not already available
11981
- */
11982
12032
  function initializeGtag(tagId) {
11983
- if (typeof window === "undefined") {
12033
+ if (typeof window === "undefined" || isGtagAvailable())
11984
12034
  return;
11985
- }
11986
- // Skip if gtag already exists
11987
- if (isGtagAvailable()) {
11988
- return;
11989
- }
11990
- // Initialize dataLayer and gtag function
11991
12035
  window.dataLayer = window.dataLayer || [];
11992
12036
  window.gtag = (...args) => {
11993
12037
  window.dataLayer.push(args);
11994
12038
  };
11995
- // Set current timestamp
11996
12039
  window.gtag("js", new Date());
11997
- // Load gtag script
11998
12040
  const script = document.createElement("script");
11999
12041
  script.async = true;
12000
12042
  script.src = `https://www.googletagmanager.com/gtag/js?id=${tagId}`;
12001
12043
  document.head.appendChild(script);
12002
- // Configure the tag
12003
12044
  window.gtag("config", tagId, {
12004
12045
  anonymize_ip: true,
12005
12046
  allow_google_signals: false,
12006
12047
  allow_ad_personalization_signals: false,
12007
12048
  });
12008
12049
  }
12009
- /**
12010
- * Send conversion event using available gtag
12011
- */
12012
- function sendConversion(config) {
12013
- if (typeof window === "undefined") {
12014
- return;
12050
+ function getGtag() {
12051
+ if (typeof window === "undefined")
12052
+ return null;
12053
+ if (typeof window.gtag === "function") {
12054
+ return window.gtag;
12015
12055
  }
12016
- let gtag = window.gtag;
12017
- // Try parent window gtag if current window doesn't have it
12018
- if (typeof gtag !== "function" && window !== window.parent) {
12056
+ if (window !== window.parent) {
12019
12057
  try {
12020
- gtag = window.parent?.gtag;
12058
+ const parentGtag = window.parent?.gtag;
12059
+ if (typeof parentGtag === "function")
12060
+ return parentGtag;
12021
12061
  }
12022
12062
  catch {
12023
- // Cannot access parent window (cross-origin)
12063
+ // Cross-origin
12024
12064
  }
12025
12065
  }
12026
- if (typeof gtag !== "function") {
12066
+ return null;
12067
+ }
12068
+ /**
12069
+ * Push an event to the dataLayer for GTM visibility,
12070
+ * then also fire via gtag for direct Google Ads tracking.
12071
+ */
12072
+ function sendEvent(eventName, params) {
12073
+ if (typeof window === "undefined")
12027
12074
  return;
12075
+ // GTM dataLayer push (object format — visible in Tag Assistant)
12076
+ window.dataLayer = window.dataLayer || [];
12077
+ window.dataLayer.push({
12078
+ event: eventName,
12079
+ ...params,
12080
+ });
12081
+ // gtag call (array format — processed by gtag.js for Google Ads)
12082
+ const gtag = getGtag();
12083
+ if (gtag) {
12084
+ gtag("event", eventName, params);
12028
12085
  }
12029
- // Build conversion data
12030
- const conversionData = {
12086
+ }
12087
+ function sendConversion(config) {
12088
+ const params = {
12031
12089
  send_to: `${config.tagId}/${config.conversionId}`,
12032
12090
  };
12033
- // Add optional parameters
12034
12091
  if (config.conversionValue !== undefined) {
12035
- conversionData.value = config.conversionValue;
12092
+ params.value = config.conversionValue;
12036
12093
  }
12037
12094
  if (config.conversionCurrency) {
12038
- conversionData.currency = config.conversionCurrency;
12095
+ params.currency = config.conversionCurrency;
12039
12096
  }
12040
12097
  if (config.transactionId) {
12041
- conversionData.transaction_id = config.transactionId;
12098
+ params.transaction_id = config.transactionId;
12042
12099
  }
12043
- // Send conversion event
12044
- gtag("event", "conversion", conversionData);
12100
+ sendEvent("conversion", params);
12045
12101
  }
12046
12102
  /**
12047
- * Main function to handle Google Ads conversion tracking
12048
- * Waits 1500ms, checks/initializes gtag, then sends conversion
12103
+ * Track widget pageview (fired once on widget mount).
12049
12104
  */
12050
- function handleGoogleAdsConversion(config) {
12051
- // Validate required config
12052
- if (!config.tagId || !config.conversionId) {
12105
+ function handleGoogleAdsPageview(tagId, consent) {
12106
+ if (!tagId || false || typeof window === "undefined")
12053
12107
  return;
12108
+ if (!isGtagAvailable()) {
12109
+ initializeGtag(tagId);
12054
12110
  }
12055
- // Wait 1500ms before proceeding
12111
+ const fire = () => sendEvent("widget_pageview", {
12112
+ send_to: tagId,
12113
+ page_location: window.location.href,
12114
+ page_title: document.title,
12115
+ });
12116
+ if (isGtagAvailable()) {
12117
+ fire();
12118
+ return;
12119
+ }
12120
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${tagId}"]`);
12121
+ if (script) {
12122
+ script.addEventListener("load", fire, { once: true });
12123
+ }
12124
+ }
12125
+ /**
12126
+ * Handle Google Ads conversion tracking.
12127
+ * Waits 1500ms for the success page to settle, then fires.
12128
+ */
12129
+ function handleGoogleAdsConversion(config) {
12130
+ if (!config.tagId || !config.conversionId)
12131
+ return;
12056
12132
  setTimeout(() => {
12057
- // Check if gtag is available, initialize if not
12058
- if (!isGtagAvailable()) {
12059
- initializeGtag(config.tagId);
12133
+ if (isGtagAvailable()) {
12134
+ sendConversion(config);
12135
+ return;
12136
+ }
12137
+ initializeGtag(config.tagId);
12138
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${config.tagId}"]`);
12139
+ if (script) {
12140
+ script.addEventListener("load", () => sendConversion(config));
12141
+ script.addEventListener("error", () => sendConversion(config));
12142
+ }
12143
+ else {
12144
+ sendConversion(config);
12060
12145
  }
12061
- sendConversion(config);
12062
12146
  }, 1500);
12063
12147
  }
12064
12148
 
12065
- const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, }) => {
12149
+ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, googleAdsConfig: googleAdsConfigProp, }) => {
12066
12150
  const t = useTranslations();
12067
12151
  const { locale } = useLocale();
12068
12152
  const timezone = useTimezone();
@@ -12079,7 +12163,7 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12079
12163
  try {
12080
12164
  const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/get-booking-by-payment"), {
12081
12165
  method: "POST",
12082
- headers: createApiHeaders(config),
12166
+ headers: createApiHeaders(config, locale),
12083
12167
  body: JSON.stringify(createRequestBody(config, {
12084
12168
  paymentIntentId: targetPaymentIntentId,
12085
12169
  })),
@@ -12111,20 +12195,16 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12111
12195
  });
12112
12196
  setPaymentStatus(data.stripePaymentIntent?.status || data.order.status);
12113
12197
  const finalPaymentStatus = data.stripePaymentIntent?.status || data.order.status;
12198
+ const adsConfig = googleAdsConfigProp ?? data.googleAdsConfig;
12114
12199
  if (finalPaymentStatus === "succeeded" &&
12115
- config.googleAds?.tagId &&
12116
- config.googleAds?.conversionId &&
12117
- config.googleAds?.consent !== false) {
12118
- // Prepare conversion tracking data
12119
- const conversionValue = data.order.total / 100;
12120
- const transactionId = data.order.id;
12121
- // Track the conversion
12200
+ adsConfig?.tagId &&
12201
+ adsConfig?.conversionId) {
12122
12202
  handleGoogleAdsConversion({
12123
- tagId: config.googleAds.tagId,
12124
- conversionId: config.googleAds.conversionId,
12125
- conversionValue,
12126
- conversionCurrency: config.googleAds.conversionCurrency || "EUR",
12127
- transactionId,
12203
+ tagId: adsConfig.tagId,
12204
+ conversionId: adsConfig.conversionId,
12205
+ conversionValue: data.order.total / 100,
12206
+ conversionCurrency: adsConfig.conversionCurrency || "EUR",
12207
+ transactionId: data.order.id,
12128
12208
  });
12129
12209
  }
12130
12210
  }
@@ -12313,7 +12393,7 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12313
12393
  flexDirection: "column",
12314
12394
  gap: "var(--bw-spacing-small)",
12315
12395
  }, children: formData.participants
12316
- .filter((p) => p.name.trim())
12396
+ .filter((p) => p.name?.trim() || p.age || p.level)
12317
12397
  .map((participant, index) => (jsx("div", { className: "print-participant", style: {
12318
12398
  display: "flex",
12319
12399
  justifyContent: "space-between",
@@ -12324,11 +12404,15 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12324
12404
  }, children: jsxs("div", { className: "print-participant-info", children: [jsx("div", { className: "print-participant-name", style: {
12325
12405
  color: "var(--bw-text-color)",
12326
12406
  fontFamily: "var(--bw-font-family)",
12327
- }, children: participant.name }), participant.age && (jsx("div", { className: "print-participant-age", style: {
12407
+ }, children: participant.name || `#${index + 1}` }), participant.age && (jsx("div", { className: "print-participant-age", style: {
12328
12408
  color: "var(--bw-text-muted)",
12329
12409
  fontSize: "var(--bw-font-size-small)",
12330
12410
  fontFamily: "var(--bw-font-family)",
12331
- }, children: t("success.age", { age: participant.age }) }))] }) }, index))) }) })] })), jsxs("div", { className: "print-booking-card", style: {
12411
+ }, children: t("success.age", { age: participant.age }) })), participant.level && (jsxs("div", { style: {
12412
+ color: "var(--bw-text-muted)",
12413
+ fontSize: "var(--bw-font-size-small)",
12414
+ fontFamily: "var(--bw-font-family)",
12415
+ }, children: [t("booking.participantLevel"), ": ", t(`level.${participant.level}`)] }))] }) }, index))) }) })] })), jsxs("div", { className: "print-booking-card", style: {
12332
12416
  backgroundColor: "var(--bw-surface-color)",
12333
12417
  border: `1px solid var(--bw-border-color)`,
12334
12418
  borderRadius: "var(--bw-border-radius)",
@@ -15385,6 +15469,13 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15385
15469
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = useState(false);
15386
15470
  const [shouldRenderUpsells, setShouldRenderUpsells] = useState(false);
15387
15471
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = useState(false);
15472
+ // Google Ads config (received from API, set once from the first API response)
15473
+ const [googleAdsConfig, setGoogleAdsConfig] = useState(null);
15474
+ const extractGoogleAdsConfig = (data) => {
15475
+ if (!googleAdsConfig && data?.googleAdsConfig?.tagId) {
15476
+ setGoogleAdsConfig(data.googleAdsConfig);
15477
+ }
15478
+ };
15388
15479
  // Promo dialog state
15389
15480
  const [showPromoDialog, setShowPromoDialog] = useState(false);
15390
15481
  const [widgetContainerRef, setWidgetContainerRef] = useState(null);
@@ -15468,6 +15559,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15468
15559
  image: resolvedImage,
15469
15560
  };
15470
15561
  setVoucherConfig(mergedConfig);
15562
+ extractGoogleAdsConfig(data);
15471
15563
  setVoucherEventTypes(data.eventTypes || []);
15472
15564
  // Set system config for payment processing
15473
15565
  if (data.paymentProvider) {
@@ -15490,6 +15582,14 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15490
15582
  setIsLoadingVoucherConfig(false);
15491
15583
  }
15492
15584
  };
15585
+ // Fire widget pageview once when Google Ads config is received from API
15586
+ const pageviewFiredRef = useRef(false);
15587
+ useEffect(() => {
15588
+ if (!pageviewFiredRef.current && googleAdsConfig?.tagId) {
15589
+ pageviewFiredRef.current = true;
15590
+ handleGoogleAdsPageview(googleAdsConfig.tagId);
15591
+ }
15592
+ }, [googleAdsConfig]);
15493
15593
  // Determine initial step and load data
15494
15594
  useEffect(() => {
15495
15595
  const initializeWidget = async () => {
@@ -15703,6 +15803,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15703
15803
  onWidgetLanguage?.(wl);
15704
15804
  onTimezone?.(wl.timezone);
15705
15805
  }
15806
+ extractGoogleAdsConfig(data);
15706
15807
  setEventTypes(data.eventTypes);
15707
15808
  if (isSingleEventTypeMode && data.eventTypes.length === 1) {
15708
15809
  setSelectedEventType(data.eventTypes[0]);
@@ -15739,6 +15840,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15739
15840
  onWidgetLanguage?.(wl);
15740
15841
  onTimezone?.(wl.timezone);
15741
15842
  }
15843
+ extractGoogleAdsConfig(data);
15742
15844
  setUpcomingEvents(data.upcomingEvents || []);
15743
15845
  }
15744
15846
  else {
@@ -15774,6 +15876,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15774
15876
  onWidgetLanguage?.(wl);
15775
15877
  onTimezone?.(wl.timezone);
15776
15878
  }
15879
+ extractGoogleAdsConfig(data);
15777
15880
  setSpecials(data.specials || []);
15778
15881
  }
15779
15882
  else {
@@ -15800,6 +15903,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15800
15903
  onWidgetLanguage?.(wl);
15801
15904
  onTimezone?.(wl.timezone);
15802
15905
  }
15906
+ extractGoogleAdsConfig(data);
15803
15907
  setEventInstances(data.eventInstances);
15804
15908
  if (data.paymentProvider) {
15805
15909
  setSystemConfig({
@@ -15860,6 +15964,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15860
15964
  onWidgetLanguage?.(wl);
15861
15965
  onTimezone?.(wl.timezone);
15862
15966
  }
15967
+ extractGoogleAdsConfig(data);
15863
15968
  setEventDetails(data.eventDetails);
15864
15969
  setSystemConfig({
15865
15970
  paymentProvider: data.paymentProvider,
@@ -15929,6 +16034,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15929
16034
  onWidgetLanguage?.(wl);
15930
16035
  onTimezone?.(wl.timezone);
15931
16036
  }
16037
+ extractGoogleAdsConfig(data);
15932
16038
  return data.upsells || [];
15933
16039
  }
15934
16040
  else {
@@ -16342,7 +16448,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16342
16448
  url.searchParams.delete("mollie_payment_id");
16343
16449
  url.searchParams.delete("mollie_status");
16344
16450
  window.history.replaceState({}, "", url.toString());
16345
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16451
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16346
16452
  }
16347
16453
  if (viewMode === "specials" && showingPreview) {
16348
16454
  return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [jsx(SpecialsView, { specials: specials, onEventSelect: handleUpcomingEventSelect, isLoading: isLoadingSpecials, showSavingsAmount: config.specialsSettings?.showSavingsAmount ?? true, showSavingsPercent: config.specialsSettings?.showSavingsPercent ?? false, emptyStateText: config.specialsSettings?.emptyStateText }), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, onBackToEventInstances: () => {
@@ -16367,7 +16473,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16367
16473
  setShouldRenderBookingForm(false);
16368
16474
  setSelectedUpsells([]);
16369
16475
  setUpsells([]);
16370
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16476
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16371
16477
  }
16372
16478
  if (viewMode === "next-events" && !showingPreview && currentStep === "eventInstances") {
16373
16479
  return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => {
@@ -16390,7 +16496,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16390
16496
  url.searchParams.delete("mollie_payment_id");
16391
16497
  url.searchParams.delete("mollie_status");
16392
16498
  window.history.replaceState({}, "", url.toString());
16393
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16499
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16394
16500
  }
16395
16501
  if (viewMode === "button" && (isSingleEventTypeMode || isDirectInstanceMode)) {
16396
16502
  return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, style: {
@@ -16436,7 +16542,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16436
16542
  url.searchParams.delete("mollie_payment_id");
16437
16543
  url.searchParams.delete("mollie_status");
16438
16544
  window.history.replaceState({}, "", url.toString());
16439
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16545
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16440
16546
  }
16441
16547
  // Cards mode (default) - show event type selection with optional voucher card
16442
16548
  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: {
@@ -16492,7 +16598,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16492
16598
  url.searchParams.delete("mollie_payment_id");
16493
16599
  url.searchParams.delete("mollie_status");
16494
16600
  window.history.replaceState({}, "", url.toString());
16495
- }, config: config, 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: () => {
16601
+ }, 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: () => {
16496
16602
  setIsSuccess(false);
16497
16603
  setVoucherPurchaseResult(null);
16498
16604
  const url = new URL(window.location.href);
@@ -16507,11 +16613,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16507
16613
  function UniversalBookingWidget(props) {
16508
16614
  const [languagePolicy, setLanguagePolicy] = useState(null);
16509
16615
  const [orgTimezone, setOrgTimezone] = useState("Europe/Berlin");
16510
- const serverLockedLocale = languagePolicy && !languagePolicy.multiLanguageEnabled
16511
- ? languagePolicy.organizationLocale
16512
- : undefined;
16513
- const i18nConfigLocale = serverLockedLocale !== undefined ? serverLockedLocale : props.config.locale;
16514
- const providerProps = i18nConfigLocale ? { configLocale: i18nConfigLocale } : {};
16616
+ const providerProps = props.config.locale ? { configLocale: props.config.locale } : {};
16515
16617
  const showLanguagePicker = props.config.showLanguagePicker !== false &&
16516
16618
  (languagePolicy === null || languagePolicy.multiLanguageEnabled);
16517
16619
  return (jsx(I18nProvider, { ...providerProps, children: jsx(ShowLanguagePickerProvider, { value: showLanguagePicker, children: jsx(TimezoneProvider, { value: orgTimezone, children: jsx(UniversalBookingWidgetInner, { ...props, onWidgetLanguage: setLanguagePolicy, onTimezone: setOrgTimezone }) }) }) }));