@infuro/cms-core 1.0.71 → 1.0.73

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/admin.cjs CHANGED
@@ -381,9 +381,8 @@ var init_vendor_scope = __esm({
381
381
  }
382
382
  });
383
383
  function AdminHeader() {
384
- const { data: session } = react.useSession();
384
+ const { data: session, update } = react.useSession();
385
385
  const sessionUser = session?.user;
386
- const roleLabel = sessionUser?.groupName?.trim() || (sessionUser?.isRBACAdmin ? "Administrator" : "Admin user");
387
386
  const isVendor = isVendorPortalUser(sessionUser);
388
387
  const isMobile = useIsMobile();
389
388
  const configuredLogo = process.env.NEXT_PUBLIC_ADMIN_LOGO_URL || DEFAULT_ADMIN_LOGO;
@@ -392,9 +391,125 @@ function AdminHeader() {
392
391
  const isDataLogo = React26.useMemo(() => logoSrc.startsWith("data:"), [
393
392
  logoSrc
394
393
  ]);
394
+ const [isSwitching, setIsSwitching] = React26.useState(false);
395
+ const [cookieVendorId, setCookieVendorId] = React26.useState(() => {
396
+ if (typeof window === "undefined") return null;
397
+ try {
398
+ const match = document.cookie.match(/(?:^|;\s*)infuro_active_vendor_id=(\d+)/);
399
+ if (match && match[1]) return Number(match[1]);
400
+ const stored = localStorage.getItem("infuro_active_vendor_id");
401
+ if (stored) return Number(stored);
402
+ } catch {
403
+ }
404
+ return null;
405
+ });
406
+ const [fetchedStores, setFetchedStores] = React26.useState(null);
407
+ const [fetchedRoleName, setFetchedRoleName] = React26.useState(null);
408
+ const [fetchedActiveVendorId, setFetchedActiveVendorId] = React26.useState(null);
409
+ React26.useEffect(() => {
410
+ if (sessionUser?.vendorStores && sessionUser.vendorStores.length > 1 && !cookieVendorId) {
411
+ return;
412
+ }
413
+ let isMounted = true;
414
+ fetch("/api/admin/vendor/context").then((res) => res.ok ? res.json() : null).then((data) => {
415
+ if (!isMounted || !data) return;
416
+ if (Array.isArray(data.vendorStores)) {
417
+ setFetchedStores(data.vendorStores);
418
+ }
419
+ if (data.vendorRoleName) {
420
+ setFetchedRoleName(data.vendorRoleName);
421
+ }
422
+ if (typeof data.activeVendorId === "number") {
423
+ setFetchedActiveVendorId(data.activeVendorId);
424
+ }
425
+ try {
426
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
427
+ window.dispatchEvent(new CustomEvent("infuro_vendor_context_updated", {
428
+ detail: data
429
+ }));
430
+ } catch {
431
+ }
432
+ }).catch(() => {
433
+ });
434
+ return () => {
435
+ isMounted = false;
436
+ };
437
+ }, [
438
+ sessionUser?.email,
439
+ sessionUser?.vendorStores,
440
+ cookieVendorId
441
+ ]);
442
+ const stores = React26.useMemo(() => {
443
+ if (sessionUser?.vendorStores && sessionUser.vendorStores.length > 0) {
444
+ return sessionUser.vendorStores;
445
+ }
446
+ return fetchedStores || [];
447
+ }, [
448
+ sessionUser?.vendorStores,
449
+ fetchedStores
450
+ ]);
451
+ const effectiveActiveVendorId = cookieVendorId ?? sessionUser?.activeVendorId ?? fetchedActiveVendorId;
452
+ const activeStore = React26.useMemo(() => {
453
+ return stores.find((s) => s.id === effectiveActiveVendorId) || (stores.length > 0 ? stores[0] : null);
454
+ }, [
455
+ stores,
456
+ effectiveActiveVendorId
457
+ ]);
458
+ const effectiveRoleName = activeStore?.roleName || sessionUser?.vendorRoleName || fetchedRoleName;
459
+ const roleLabel = ((isVendor || stores.length > 0) && effectiveRoleName ? effectiveRoleName : null) || sessionUser?.groupName?.trim() || (sessionUser?.isRBACAdmin ? "Administrator" : "Admin user");
395
460
  const handleLogout = /* @__PURE__ */ __name(() => {
396
461
  void adminSignOut();
397
462
  }, "handleLogout");
463
+ const handleSwitchStore = /* @__PURE__ */ __name(async (vendorId) => {
464
+ if (vendorId === activeStore?.id || isSwitching) return;
465
+ try {
466
+ setIsSwitching(true);
467
+ document.cookie = `infuro_active_vendor_id=${vendorId}; path=/; max-age=31536000; SameSite=Lax`;
468
+ try {
469
+ localStorage.setItem("infuro_active_vendor_id", String(vendorId));
470
+ } catch {
471
+ }
472
+ setCookieVendorId(vendorId);
473
+ const res = await fetch("/api/admin/vendor/switch", {
474
+ method: "POST",
475
+ headers: {
476
+ "Content-Type": "application/json"
477
+ },
478
+ body: JSON.stringify({
479
+ vendorId
480
+ })
481
+ });
482
+ if (res.ok) {
483
+ const data = await res.json();
484
+ try {
485
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
486
+ window.dispatchEvent(new CustomEvent("infuro_vendor_context_updated", {
487
+ detail: data
488
+ }));
489
+ } catch {
490
+ }
491
+ if (typeof update === "function") {
492
+ try {
493
+ await update({
494
+ activeVendorId: vendorId,
495
+ vendorRole: data.vendorRole,
496
+ vendorRoleId: data.vendorRoleId,
497
+ vendorRoleName: data.vendorRoleName,
498
+ isVendorRoleOwner: data.isVendorRoleOwner,
499
+ vendorEntityPerms: data.vendorEntityPerms,
500
+ vendorStores: data.vendorStores
501
+ });
502
+ } catch {
503
+ }
504
+ }
505
+ window.location.reload();
506
+ }
507
+ } catch (err) {
508
+ console.error("Failed to switch store:", err);
509
+ } finally {
510
+ setIsSwitching(false);
511
+ }
512
+ }, "handleSwitchStore");
398
513
  return /* @__PURE__ */ React.createElement("header", {
399
514
  className: "bg-white border-b border-gray-200 px-4 py-2"
400
515
  }, /* @__PURE__ */ React.createElement("div", {
@@ -427,7 +542,46 @@ function AdminHeader() {
427
542
  className: "text-sm font-semibold text-gray-800"
428
543
  }, "Infuro"))), /* @__PURE__ */ React.createElement("div", {
429
544
  className: "flex items-center space-x-3"
430
- }, /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
545
+ }, stores.length > 1 && /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
546
+ asChild: true
547
+ }, /* @__PURE__ */ React.createElement(Button, {
548
+ variant: "outline",
549
+ size: "sm",
550
+ disabled: isSwitching,
551
+ className: "h-8 px-2.5 flex items-center gap-1.5 border-gray-200 bg-gray-50/80 hover:bg-gray-100 text-gray-800 text-xs font-medium rounded-lg shadow-none transition-colors"
552
+ }, isSwitching ? /* @__PURE__ */ React.createElement(LucideIcons.Loader2, {
553
+ className: "h-3.5 w-3.5 animate-spin text-gray-500 shrink-0"
554
+ }) : /* @__PURE__ */ React.createElement(LucideIcons.Store, {
555
+ className: "h-3.5 w-3.5 text-gray-500 shrink-0"
556
+ }), /* @__PURE__ */ React.createElement("span", {
557
+ className: "max-w-[130px] sm:max-w-[180px] truncate font-semibold"
558
+ }, activeStore?.name || "Select Store"), activeStore?.roleName && /* @__PURE__ */ React.createElement("span", {
559
+ className: "hidden sm:inline-block px-1.5 py-0.5 text-[10px] font-medium bg-gray-200/80 text-gray-700 rounded"
560
+ }, activeStore.roleName), /* @__PURE__ */ React.createElement(LucideIcons.ChevronDown, {
561
+ className: "h-3 w-3 text-gray-400 shrink-0"
562
+ }))), /* @__PURE__ */ React.createElement(DropdownMenuContent, {
563
+ align: "end",
564
+ className: "w-56 p-1"
565
+ }, /* @__PURE__ */ React.createElement(DropdownMenuLabel, {
566
+ className: "text-[11px] font-semibold text-gray-500 uppercase px-2 py-1"
567
+ }, "Switch Store"), /* @__PURE__ */ React.createElement(DropdownMenuSeparator, {
568
+ className: "my-1"
569
+ }), stores.map((st) => {
570
+ const isSelected = st.id === activeStore?.id;
571
+ return /* @__PURE__ */ React.createElement(DropdownMenuItem, {
572
+ key: st.id,
573
+ onClick: /* @__PURE__ */ __name(() => handleSwitchStore(st.id), "onClick"),
574
+ className: "flex items-center justify-between px-2.5 py-2 cursor-pointer text-xs rounded-md"
575
+ }, /* @__PURE__ */ React.createElement("div", {
576
+ className: "flex flex-col gap-0.5 truncate pr-2"
577
+ }, /* @__PURE__ */ React.createElement("span", {
578
+ className: `font-medium ${isSelected ? "text-primary font-semibold" : "text-gray-800"}`
579
+ }, st.name), st.roleName && /* @__PURE__ */ React.createElement("span", {
580
+ className: "text-[10px] text-gray-400"
581
+ }, st.roleName)), isSelected && /* @__PURE__ */ React.createElement(LucideIcons.Check, {
582
+ className: "h-3.5 w-3.5 text-primary shrink-0"
583
+ }));
584
+ }))), /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
431
585
  asChild: true
