@bigz-app/booking-widget 1.3.0 → 1.3.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/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 }));
@@ -12079,7 +12139,7 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12079
12139
  try {
12080
12140
  const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/get-booking-by-payment"), {
12081
12141
  method: "POST",
12082
- headers: createApiHeaders(config),
12142
+ headers: createApiHeaders(config, locale),
12083
12143
  body: JSON.stringify(createRequestBody(config, {
12084
12144
  paymentIntentId: targetPaymentIntentId,
12085
12145
  })),
@@ -12313,7 +12373,7 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12313
12373
  flexDirection: "column",
12314
12374
  gap: "var(--bw-spacing-small)",
12315
12375
  }, children: formData.participants
12316
- .filter((p) => p.name.trim())
12376
+ .filter((p) => p.name?.trim() || p.age || p.level)
12317
12377
  .map((participant, index) => (jsx("div", { className: "print-participant", style: {
12318
12378
  display: "flex",
12319
12379
  justifyContent: "space-between",
@@ -12324,11 +12384,15 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12324
12384
  }, children: jsxs("div", { className: "print-participant-info", children: [jsx("div", { className: "print-participant-name", style: {
12325
12385
  color: "var(--bw-text-color)",
12326
12386
  fontFamily: "var(--bw-font-family)",
12327
- }, children: participant.name }), participant.age && (jsx("div", { className: "print-participant-age", style: {
12387
+ }, children: participant.name || `#${index + 1}` }), participant.age && (jsx("div", { className: "print-participant-age", style: {
12328
12388
  color: "var(--bw-text-muted)",
12329
12389
  fontSize: "var(--bw-font-size-small)",
12330
12390
  fontFamily: "var(--bw-font-family)",
12331
- }, children: t("success.age", { age: participant.age }) }))] }) }, index))) }) })] })), jsxs("div", { className: "print-booking-card", style: {
12391
+ }, children: t("success.age", { age: participant.age }) })), participant.level && (jsxs("div", { style: {
12392
+ color: "var(--bw-text-muted)",
12393
+ fontSize: "var(--bw-font-size-small)",
12394
+ fontFamily: "var(--bw-font-family)",
12395
+ }, children: [t("booking.participantLevel"), ": ", t(`level.${participant.level}`)] }))] }) }, index))) }) })] })), jsxs("div", { className: "print-booking-card", style: {
12332
12396
  backgroundColor: "var(--bw-surface-color)",
12333
12397
  border: `1px solid var(--bw-border-color)`,
12334
12398
  borderRadius: "var(--bw-border-radius)",
@@ -16507,11 +16571,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16507
16571
  function UniversalBookingWidget(props) {
16508
16572
  const [languagePolicy, setLanguagePolicy] = useState(null);
16509
16573
  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 } : {};
16574
+ const providerProps = props.config.locale ? { configLocale: props.config.locale } : {};
16515
16575
  const showLanguagePicker = props.config.showLanguagePicker !== false &&
16516
16576
  (languagePolicy === null || languagePolicy.multiLanguageEnabled);
16517
16577
  return (jsx(I18nProvider, { ...providerProps, children: jsx(ShowLanguagePickerProvider, { value: showLanguagePicker, children: jsx(TimezoneProvider, { value: orgTimezone, children: jsx(UniversalBookingWidgetInner, { ...props, onWidgetLanguage: setLanguagePolicy, onTimezone: setOrgTimezone }) }) }) }));