432
586
  }, /* @__PURE__ */ React.createElement(Button, {
433
587
  variant: "ghost",
@@ -895,7 +1049,7 @@ var init_admin_config_context = __esm({
895
1049
  var CMS_VERSION;
896
1050
  var init_cms_version = __esm({
897
1051
  "src/lib/cms-version.ts"() {
898
- CMS_VERSION = "1.0.71" ;
1052
+ CMS_VERSION = "1.0.73" ;
899
1053
  }
900
1054
  });
901
1055
  function useCatalogCategories(enabled = true) {
@@ -1003,8 +1157,54 @@ function AdminSidebar({ variant = "sidebar" }) {
1003
1157
  const searchParams = navigation.useSearchParams();
1004
1158
  const { data: session } = react.useSession();
1005
1159
  const sessionUser = session?.user;
1006
- const showVendorOnboard = canOnboardVendors(sessionUser);
1007
- const vendorPortal = isVendorPortalUser(sessionUser);
1160
+ const [vendorContext, setVendorContext] = React26.useState(() => {
1161
+ if (typeof window === "undefined") return null;
1162
+ try {
1163
+ const stored = localStorage.getItem("infuro_active_vendor_context");
1164
+ if (stored) return JSON.parse(stored);
1165
+ } catch {
1166
+ }
1167
+ return null;
1168
+ });
1169
+ React26.useEffect(() => {
1170
+ const onContextUpdated = /* @__PURE__ */ __name((e) => {
1171
+ const detail = e.detail;
1172
+ if (detail) setVendorContext(detail);
1173
+ }, "onContextUpdated");
1174
+ window.addEventListener("infuro_vendor_context_updated", onContextUpdated);
1175
+ fetch("/api/admin/vendor/context").then((r) => r.ok ? r.json() : null).then((data) => {
1176
+ if (data) {
1177
+ setVendorContext(data);
1178
+ try {
1179
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
1180
+ } catch {
1181
+ }
1182
+ }
1183
+ }).catch(() => {
1184
+ });
1185
+ return () => {
1186
+ window.removeEventListener("infuro_vendor_context_updated", onContextUpdated);
1187
+ };
1188
+ }, []);
1189
+ const effectiveSessionUser = React26.useMemo(() => {
1190
+ if (!sessionUser) return null;
1191
+ if (!vendorContext) return sessionUser;
1192
+ return {
1193
+ ...sessionUser,
1194
+ activeVendorId: vendorContext.activeVendorId ?? sessionUser.activeVendorId,
1195
+ vendorRole: vendorContext.vendorRole ?? sessionUser.vendorRole,
1196
+ vendorRoleId: vendorContext.vendorRoleId ?? sessionUser.vendorRoleId,
1197
+ vendorRoleName: vendorContext.vendorRoleName ?? sessionUser.vendorRoleName,
1198
+ isVendorRoleOwner: vendorContext.isVendorRoleOwner ?? sessionUser.isVendorRoleOwner,
1199
+ vendorEntityPerms: vendorContext.vendorEntityPerms ?? sessionUser.vendorEntityPerms,
1200
+ vendorStores: vendorContext.vendorStores ?? sessionUser.vendorStores
1201
+ };
1202
+ }, [
1203
+ sessionUser,
1204
+ vendorContext
1205
+ ]);
1206
+ const showVendorOnboard = canOnboardVendors(effectiveSessionUser);
1207
+ const vendorPortal = isVendorPortalUser(effectiveSessionUser);
1008
1208
  const { customNavItems, customNavSections = [], storeEnabled, multiVendorEnabled, eventsEnabled, vendorCanCreateCategories, vendorCanCreateCollections, vendorCanCreateBrands } = React26.useContext(exports.AdminConfigContext);
1009
1209
  const showStoreNav = storeEnabled || vendorPortal;
1010
1210
  const showPlatformNav = !vendorPortal;
@@ -1031,8 +1231,8 @@ function AdminSidebar({ variant = "sidebar" }) {
1031
1231
  const headingCls = "text-[11px] font-semibold text-gray-400 uppercase tracking-wider px-2.5 mb-1.5";
1032
1232
  const asideCls = isDrawer ? "w-full h-full min-h-0 bg-white flex flex-col overflow-hidden" : "w-52 h-full min-h-0 bg-white border-r border-gray-200 flex-shrink-0 flex flex-col overflow-hidden";
1033
1233
  const canReadEntity = /* @__PURE__ */ __name((entity) => {
1034
- if (!sessionUser) return true;
1035
- return sessionHasEntityAccessFromExplanation(sessionUser, entity, "read");
1234
+ if (!effectiveSessionUser) return true;
1235
+ return sessionHasEntityAccessFromExplanation(effectiveSessionUser, entity, "read");
1036
1236
  }, "canReadEntity");
1037
1237
  return /* @__PURE__ */ React.createElement("aside", {
1038
1238
  className: asideCls
@@ -1242,12 +1442,12 @@ function AdminSidebar({ variant = "sidebar" }) {
1242
1442
  className: `${linkCls} ${isActive("/admin/vendor-profile") ? linkActive : linkInactive}`
1243
1443
  }, /* @__PURE__ */ React.createElement(LucideIcons.User, {
1244
1444
  className: `h-4 w-4 mr-2 ${isActive("/admin/vendor-profile") ? iconActive : iconInactive}`
1245
- }), "Profile")), canManageVendorTeam(sessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1445
+ }), "Profile")), canManageVendorTeam(effectiveSessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1246
1446
  href: "/admin/vendor-team",
1247
1447
  className: `${linkCls} ${isActive("/admin/vendor-team") ? linkActive : linkInactive}`
1248
1448
  }, /* @__PURE__ */ React.createElement(LucideIcons.Users, {
1249
1449
  className: `h-4 w-4 mr-2 ${isActive("/admin/vendor-team") ? iconActive : iconInactive}`
1250
- }), "Team")), canManageVendorRoles(sessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1450
+ }), "Team")), canManageVendorRoles(effectiveSessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1251
1451
  href: "/admin/vendor-roles",
1252
1452
  className: `${linkCls} ${isActive("/admin/vendor-roles") ? linkActive : linkInactive}`
1253
1453
  }, /* @__PURE__ */ React.createElement(LucideIcons.Shield, {
@@ -6438,6 +6638,20 @@ var init_FieldConfiguration = __esm({
6438
6638
  }
6439
6639
  });
6440
6640
 
6641
+ // src/lib/slug-sanitizer.ts
6642
+ function sanitizeSlugInput(value) {
6643
+ return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-");
6644
+ }
6645
+ function normalizeSlug(value) {
6646
+ return sanitizeSlugInput(value).replace(/^-+|-+$/g, "");
6647
+ }
6648
+ var init_slug_sanitizer = __esm({
6649
+ "src/lib/slug-sanitizer.ts"() {
6650
+ __name(sanitizeSlugInput, "sanitizeSlugInput");
6651
+ __name(normalizeSlug, "normalizeSlug");
6652
+ }
6653
+ });
6654
+
6441
6655
  // src/components/Admin/FormBuilder.tsx
6442
6656
  var FormBuilder_exports = {};
6443
6657
  __export(FormBuilder_exports, {
@@ -6516,18 +6730,6 @@ function FormBuilder({ formId, duplicateFromId }) {
6516
6730
  loadId,
6517
6731
  loadFormData
6518
6732
  ]);
6519
- React26.useEffect(() => {
6520
- if (formData.name && !formData.slug) {
6521
- const generatedSlug = formData.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
6522
- setFormData((prev) => ({
6523
- ...prev,
6524
- slug: generatedSlug
6525
- }));
6526
- }
6527
- }, [
6528
- formData.name,
6529
- formData.slug
6530
- ]);
6531
6733
  const addField = /* @__PURE__ */ __name(() => {
6532
6734
  const newField = {
6533
6735
  id: `field_${Date.now()}`,
@@ -6632,6 +6834,7 @@ function FormBuilder({ formId, duplicateFromId }) {
6632
6834
  setErrors([]);
6633
6835
  const payload = {
6634
6836
  ...formData,
6837
+ slug: normalizeSlug(formData.slug),
6635
6838
  published: isPublishing ? true : formData.published,
6636
6839
  fields: formData.fields.map((field) => {
6637
6840
  const numericId = typeof field.id === "number" ? field.id : /^\d+$/.test(String(field.id)) ? parseInt(String(field.id), 10) : void 0;
@@ -6762,8 +6965,12 @@ function FormBuilder({ formId, duplicateFromId }) {
6762
6965
  value: formData.slug,
6763
6966
  onChange: /* @__PURE__ */ __name((e) => setFormData((prev) => ({
6764
6967
  ...prev,
6765
- slug: e.target.value
6968
+ slug: sanitizeSlugInput(e.target.value)
6766
6969
  })), "onChange"),
6970
+ onBlur: /* @__PURE__ */ __name(() => setFormData((prev) => ({
6971
+ ...prev,
6972
+ slug: normalizeSlug(prev.slug)
6973
+ })), "onBlur"),
6767
6974
  placeholder: "form-slug"
6768
6975
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
6769
6976
  className: "block text-sm font-medium text-gray-700 mb-1"
@@ -6952,6 +7159,7 @@ var init_FormBuilder = __esm({
6952
7159
  init_switch();
6953
7160
  init_badge();
6954
7161
  init_FieldConfiguration();
7162
+ init_slug_sanitizer();
6955
7163
  __name(FormBuilder, "FormBuilder");
6956
7164
  }
6957
7165
  });
@@ -8001,9 +8209,10 @@ function BlogEditor({ existingBlog, duplicateSource }) {
8001
8209
  setIsSaving(false);
8002
8210
  return;
8003
8211
  }
8212
+ const normalizedSlug = normalizeSlug(slug);
8004
8213
  const body = {
8005
8214
  title,
8006
- slug,
8215
+ slug: normalizedSlug,
8007
8216
  content,
8008
8217
  published: isPublishing,
8009
8218
  tags,
@@ -8273,7 +8482,8 @@ function BlogEditor({ existingBlog, duplicateSource }) {
8273
8482
  type: "text",
8274
8483
  placeholder: "blog-post-url",
8275
8484
  value: slug,
8276
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
8485
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
8486
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
8277
8487
  className: "h-8 text-sm"
8278
8488
  }), /* @__PURE__ */ React.createElement("p", {
8279
8489
  className: "text-xs text-gray-500 mt-1"
@@ -8366,6 +8576,7 @@ var init_BlogEditorPage = __esm({
8366
8576
  init_JoditRichText();
8367
8577
  init_CategoryAutocomplete();
8368
8578
  init_UserAutocomplete();
8579
+ init_slug_sanitizer();
8369
8580
  __name(BlogEditor, "BlogEditor");
8370
8581
  }
8371
8582
  });
@@ -24475,7 +24686,8 @@ function DetailPageHeader({ title, subtitle, backHref, backLabel = "Back", close
24475
24686
  size: "icon",
24476
24687
  className: "h-8 w-8 border border-gray-600 bg-transparent text-white hover:bg-gray-700",
24477
24688
  onClick: item.onClick,
24478
- title: item.label
24689
+ title: item.label,
24690
+ disabled: item.disabled
24479
24691
  }, /* @__PURE__ */ React.createElement(Icon2, {
24480
24692
  className: "h-4 w-4"
24481
24693
  }), /* @__PURE__ */ React.createElement("span", {
@@ -24489,7 +24701,8 @@ function DetailPageHeader({ title, subtitle, backHref, backLabel = "Back", close
24489
24701
  key: i,
24490
24702
  variant: item.variant ?? "outline",
24491
24703
  size: "sm",
24492
- onClick: item.onClick
24704
+ onClick: item.onClick,
24705
+ disabled: item.disabled
24493
24706
  }, /* @__PURE__ */ React.createElement(Icon2, {
24494
24707
  className: "h-4 w-4 mr-1"
24495
24708
  }), item.label);
@@ -26950,14 +27163,55 @@ function OrderPlacementPage({ editOrderId }) {
26950
27163
  status: "available"
26951
27164
  });
26952
27165
  if (q.length >= 2) params.set("search", q);
26953
- const res = await fetch(`/api/products?${params}`);
27166
+ const [res, epRes, evRes] = await Promise.all([
27167
+ fetch(`/api/products?${params}`),
27168
+ fetch("/api/event_products?limit=500").catch(() => null),
27169
+ fetch("/api/events?limit=500").catch(() => null)
27170
+ ]);
26954
27171
  if (!res.ok) {
26955
27172
  setProductHits([]);
26956
27173
  return;
26957
27174
  }
26958
27175
  const body = await res.json();
26959
27176
  const hits = Array.isArray(body.data) ? body.data : [];
26960
- hits.forEach((p) => productCache.current.set(p.id, p));
27177
+ const eventMap = /* @__PURE__ */ new Map();
27178
+ if (evRes && evRes.ok) {
27179
+ const evBody = await evRes.json().catch(() => ({}));
27180
+ const evList = Array.isArray(evBody.data) ? evBody.data : [];
27181
+ for (const ev of evList) {
27182
+ if (ev && ev.id) eventMap.set(Number(ev.id), ev);
27183
+ }
27184
+ }
27185
+ const productEventLimits = /* @__PURE__ */ new Map();
27186
+ if (epRes && epRes.ok) {
27187
+ const epBody = await epRes.json().catch(() => ({}));
27188
+ const epList = Array.isArray(epBody.data) ? epBody.data : [];
27189
+ for (const ep of epList) {
27190
+ const ev = eventMap.get(Number(ep.eventId));
27191
+ if (ev) {
27192
+ const maxGroup = typeof ev.maxGroupPurchaseQuantity === "number" && Number.isFinite(ev.maxGroupPurchaseQuantity) && ev.maxGroupPurchaseQuantity > 0 ? Math.floor(ev.maxGroupPurchaseQuantity) : null;
27193
+ const allowsGroup = ev.allowGroupOrders !== false;
27194
+ let cap;
27195
+ if (maxGroup !== null && maxGroup > 1) {
27196
+ cap = maxGroup;
27197
+ } else if (!allowsGroup) {
27198
+ cap = 1;
27199
+ } else if (maxGroup !== null) {
27200
+ cap = maxGroup;
27201
+ } else {
27202
+ cap = 999;
27203
+ }
27204
+ productEventLimits.set(Number(ep.productId), cap);
27205
+ }
27206
+ }
27207
+ }
27208
+ hits.forEach((p) => {
27209
+ const cap = productEventLimits.get(p.id);
27210
+ if (cap !== void 0) {
27211
+ p.maxPurchaseLimit = cap;
27212
+ }
27213
+ productCache.current.set(p.id, p);
27214
+ });
26961
27215
  setProductHits(hits);
26962
27216
  } catch {
26963
27217
  setProductHits([]);
@@ -27234,6 +27488,7 @@ function OrderPlacementPage({ editOrderId }) {
27234
27488
  sonner.toast.message("Select one or more products in the list first");
27235
27489
  return;
27236
27490
  }
27491
+ let reachedLimit = false;
27237
27492
  setLines((prev) => {
27238
27493
  const next = [
27239
27494
  ...prev
@@ -27241,29 +27496,47 @@ function OrderPlacementPage({ editOrderId }) {
27241
27496
  for (const id of stagedIds) {
27242
27497
  const hit = productCache.current.get(id);
27243
27498
  if (!hit) continue;
27499
+ const maxAllowed = typeof hit.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27244
27500
  const idx = next.findIndex((l) => l.productId === id);
27245
27501
  if (idx >= 0) {
27246
27502
  const row = next[idx];
27247
- next[idx] = {
27248
- ...row,
27249
- quantity: row.quantity + 1
27250
- };
27251
- } else next.push({
27252
- key: String(id),
27253
- productId: id,
27254
- label: hit.name ?? `Product #${id}`,
27255
- sku: hit.sku,
27256
- quantity: 1
27257
- });
27503
+ if (row.quantity >= maxAllowed) {
27504
+ reachedLimit = true;
27505
+ } else {
27506
+ next[idx] = {
27507
+ ...row,
27508
+ quantity: Math.min(maxAllowed, row.quantity + 1)
27509
+ };
27510
+ }
27511
+ } else {
27512
+ next.push({
27513
+ key: String(id),
27514
+ productId: id,
27515
+ label: hit.name ?? `Product #${id}`,
27516
+ sku: hit.sku,
27517
+ quantity: 1
27518
+ });
27519
+ }
27258
27520
  }
27259
27521
  return next;
27260
27522
  });
27261
27523
  setStagedIds(/* @__PURE__ */ new Set());
27262
- sonner.toast.success("Added to cart");
27524
+ if (reachedLimit) {
27525
+ sonner.toast.error("One or more items reached their maximum purchase limit");
27526
+ } else {
27527
+ sonner.toast.success("Added to cart");
27528
+ }
27263
27529
  }
27264
27530
  __name(addStagedToCart, "addStagedToCart");
27265
27531
  function updateQty(key, quantity) {
27266
- const q = Math.min(99999, Math.max(1, Math.floor(quantity) || 1));
27532
+ const line = lines.find((l) => l.key === key);
27533
+ const hit = line ? productCache.current.get(line.productId) : null;
27534
+ const maxAllowed = typeof hit?.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27535
+ const parsed = Math.max(1, Math.floor(quantity) || 1);
27536
+ const q = Math.min(maxAllowed, parsed);
27537
+ if (parsed > maxAllowed) {
27538
+ sonner.toast.error(`Maximum purchase limit for "${line?.label ?? "this product"}" is ${maxAllowed}`);
27539
+ }
27267
27540
  setLines((prev) => prev.map((l) => l.key === key ? {
27268
27541
  ...l,
27269
27542
  quantity: q
@@ -27313,6 +27586,13 @@ function OrderPlacementPage({ editOrderId }) {
27313
27586
  sonner.toast.error("Add at least one product to the cart");
27314
27587
  return;
27315
27588
  }
27589
+ for (const line of lines) {
27590
+ const hit = productCache.current.get(line.productId);
27591
+ if (hit?.maxPurchaseLimit && line.quantity > hit.maxPurchaseLimit) {
27592
+ sonner.toast.error(`Quantity for "${line.label}" cannot exceed maximum allowed limit of ${hit.maxPurchaseLimit}`);
27593
+ return;
27594
+ }
27595
+ }
27316
27596
  const unknown = preview?.lines.filter((l) => !l.found) ?? [];
27317
27597
  if (unknown.length > 0) {
27318
27598
  sonner.toast.error(`One or more items could not be validated: ${unknown.map((u) => u.productName).join(", ")}`);
@@ -27975,6 +28255,7 @@ function OrderPlacementPage({ editOrderId }) {
27975
28255
  }, /* @__PURE__ */ React.createElement(Input, {
27976
28256
  type: "number",
27977
28257
  min: 1,
28258
+ max: productCache.current.get(line.productId)?.maxPurchaseLimit ?? void 0,
27978
28259
  className: "h-8 w-14 ml-auto text-right",
27979
28260
  value: line.quantity,
27980
28261
  onChange: /* @__PURE__ */ __name((e) => updateQty(line.key, Number(e.target.value)), "onChange"),
@@ -30575,9 +30856,10 @@ function SaveButton({ pageId, pageData, existingSeoId, onSeoIdChange, onSaved, c
30575
30856
  }
30576
30857
  }
30577
30858
  }
30859
+ const normalizedSlug = normalizeSlug(pageData.slug);
30578
30860
  const payload = {
30579
30861
  title: pageData.title,
30580
- slug: pageData.slug,
30862
+ slug: normalizedSlug,
30581
30863
  content,
30582
30864
  published: pageData.published
30583
30865
  };
@@ -30817,7 +31099,8 @@ function PageBuilderPage({ pageId }) {
30817
31099
  className: "block text-xs font-medium text-gray-600 mb-1"
30818
31100
  }, "Slug *"), /* @__PURE__ */ React26__namespace.default.createElement(Input, {
30819
31101
  value: slug,
30820
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
31102
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
31103
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
30821
31104
  placeholder: "page-url-slug",
30822
31105
  className: "h-8 text-sm"
30823
31106
  })))), /* @__PURE__ */ React26__namespace.default.createElement(RightSidebar, {
@@ -30854,6 +31137,7 @@ var init_PageBuilderPage = __esm({
30854
31137
  init_admin_config_context();
30855
31138
  init_registry();
30856
31139
  init_ImageOrUrlField();
31140
+ init_slug_sanitizer();
30857
31141
  __name(createSelectable, "createSelectable");
30858
31142
  __name(buildEditorResolver, "buildEditorResolver");
30859
31143
  __name(getIcon, "getIcon");
@@ -31087,7 +31371,8 @@ function BrandEditPage({ brandId }) {
31087
31371
  ]);
31088
31372
  return;
31089
31373
  }
31090
- if (!slug.trim()) {
31374
+ const normalizedSlug = normalizeSlug(slug);
31375
+ if (!normalizedSlug) {
31091
31376
  setErrors([
31092
31377
  "Slug is required"
31093
31378
  ]);
@@ -31095,10 +31380,10 @@ function BrandEditPage({ brandId }) {
31095
31380
  }
31096
31381
  setSaving(true);
31097
31382
  try {
31098
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
31383
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
31099
31384
  const payload = {
31100
31385
  name: name.trim(),
31101
- slug: slug.trim(),
31386
+ slug: normalizedSlug,
31102
31387
  description: description || null,
31103
31388
  logo: logo || null,
31104
31389
  active,
@@ -31188,7 +31473,9 @@ function BrandEditPage({ brandId }) {
31188
31473
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
31189
31474
  type: "text",
31190
31475
  value: slug,
31191
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
31476
+ placeholder: "brand-slug",
31477
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
31478
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
31192
31479
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm",
31193
31480
  required: true
31194
31481
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -31248,6 +31535,7 @@ var init_BrandEditPage = __esm({
31248
31535
  init_DetailPageHeader();
31249
31536
  init_vendor_scope();
31250
31537
  init_ImageOrUrlField();
31538
+ init_slug_sanitizer();
31251
31539
  isCreate = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
31252
31540
  __name(BrandEditPage, "BrandEditPage");
31253
31541
  }
@@ -32712,11 +33000,21 @@ function ProductEditPage({ productId }) {
32712
33000
  const pcData = await pcRes.json();
32713
33001
  const pcs = Array.isArray(pcData.data) ? pcData.data : [];
32714
33002
  if (pcs.length > 0) {
32715
- setTaxRows(pcs.map((p) => ({
32716
- taxId: p.taxId,
32717
- rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
32718
- })));
32719
- const firstRefund = pcs[0].refundPolicyId;
33003
+ const validTaxPcs = pcs.filter((p) => p.taxId != null && Number(p.taxId) > 0);
33004
+ if (validTaxPcs.length > 0) {
33005
+ setTaxRows(validTaxPcs.map((p) => ({
33006
+ taxId: p.taxId,
33007
+ rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
33008
+ })));
33009
+ } else if (!cancelled) {
33010
+ setTaxRows([
33011
+ {
33012
+ taxId: "",
33013
+ rate: ""
33014
+ }
33015
+ ]);
33016
+ }
33017
+ const firstRefund = pcs.find((p) => p.refundPolicyId != null)?.refundPolicyId;
32720
33018
  setRefundPolicyId(firstRefund != null ? Number(firstRefund) : null);
32721
33019
  } else if (!cancelled) {
32722
33020
  setTaxRows([
@@ -32725,6 +33023,7 @@ function ProductEditPage({ productId }) {
32725
33023
  rate: ""
32726
33024
  }
32727
33025
  ]);
33026
+ setRefundPolicyId(null);
32728
33027
  }
32729
33028
  }
32730
33029
  const paRes = await fetch(`/api/product_attributes?productId=${sourceProductId}&limit=100`);
@@ -33101,14 +33400,7 @@ function ProductEditPage({ productId }) {
33101
33400
  if (a == null || b == null) return true;
33102
33401
  return Math.abs(a - b) > 1e-6;
33103
33402
  }, "ratesDiffer");
33104
- const wantedConfig = /* @__PURE__ */ new Map();
33105
- for (const row of taxRows) {
33106
- if (row.taxId === "") continue;
33107
- wantedConfig.set(row.taxId, {
33108
- rate: parseTaxRate(row.rate),
33109
- refundPolicyId
33110
- });
33111
- }
33403
+ const validTaxRows = taxRows.filter((row) => row.taxId !== "");
33112
33404
  console.log("[product_config rows to save]:", taxRows, "refundPolicyId:", refundPolicyId);
33113
33405
  const pcListRes = await fetch(`/api/product_config?productId=${savedId}&limit=200`);
33114
33406
  const pcListData = pcListRes.ok ? await pcListRes.json() : {
@@ -33116,17 +33408,60 @@ function ProductEditPage({ productId }) {
33116
33408
  };
33117
33409
  console.log("[product existing pc rows]:", pcListData);
33118
33410
  const existingPc = Array.isArray(pcListData.data) ? pcListData.data : [];
33119
- for (const ep of existingPc) {
33120
- if (!wantedConfig.has(ep.taxId)) {
33121
- await fetch(`/api/product_config/${ep.id}`, {
33122
- method: "DELETE"
33123
- });
33411
+ if (validTaxRows.length > 0) {
33412
+ const wantedTaxIds = new Set(validTaxRows.map((r) => Number(r.taxId)));
33413
+ for (const ep of existingPc) {
33414
+ if (ep.taxId == null || !wantedTaxIds.has(Number(ep.taxId))) {
33415
+ await fetch(`/api/product_config/${ep.id}`, {
33416
+ method: "DELETE"
33417
+ });
33418
+ }
33124
33419
  }
33125
- }
33126
- const survivors = existingPc.filter((ep) => wantedConfig.has(ep.taxId));
33127
- for (const [taxId, cfg] of wantedConfig) {
33128
- const ep = survivors.find((e) => e.taxId === taxId);
33129
- if (!ep) {
33420
+ for (const row of validTaxRows) {
33421
+ const taxId = Number(row.taxId);
33422
+ const rate = parseTaxRate(row.rate);
33423
+ const ep = existingPc.find((e) => e.taxId != null && Number(e.taxId) === taxId);
33424
+ if (!ep) {
33425
+ await fetch("/api/product_config", {
33426
+ method: "POST",
33427
+ headers: {
33428
+ "Content-Type": "application/json"
33429
+ },
33430
+ body: JSON.stringify({
33431
+ productId: Number(savedId),
33432
+ taxId,
33433
+ rate,
33434
+ refundPolicyId
33435
+ })
33436
+ });
33437
+ } else {
33438
+ const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33439
+ const er = Number.isFinite(existingRate) ? existingRate : null;
33440
+ const policyChanged = ep.refundPolicyId !== refundPolicyId;
33441
+ if (ratesDiffer(er, rate) || policyChanged) {
33442
+ await fetch(`/api/product_config/${ep.id}`, {
33443
+ method: "PUT",
33444
+ headers: {
33445
+ "Content-Type": "application/json"
33446
+ },
33447
+ body: JSON.stringify({
33448
+ rate,
33449
+ refundPolicyId
33450
+ })
33451
+ });
33452
+ }
33453
+ }
33454
+ }
33455
+ } else if (refundPolicyId != null) {
33456
+ const nullTaxRow = existingPc.find((e) => e.taxId == null);
33457
+ for (const ep of existingPc) {
33458
+ if (ep.id !== nullTaxRow?.id) {
33459
+ await fetch(`/api/product_config/${ep.id}`, {
33460
+ method: "DELETE"
33461
+ });
33462
+ }
33463
+ }
33464
+ if (!nullTaxRow) {
33130
33465
  await fetch("/api/product_config", {
33131
33466
  method: "POST",
33132
33467
  headers: {
@@ -33134,27 +33469,27 @@ function ProductEditPage({ productId }) {
33134
33469
  },
33135
33470
  body: JSON.stringify({
33136
33471
  productId: Number(savedId),
33137
- taxId,
33138
- rate: cfg.rate,
33139
- refundPolicyId: cfg.refundPolicyId
33472
+ taxId: null,
33473
+ rate: null,
33474
+ refundPolicyId
33140
33475
  })
33141
33476
  });
33142
- } else {
33143
- const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33144
- const er = Number.isFinite(existingRate) ? existingRate : null;
33145
- const policyChanged = ep.refundPolicyId !== cfg.refundPolicyId;
33146
- if (ratesDiffer(er, cfg.rate) || policyChanged) {
33147
- await fetch(`/api/product_config/${ep.id}`, {
33148
- method: "PUT",
33149
- headers: {
33150
- "Content-Type": "application/json"
33151
- },
33152
- body: JSON.stringify({
33153
- rate: cfg.rate,
33154
- refundPolicyId: cfg.refundPolicyId
33155
- })
33156
- });
33157
- }
33477
+ } else if (nullTaxRow.refundPolicyId !== refundPolicyId) {
33478
+ await fetch(`/api/product_config/${nullTaxRow.id}`, {
33479
+ method: "PUT",
33480
+ headers: {
33481
+ "Content-Type": "application/json"
33482
+ },
33483
+ body: JSON.stringify({
33484
+ refundPolicyId
33485
+ })
33486
+ });
33487
+ }
33488
+ } else {
33489
+ for (const ep of existingPc) {
33490
+ await fetch(`/api/product_config/${ep.id}`, {
33491
+ method: "DELETE"
33492
+ });
33158
33493
  }
33159
33494
  }
33160
33495
  if (hasVariants) {
@@ -34070,7 +34405,8 @@ function CollectionEditPage({ collectionId }) {
34070
34405
  ]);
34071
34406
  return;
34072
34407
  }
34073
- if (!slug.trim()) {
34408
+ const normalizedSlug = normalizeSlug(slug);
34409
+ if (!normalizedSlug) {
34074
34410
  setErrors([
34075
34411
  "Slug is required"
34076
34412
  ]);
@@ -34078,10 +34414,10 @@ function CollectionEditPage({ collectionId }) {
34078
34414
  }
34079
34415
  setSaving(true);
34080
34416
  try {
34081
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
34417
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
34082
34418
  const payload = {
34083
34419
  name: name.trim(),
34084
- slug: slug.trim(),
34420
+ slug: normalizedSlug,
34085
34421
  hsn: hsn.trim() || null,
34086
34422
  categoryId: categoryId || null,
34087
34423
  brandId: brandId || null,
@@ -34250,7 +34586,9 @@ function CollectionEditPage({ collectionId }) {
34250
34586
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
34251
34587
  type: "text",
34252
34588
  value: slug,
34253
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
34589
+ placeholder: "collection-slug",
34590
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
34591
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
34254
34592
  className: inputCls4,
34255
34593
  required: true
34256
34594
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -34486,6 +34824,7 @@ var init_CollectionEditPage = __esm({
34486
34824
  init_admin_config_context();
34487
34825
  init_category_related_product_labels();
34488
34826
  init_ImageOrUrlField();
34827
+ init_slug_sanitizer();
34489
34828
  isCreate3 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
34490
34829
  emptySlide = /* @__PURE__ */ __name(() => ({
34491
34830
  url: "",
@@ -35665,7 +36004,9 @@ var init_event_entity_types = __esm({
35665
36004
  // src/admin/pages/EventEditPage.tsx
35666
36005
  var EventEditPage_exports = {};
35667
36006
  __export(EventEditPage_exports, {
35668
- default: () => EventEditPage
36007
+ default: () => EventEditPage,
36008
+ normalizeSlug: () => normalizeSlug2,
36009
+ sanitizeSlugInput: () => sanitizeSlugInput2
35669
36010
  });
35670
36011
  function RequiredLabel({ children }) {
35671
36012
  return /* @__PURE__ */ React.createElement("label", {
@@ -35674,6 +36015,12 @@ function RequiredLabel({ children }) {
35674
36015
  className: "text-red-600"
35675
36016
  }, "*"));
35676
36017
  }
36018
+ function sanitizeSlugInput2(value) {
36019
+ return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-");
36020
+ }
36021
+ function normalizeSlug2(value) {
36022
+ return sanitizeSlugInput2(value).replace(/^-+|-+$/g, "");
36023
+ }
35677
36024
  function toIsoOrNull(value, timezone) {
35678
36025
  const trimmed = value.trim();
35679
36026
  if (!trimmed) return null;
@@ -35764,7 +36111,7 @@ function EventEditPage({ eventId }) {
35764
36111
  const [supportContact, setSupportContact] = React26.useState("");
35765
36112
  const [allowGroupOrders, setAllowGroupOrders] = React26.useState(false);
35766
36113
  const [sendIndividualTicketPDFToAttendees, setSendIndividualTicketPDFToAttendees] = React26.useState(false);
35767
- const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = React26.useState("");
36114
+ const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = React26.useState("1");
35768
36115
  const [sponsors, setSponsors] = React26.useState([]);
35769
36116
  const [agenda, setAgenda] = React26.useState([]);
35770
36117
  const [workshops, setWorkshops] = React26.useState([]);
@@ -35890,9 +36237,10 @@ function EventEditPage({ eventId }) {
35890
36237
  }
35891
36238
  setOfficialWebsiteUrl(data.officialWebsiteUrl ?? "");
35892
36239
  setSupportContact(data.supportContact ?? "");
35893
- setAllowGroupOrders(data.allowGroupOrders ?? false);
36240
+ const loadedAllowGroup = data.allowGroupOrders ?? false;
36241
+ setAllowGroupOrders(loadedAllowGroup);
35894
36242
  setSendIndividualTicketPDFToAttendees(data.sendIndividualTicketPDFToAttendees ?? false);
35895
- setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : "");
36243
+ setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : loadedAllowGroup ? "10" : "1");
35896
36244
  setSponsors(parseNamedListFromApi(data.sponsors, "sponsor"));
35897
36245
  setAgenda(parseNamedListFromApi(data.agenda, "agenda"));
35898
36246
  setWorkshops(parseNamedListFromApi(data.workshops, "workshop"));
@@ -35938,8 +36286,13 @@ function EventEditPage({ eventId }) {
35938
36286
  ]);
35939
36287
  const buildPayload = /* @__PURE__ */ __name(() => {
35940
36288
  const nextErrors = [];
36289
+ const normalizedSlug = normalizeSlug2(slug);
35941
36290
  if (!name.trim()) nextErrors.push("Name is required");
35942
- if (!slug.trim()) nextErrors.push("Slug is required");
36291
+ if (!normalizedSlug) {
36292
+ nextErrors.push("Slug is required");
36293
+ } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalizedSlug)) {
36294
+ nextErrors.push("Slug must contain only lowercase letters, numbers, and hyphens without leading or trailing hyphens or slashes");
36295
+ }
35943
36296
  if (!startDate.trim()) nextErrors.push("Event Start Date & Time is required");
35944
36297
  if (!endDate.trim()) nextErrors.push("Event End Date & Time is required");
35945
36298
  if (startDate.trim() && !timezone.trim()) {
@@ -35965,6 +36318,20 @@ function EventEditPage({ eventId }) {
35965
36318
  nextErrors.push("Event End Date & Time must be after Event Start Date & Time");
35966
36319
  }
35967
36320
  }
36321
+ if (registrationOpensAt.trim() && registrationClosesAt.trim()) {
36322
+ const regOpen = new Date(toIsoOrNull(registrationOpensAt, timezone) ?? "");
36323
+ const regClose = new Date(toIsoOrNull(registrationClosesAt, timezone) ?? "");
36324
+ if (!Number.isNaN(regOpen.getTime()) && !Number.isNaN(regClose.getTime()) && regClose < regOpen) {
36325
+ nextErrors.push("Registration Closes Date & Time must be after Registration Opens Date & Time");
36326
+ }
36327
+ }
36328
+ if (registrationClosesAt.trim() && endDate.trim()) {
36329
+ const regClose = new Date(toIsoOrNull(registrationClosesAt, timezone) ?? "");
36330
+ const end = new Date(toIsoOrNull(endDate, timezone) ?? "");
36331
+ if (!Number.isNaN(regClose.getTime()) && !Number.isNaN(end.getTime()) && regClose > end) {
36332
+ nextErrors.push("Registration cannot close after Event End Date & Time");
36333
+ }
36334
+ }
35968
36335
  if (additionalVenueDetails.length > ADDITIONAL_VENUE_MAX_CHARS) {
35969
36336
  nextErrors.push(`Additional Venue Details must be ${ADDITIONAL_VENUE_MAX_CHARS} characters or fewer`);
35970
36337
  }
@@ -35974,7 +36341,7 @@ function EventEditPage({ eventId }) {
35974
36341
  }
35975
36342
  const payload = {
35976
36343
  name: name.trim(),
35977
- slug: slug.trim(),
36344
+ slug: normalizedSlug,
35978
36345
  description: description.trim() || null,
35979
36346
  isActive: create && vendorPortal && approvalOn ? false : isActive,
35980
36347
  comingSoon,
@@ -36002,7 +36369,7 @@ function EventEditPage({ eventId }) {
36002
36369
  supportContact: supportContact.trim() || null,
36003
36370
  allowGroupOrders,
36004
36371
  sendIndividualTicketPDFToAttendees,
36005
- maxGroupPurchaseQuantity: maxGroupPurchaseQuantity.trim() ? Number(maxGroupPurchaseQuantity) : null,
36372
+ maxGroupPurchaseQuantity: allowGroupOrders ? Number(maxGroupPurchaseQuantity) > 0 ? Number(maxGroupPurchaseQuantity) : 10 : 1,
36006
36373
  sponsors: namedListToPayload(sponsors, {
36007
36374
  legacySponsorKeys: true
36008
36375
  }),
@@ -36270,9 +36637,16 @@ function EventEditPage({ eventId }) {
36270
36637
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36271
36638
  className: labelCls6
36272
36639
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
36640
+ id: "event-slug",
36273
36641
  type: "text",
36274
36642
  value: slug,
36275
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
36643
+ placeholder: "event-slug",
36644
+ onChange: /* @__PURE__ */ __name((e) => {
36645
+ setSlug(sanitizeSlugInput2(e.target.value));
36646
+ }, "onChange"),
36647
+ onBlur: /* @__PURE__ */ __name(() => {
36648
+ setSlug((prev) => normalizeSlug2(prev));
36649
+ }, "onBlur"),
36276
36650
  className: inputCls6
36277
36651
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36278
36652
  className: labelCls6
@@ -36532,7 +36906,16 @@ function EventEditPage({ eventId }) {
36532
36906
  }, "Max group purchase quantity"), /* @__PURE__ */ React.createElement("input", {
36533
36907
  type: "number",
36534
36908
  value: maxGroupPurchaseQuantity,
36535
- onChange: /* @__PURE__ */ __name((e) => setMaxGroupPurchaseQuantity(e.target.value), "onChange"),
36909
+ onChange: /* @__PURE__ */ __name((e) => {
36910
+ const val = e.target.value;
36911
+ setMaxGroupPurchaseQuantity(val);
36912
+ const num = Number(val);
36913
+ if (Number.isFinite(num) && num > 1) {
36914
+ setAllowGroupOrders(true);
36915
+ } else if (Number.isFinite(num) && num <= 1) {
36916
+ setAllowGroupOrders(false);
36917
+ }
36918
+ }, "onChange"),
36536
36919
  className: inputCls6,
36537
36920
  min: 1
36538
36921
  })), /* @__PURE__ */ React.createElement("div", {
@@ -36542,7 +36925,18 @@ function EventEditPage({ eventId }) {
36542
36925
  }, /* @__PURE__ */ React.createElement("input", {
36543
36926
  type: "checkbox",
36544
36927
  checked: allowGroupOrders,
36545
- onChange: /* @__PURE__ */ __name((e) => setAllowGroupOrders(e.target.checked), "onChange"),
36928
+ onChange: /* @__PURE__ */ __name((e) => {
36929
+ const checked = e.target.checked;
36930
+ setAllowGroupOrders(checked);
36931
+ if (!checked) {
36932
+ setMaxGroupPurchaseQuantity("1");
36933
+ } else {
36934
+ const current = Number(maxGroupPurchaseQuantity);
36935
+ if (!Number.isFinite(current) || current <= 1) {
36936
+ setMaxGroupPurchaseQuantity("10");
36937
+ }
36938
+ }
36939
+ }, "onChange"),
36546
36940
  className: "h-4 w-4 rounded border-gray-300"
36547
36941
  }), /* @__PURE__ */ React.createElement("span", {
36548
36942
  className: "text-sm text-gray-900"
@@ -36616,6 +37010,8 @@ var init_EventEditPage = __esm({
36616
37010
  labelCls6 = "block text-xs font-medium text-gray-600 mb-1";
36617
37011
  inputCls6 = "w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm";
36618
37012
  __name(RequiredLabel, "RequiredLabel");
37013
+ __name(sanitizeSlugInput2, "sanitizeSlugInput");
37014
+ __name(normalizeSlug2, "normalizeSlug");
36619
37015
  ENTITY_TYPE_OPTIONS = [
36620
37016
  ...EVENT_ENTITY_TYPE_OPTIONS
36621
37017
  ];
@@ -36994,7 +37390,7 @@ function ComboEditPage({ comboId }) {
36994
37390
  const resolvedEventId = trimmedEventId && /^\d+$/.test(trimmedEventId) ? Number(trimmedEventId) : null;
36995
37391
  const payload = {
36996
37392
  name: trimmedName,
36997
- slug: slug.trim() || void 0,
37393
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
36998
37394
  desc: desc || null,
36999
37395
  eventId: resolvedEventId,
37000
37396
  price,
@@ -37089,8 +37485,9 @@ function ComboEditPage({ comboId }) {
37089
37485
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
37090
37486
  type: "text",
37091
37487
  value: slug,
37092
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37093
- placeholder: "Auto generate from name",
37488
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
37489
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
37490
+ placeholder: "combo-slug",
37094
37491
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
37095
37492
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
37096
37493
  className: "block text-xs font-medium text-gray-600 mb-1"
@@ -37256,6 +37653,7 @@ var init_ComboEditPage = __esm({
37256
37653
  init_DetailPageHeader();
37257
37654
  init_inventory_validation();
37258
37655
  init_admin_config_context();
37656
+ init_slug_sanitizer();
37259
37657
  isCreate5 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
37260
37658
  __name(formatDateTimeLocal, "formatDateTimeLocal");
37261
37659
  __name(fetchEventDefaultCurrency, "fetchEventDefaultCurrency");
@@ -37319,6 +37717,7 @@ function VendorEditPage({ vendorId }) {
37319
37717
  const create = isCreate6(vendorId);
37320
37718
  const [loading, setLoading] = React26.useState(!create);
37321
37719
  const [saving, setSaving] = React26.useState(false);
37720
+ const createInFlightRef = React26.useRef(false);
37322
37721
  const [errors, setErrors] = React26.useState([]);
37323
37722
  const [name, setName] = React26.useState("");
37324
37723
  const [legalName, setLegalName] = React26.useState("");
@@ -37486,7 +37885,7 @@ function VendorEditPage({ vendorId }) {
37486
37885
  const vendorPayload = /* @__PURE__ */ __name(() => ({
37487
37886
  name: name.trim(),
37488
37887
  legalName: legalName.trim() || null,
37489
- slug: slug.trim() || void 0,
37888
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37490
37889
  businessType: businessType || null,
37491
37890
  description: description.trim() || null,
37492
37891
  website: website.trim() || null,
@@ -37677,7 +38076,7 @@ function VendorEditPage({ vendorId }) {
37677
38076
  router.push(listReturnUrl);
37678
38077
  }, "closeInviteDialog");
37679
38078
  const handleCreate = /* @__PURE__ */ __name(async () => {
37680
- setSaving(true);
38079
+ if (createInFlightRef.current) return;
37681
38080
  setErrors([]);
37682
38081
  const createMissing = [];
37683
38082
  if (!name.trim()) createMissing.push("Store name is required");
@@ -37685,14 +38084,12 @@ function VendorEditPage({ vendorId }) {
37685
38084
  if (!ownerEmail.trim()) createMissing.push("Owner email is required");
37686
38085
  if (createMissing.length) {
37687
38086
  setErrors(createMissing);
37688
- setSaving(false);
37689
38087
  return;
37690
38088
  }
37691
38089
  if (!termsAccepted) {
37692
38090
  setErrors([
37693
38091
  "You must confirm that the vendor has accepted the terms and conditions"
37694
38092
  ]);
37695
- setSaving(false);
37696
38093
  return;
37697
38094
  }
37698
38095
  const formattedOwnerPhone = formatPhoneWithCountryCode(ownerPhoneCode, ownerPhoneNum);
@@ -37703,7 +38100,6 @@ function VendorEditPage({ vendorId }) {
37703
38100
  setErrors([
37704
38101
  taxError
37705
38102
  ]);
37706
- setSaving(false);
37707
38103
  return;
37708
38104
  }
37709
38105
  const personErr = validatePersonKyc({
@@ -37716,25 +38112,22 @@ function VendorEditPage({ vendorId }) {
37716
38112
  setErrors([
37717
38113
  personErr
37718
38114
  ]);
37719
- setSaving(false);
37720
38115
  return;
37721
38116
  }
37722
38117
  if (!ownerAddressLine1.trim()) {
37723
38118
  setErrors([
37724
38119
  "Owner address line 1 is required"
37725
38120
  ]);
37726
- setSaving(false);
37727
38121
  return;
37728
38122
  }
37729
- const abortController = new AbortController();
37730
- const timeoutId = setTimeout(() => abortController.abort(), 15e3);
38123
+ createInFlightRef.current = true;
38124
+ setSaving(true);
37731
38125
  try {
37732
38126
  const res = await fetch("/api/admin/vendors/onboard", {
37733
38127
  method: "POST",
37734
38128
  headers: {
37735
38129
  "Content-Type": "application/json"
37736
38130
  },
37737
- signal: abortController.signal,
37738
38131
  body: JSON.stringify({
37739
38132
  activation: "invite",
37740
38133
  sendOwnerEmail: false,
@@ -37755,7 +38148,6 @@ function VendorEditPage({ vendorId }) {
37755
38148
  }
37756
38149
  })
37757
38150
  });
37758
- clearTimeout(timeoutId);
37759
38151
  const data = await res.json().catch(() => ({}));
37760
38152
  if (!res.ok) {
37761
38153
  setErrors([
@@ -37781,17 +38173,11 @@ function VendorEditPage({ vendorId }) {
37781
38173
  inviteLink
37782
38174
  });
37783
38175
  } catch (err) {
37784
- if (err instanceof DOMException && err.name === "AbortError") {
37785
- setErrors([
37786
- "Request timed out. Your entered data is preserved. Please click submit again."
37787
- ]);
37788
- } else {
37789
- setErrors([
37790
- err instanceof Error && err.message || "Request failed"
37791
- ]);
37792
- }
38176
+ setErrors([
38177
+ err instanceof Error && err.message || "Request failed"
38178
+ ]);
37793
38179
  } finally {
37794
- clearTimeout(timeoutId);
38180
+ createInFlightRef.current = false;
37795
38181
  setSaving(false);
37796
38182
  }
37797
38183
  }, "handleCreate");
@@ -37841,7 +38227,7 @@ function VendorEditPage({ vendorId }) {
37841
38227
  signal: abortController.signal,
37842
38228
  body: JSON.stringify({
37843
38229
  ...vendorPayload(),
37844
- slug: slug.trim(),
38230
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37845
38231
  metadata: (() => {
37846
38232
  const next = {
37847
38233
  ...metadata ?? {}
@@ -37919,13 +38305,15 @@ function VendorEditPage({ vendorId }) {
37919
38305
  {
37920
38306
  label: saving ? "Creating\u2026" : "Create vendor",
37921
38307
  icon: LucideIcons.Save,
37922
- onClick: handleCreate
38308
+ onClick: handleCreate,
38309
+ disabled: saving
37923
38310
  }
37924
38311
  ] : [
37925
38312
  {
37926
38313
  label: saving ? "Saving\u2026" : "Save",
37927
38314
  icon: LucideIcons.Save,
37928
- onClick: handleSave
38315
+ onClick: handleSave,
38316
+ disabled: saving
37929
38317
  },
37930
38318
  {
37931
38319
  label: active ? "Deactivate" : "Activate",
@@ -37962,8 +38350,9 @@ function VendorEditPage({ vendorId }) {
37962
38350
  }, "Slug ", create ? "(optional)" : "*"), /* @__PURE__ */ React26__namespace.default.createElement(Input, {
37963
38351
  id: "vendorSlug",
37964
38352
  value: slug,
37965
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37966
- placeholder: create ? "auto from name" : void 0,
38353
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
38354
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
38355
+ placeholder: "vendor-slug",
37967
38356
  className: `mt-1 ${fieldClass}`
37968
38357
  })), /* @__PURE__ */ React26__namespace.default.createElement("div", null, /* @__PURE__ */ React26__namespace.default.createElement(FieldLabel, {
37969
38358
  htmlFor: "businessType"
@@ -38304,6 +38693,7 @@ var init_VendorEditPage = __esm({
38304
38693
  init_checkbox();
38305
38694
  init_dialog();
38306
38695
  init_vendor_profile();
38696
+ init_slug_sanitizer();
38307
38697
  init_vendor_list_config();
38308
38698
  init_vendor_access_denied();
38309
38699
  init_vendor_access_denied();