@infuro/cms-core 1.0.72 → 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.js CHANGED
@@ -3,14 +3,14 @@ import { useSession, signOut, signIn } from 'next-auth/react';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import * as React26 from 'react';
6
- import React26__default, { useState, useMemo, useContext, useEffect, useRef, useCallback, Suspense, createElement, createContext } from 'react';
6
+ import React26__default, { useState, useMemo, useEffect, useContext, useRef, useCallback, Suspense, createElement, createContext } from 'react';
7
7
  var React = React26__default;
8
8
 
9
9
  import { Slot } from '@radix-ui/react-slot';
10
10
  import { cva } from 'class-variance-authority';
11
11
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
12
12
  import * as LucideIcons from 'lucide-react';
13
- import { ChevronDown, User, Settings, LogOut, LayoutDashboard, Store, FolderTree, Layers, Building2, CalendarDays, ShoppingBag, Package, Receipt, BadgePercent, ShoppingCart, CreditCard, Users, Inbox, File, LinkIcon, ClipboardList, MessageSquare, Image as Image$1, Shield, FileText, Puzzle, X, ShieldAlert, Plus, MoreVertical, Upload, Download, Edit, Copy, RefreshCw, Trash2, Mail, Check, Save, Search, AlertCircle, MapPin, BarChart3, TrendingUp, UserCheck, Activity, Globe, Eye, MousePointer, ArrowLeftRight, RotateCcw, KeyRound, Loader2, CalendarClock, CheckCircle2, XCircle, CheckSquare, Square, XSquare, Rss, ArrowLeft, PlayCircle, UserPlus, Pencil, ShieldCheck, Smartphone, Send, FolderPlus, LayoutGrid, List, Move, Archive, Edit2, ExternalLink, Power, Star, ChevronUp, Zap, Filter, ImageIcon, GripVertical, ChevronRight, ArrowRight, ArrowUpRight, IndianRupee, Clock, Play, MessageCircle, Bot, FileUp, Tag, Box, SlidersHorizontal, Calculator, Link2 as Link2$1, Unlink, Gift, MoreHorizontal, EyeOff, Phone, QrCode, Car, Settings2, DollarSign, Hash, Circle, ChevronLeft, Bell, Share2, HardDrive, Folder, Film } from 'lucide-react';
13
+ import { Loader2, Store, ChevronDown, Check, User, Settings, LogOut, LayoutDashboard, FolderTree, Layers, Building2, CalendarDays, ShoppingBag, Package, Receipt, BadgePercent, ShoppingCart, CreditCard, Users, Inbox, File, LinkIcon, ClipboardList, MessageSquare, Image as Image$1, Shield, FileText, Puzzle, X, ShieldAlert, Plus, MoreVertical, Upload, Download, Edit, Copy, RefreshCw, Trash2, Mail, Save, Search, AlertCircle, MapPin, BarChart3, TrendingUp, UserCheck, Activity, Globe, Eye, MousePointer, ArrowLeftRight, RotateCcw, KeyRound, CalendarClock, CheckCircle2, XCircle, CheckSquare, Square, XSquare, Rss, ArrowLeft, PlayCircle, UserPlus, Pencil, ShieldCheck, Smartphone, Send, FolderPlus, LayoutGrid, List, Move, Archive, Edit2, ExternalLink, Power, Star, ChevronUp, Zap, Filter, ImageIcon, GripVertical, ChevronRight, ArrowRight, ArrowUpRight, IndianRupee, Clock, Play, MessageCircle, Bot, FileUp, Tag, Box, SlidersHorizontal, Calculator, Link2 as Link2$1, Unlink, Gift, MoreHorizontal, EyeOff, Phone, QrCode, Car, Settings2, DollarSign, Hash, Circle, ChevronLeft, Bell, Share2, HardDrive, Folder, Film } from 'lucide-react';
14
14
  import Image from 'next/image';
15
15
  import Link2 from 'next/link';
16
16
  import { usePathname, useSearchParams, useRouter } from 'next/navigation';
@@ -347,9 +347,8 @@ var init_vendor_scope = __esm({
347
347
  }
348
348
  });
349
349
  function AdminHeader() {
350
- const { data: session } = useSession();
350
+ const { data: session, update } = useSession();
351
351
  const sessionUser = session?.user;
352
- const roleLabel = sessionUser?.groupName?.trim() || (sessionUser?.isRBACAdmin ? "Administrator" : "Admin user");
353
352
  const isVendor = isVendorPortalUser(sessionUser);
354
353
  const isMobile = useIsMobile();
355
354
  const configuredLogo = process.env.NEXT_PUBLIC_ADMIN_LOGO_URL || DEFAULT_ADMIN_LOGO;
@@ -358,9 +357,125 @@ function AdminHeader() {
358
357
  const isDataLogo = useMemo(() => logoSrc.startsWith("data:"), [
359
358
  logoSrc
360
359
  ]);
360
+ const [isSwitching, setIsSwitching] = useState(false);
361
+ const [cookieVendorId, setCookieVendorId] = useState(() => {
362
+ if (typeof window === "undefined") return null;
363
+ try {
364
+ const match = document.cookie.match(/(?:^|;\s*)infuro_active_vendor_id=(\d+)/);
365
+ if (match && match[1]) return Number(match[1]);
366
+ const stored = localStorage.getItem("infuro_active_vendor_id");
367
+ if (stored) return Number(stored);
368
+ } catch {
369
+ }
370
+ return null;
371
+ });
372
+ const [fetchedStores, setFetchedStores] = useState(null);
373
+ const [fetchedRoleName, setFetchedRoleName] = useState(null);
374
+ const [fetchedActiveVendorId, setFetchedActiveVendorId] = useState(null);
375
+ useEffect(() => {
376
+ if (sessionUser?.vendorStores && sessionUser.vendorStores.length > 1 && !cookieVendorId) {
377
+ return;
378
+ }
379
+ let isMounted = true;
380
+ fetch("/api/admin/vendor/context").then((res) => res.ok ? res.json() : null).then((data) => {
381
+ if (!isMounted || !data) return;
382
+ if (Array.isArray(data.vendorStores)) {
383
+ setFetchedStores(data.vendorStores);
384
+ }
385
+ if (data.vendorRoleName) {
386
+ setFetchedRoleName(data.vendorRoleName);
387
+ }
388
+ if (typeof data.activeVendorId === "number") {
389
+ setFetchedActiveVendorId(data.activeVendorId);
390
+ }
391
+ try {
392
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
393
+ window.dispatchEvent(new CustomEvent("infuro_vendor_context_updated", {
394
+ detail: data
395
+ }));
396
+ } catch {
397
+ }
398
+ }).catch(() => {
399
+ });
400
+ return () => {
401
+ isMounted = false;
402
+ };
403
+ }, [
404
+ sessionUser?.email,
405
+ sessionUser?.vendorStores,
406
+ cookieVendorId
407
+ ]);
408
+ const stores = useMemo(() => {
409
+ if (sessionUser?.vendorStores && sessionUser.vendorStores.length > 0) {
410
+ return sessionUser.vendorStores;
411
+ }
412
+ return fetchedStores || [];
413
+ }, [
414
+ sessionUser?.vendorStores,
415
+ fetchedStores
416
+ ]);
417
+ const effectiveActiveVendorId = cookieVendorId ?? sessionUser?.activeVendorId ?? fetchedActiveVendorId;
418
+ const activeStore = useMemo(() => {
419
+ return stores.find((s) => s.id === effectiveActiveVendorId) || (stores.length > 0 ? stores[0] : null);
420
+ }, [
421
+ stores,
422
+ effectiveActiveVendorId
423
+ ]);
424
+ const effectiveRoleName = activeStore?.roleName || sessionUser?.vendorRoleName || fetchedRoleName;
425
+ const roleLabel = ((isVendor || stores.length > 0) && effectiveRoleName ? effectiveRoleName : null) || sessionUser?.groupName?.trim() || (sessionUser?.isRBACAdmin ? "Administrator" : "Admin user");
361
426
  const handleLogout = /* @__PURE__ */ __name(() => {
362
427
  void adminSignOut();
363
428
  }, "handleLogout");
429
+ const handleSwitchStore = /* @__PURE__ */ __name(async (vendorId) => {
430
+ if (vendorId === activeStore?.id || isSwitching) return;
431
+ try {
432
+ setIsSwitching(true);
433
+ document.cookie = `infuro_active_vendor_id=${vendorId}; path=/; max-age=31536000; SameSite=Lax`;
434
+ try {
435
+ localStorage.setItem("infuro_active_vendor_id", String(vendorId));
436
+ } catch {
437
+ }
438
+ setCookieVendorId(vendorId);
439
+ const res = await fetch("/api/admin/vendor/switch", {
440
+ method: "POST",
441
+ headers: {
442
+ "Content-Type": "application/json"
443
+ },
444
+ body: JSON.stringify({
445
+ vendorId
446
+ })
447
+ });
448
+ if (res.ok) {
449
+ const data = await res.json();
450
+ try {
451
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
452
+ window.dispatchEvent(new CustomEvent("infuro_vendor_context_updated", {
453
+ detail: data
454
+ }));
455
+ } catch {
456
+ }
457
+ if (typeof update === "function") {
458
+ try {
459
+ await update({
460
+ activeVendorId: vendorId,
461
+ vendorRole: data.vendorRole,
462
+ vendorRoleId: data.vendorRoleId,
463
+ vendorRoleName: data.vendorRoleName,
464
+ isVendorRoleOwner: data.isVendorRoleOwner,
465
+ vendorEntityPerms: data.vendorEntityPerms,
466
+ vendorStores: data.vendorStores
467
+ });
468
+ } catch {
469
+ }
470
+ }
471
+ window.location.reload();
472
+ }
473
+ } catch (err) {
474
+ console.error("Failed to switch store:", err);
475
+ } finally {
476
+ setIsSwitching(false);
477
+ }
478
+ }, "handleSwitchStore");
364
479
  return /* @__PURE__ */ React.createElement("header", {
365
480
  className: "bg-white border-b border-gray-200 px-4 py-2"
366
481
  }, /* @__PURE__ */ React.createElement("div", {
@@ -393,7 +508,46 @@ function AdminHeader() {
393
508
  className: "text-sm font-semibold text-gray-800"
394
509
  }, "Infuro"))), /* @__PURE__ */ React.createElement("div", {
395
510
  className: "flex items-center space-x-3"
396
- }, /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
511
+ }, stores.length > 1 && /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
512
+ asChild: true
513
+ }, /* @__PURE__ */ React.createElement(Button, {
514
+ variant: "outline",
515
+ size: "sm",
516
+ disabled: isSwitching,
517
+ 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"
518
+ }, isSwitching ? /* @__PURE__ */ React.createElement(Loader2, {
519
+ className: "h-3.5 w-3.5 animate-spin text-gray-500 shrink-0"
520
+ }) : /* @__PURE__ */ React.createElement(Store, {
521
+ className: "h-3.5 w-3.5 text-gray-500 shrink-0"
522
+ }), /* @__PURE__ */ React.createElement("span", {
523
+ className: "max-w-[130px] sm:max-w-[180px] truncate font-semibold"
524
+ }, activeStore?.name || "Select Store"), activeStore?.roleName && /* @__PURE__ */ React.createElement("span", {
525
+ className: "hidden sm:inline-block px-1.5 py-0.5 text-[10px] font-medium bg-gray-200/80 text-gray-700 rounded"
526
+ }, activeStore.roleName), /* @__PURE__ */ React.createElement(ChevronDown, {
527
+ className: "h-3 w-3 text-gray-400 shrink-0"
528
+ }))), /* @__PURE__ */ React.createElement(DropdownMenuContent, {
529
+ align: "end",
530
+ className: "w-56 p-1"
531
+ }, /* @__PURE__ */ React.createElement(DropdownMenuLabel, {
532
+ className: "text-[11px] font-semibold text-gray-500 uppercase px-2 py-1"
533
+ }, "Switch Store"), /* @__PURE__ */ React.createElement(DropdownMenuSeparator, {
534
+ className: "my-1"
535
+ }), stores.map((st) => {
536
+ const isSelected = st.id === activeStore?.id;
537
+ return /* @__PURE__ */ React.createElement(DropdownMenuItem, {
538
+ key: st.id,
539
+ onClick: /* @__PURE__ */ __name(() => handleSwitchStore(st.id), "onClick"),
540
+ className: "flex items-center justify-between px-2.5 py-2 cursor-pointer text-xs rounded-md"
541
+ }, /* @__PURE__ */ React.createElement("div", {
542
+ className: "flex flex-col gap-0.5 truncate pr-2"
543
+ }, /* @__PURE__ */ React.createElement("span", {
544
+ className: `font-medium ${isSelected ? "text-primary font-semibold" : "text-gray-800"}`
545
+ }, st.name), st.roleName && /* @__PURE__ */ React.createElement("span", {
546
+ className: "text-[10px] text-gray-400"
547
+ }, st.roleName)), isSelected && /* @__PURE__ */ React.createElement(Check, {
548
+ className: "h-3.5 w-3.5 text-primary shrink-0"
549
+ }));
550
+ }))), /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
397
551
  asChild: true
398
552
  }, /* @__PURE__ */ React.createElement(Button, {
399
553
  variant: "ghost",
@@ -861,7 +1015,7 @@ var init_admin_config_context = __esm({
861
1015
  var CMS_VERSION;
862
1016
  var init_cms_version = __esm({
863
1017
  "src/lib/cms-version.ts"() {
864
- CMS_VERSION = "1.0.72" ;
1018
+ CMS_VERSION = "1.0.73" ;
865
1019
  }
866
1020
  });
867
1021
  function useCatalogCategories(enabled = true) {
@@ -969,8 +1123,54 @@ function AdminSidebar({ variant = "sidebar" }) {
969
1123
  const searchParams = useSearchParams();
970
1124
  const { data: session } = useSession();
971
1125
  const sessionUser = session?.user;
972
- const showVendorOnboard = canOnboardVendors(sessionUser);
973
- const vendorPortal = isVendorPortalUser(sessionUser);
1126
+ const [vendorContext, setVendorContext] = useState(() => {
1127
+ if (typeof window === "undefined") return null;
1128
+ try {
1129
+ const stored = localStorage.getItem("infuro_active_vendor_context");
1130
+ if (stored) return JSON.parse(stored);
1131
+ } catch {
1132
+ }
1133
+ return null;
1134
+ });
1135
+ useEffect(() => {
1136
+ const onContextUpdated = /* @__PURE__ */ __name((e) => {
1137
+ const detail = e.detail;
1138
+ if (detail) setVendorContext(detail);
1139
+ }, "onContextUpdated");
1140
+ window.addEventListener("infuro_vendor_context_updated", onContextUpdated);
1141
+ fetch("/api/admin/vendor/context").then((r) => r.ok ? r.json() : null).then((data) => {
1142
+ if (data) {
1143
+ setVendorContext(data);
1144
+ try {
1145
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ }).catch(() => {
1150
+ });
1151
+ return () => {
1152
+ window.removeEventListener("infuro_vendor_context_updated", onContextUpdated);
1153
+ };
1154
+ }, []);
1155
+ const effectiveSessionUser = useMemo(() => {
1156
+ if (!sessionUser) return null;
1157
+ if (!vendorContext) return sessionUser;
1158
+ return {
1159
+ ...sessionUser,
1160
+ activeVendorId: vendorContext.activeVendorId ?? sessionUser.activeVendorId,
1161
+ vendorRole: vendorContext.vendorRole ?? sessionUser.vendorRole,
1162
+ vendorRoleId: vendorContext.vendorRoleId ?? sessionUser.vendorRoleId,
1163
+ vendorRoleName: vendorContext.vendorRoleName ?? sessionUser.vendorRoleName,
1164
+ isVendorRoleOwner: vendorContext.isVendorRoleOwner ?? sessionUser.isVendorRoleOwner,
1165
+ vendorEntityPerms: vendorContext.vendorEntityPerms ?? sessionUser.vendorEntityPerms,
1166
+ vendorStores: vendorContext.vendorStores ?? sessionUser.vendorStores
1167
+ };
1168
+ }, [
1169
+ sessionUser,
1170
+ vendorContext
1171
+ ]);
1172
+ const showVendorOnboard = canOnboardVendors(effectiveSessionUser);
1173
+ const vendorPortal = isVendorPortalUser(effectiveSessionUser);
974
1174
  const { customNavItems, customNavSections = [], storeEnabled, multiVendorEnabled, eventsEnabled, vendorCanCreateCategories, vendorCanCreateCollections, vendorCanCreateBrands } = useContext(AdminConfigContext);
975
1175
  const showStoreNav = storeEnabled || vendorPortal;
976
1176
  const showPlatformNav = !vendorPortal;
@@ -997,8 +1197,8 @@ function AdminSidebar({ variant = "sidebar" }) {
997
1197
  const headingCls = "text-[11px] font-semibold text-gray-400 uppercase tracking-wider px-2.5 mb-1.5";
998
1198
  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";
999
1199
  const canReadEntity = /* @__PURE__ */ __name((entity) => {
1000
- if (!sessionUser) return true;
1001
- return sessionHasEntityAccessFromExplanation(sessionUser, entity, "read");
1200
+ if (!effectiveSessionUser) return true;
1201
+ return sessionHasEntityAccessFromExplanation(effectiveSessionUser, entity, "read");
1002
1202
  }, "canReadEntity");
1003
1203
  return /* @__PURE__ */ React.createElement("aside", {
1004
1204
  className: asideCls
@@ -1208,12 +1408,12 @@ function AdminSidebar({ variant = "sidebar" }) {
1208
1408
  className: `${linkCls} ${isActive("/admin/vendor-profile") ? linkActive : linkInactive}`
1209
1409
  }, /* @__PURE__ */ React.createElement(User, {
1210
1410
  className: `h-4 w-4 mr-2 ${isActive("/admin/vendor-profile") ? iconActive : iconInactive}`
1211
- }), "Profile")), canManageVendorTeam(sessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2, {
1411
+ }), "Profile")), canManageVendorTeam(effectiveSessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2, {
1212
1412
  href: "/admin/vendor-team",
1213
1413
  className: `${linkCls} ${isActive("/admin/vendor-team") ? linkActive : linkInactive}`
1214
1414
  }, /* @__PURE__ */ React.createElement(Users, {
1215
1415
  className: `h-4 w-4 mr-2 ${isActive("/admin/vendor-team") ? iconActive : iconInactive}`
1216
- }), "Team")), canManageVendorRoles(sessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2, {
1416
+ }), "Team")), canManageVendorRoles(effectiveSessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2, {
1217
1417
  href: "/admin/vendor-roles",
1218
1418
  className: `${linkCls} ${isActive("/admin/vendor-roles") ? linkActive : linkInactive}`
1219
1419
  }, /* @__PURE__ */ React.createElement(Shield, {
@@ -6404,6 +6604,20 @@ var init_FieldConfiguration = __esm({
6404
6604
  }
6405
6605
  });
6406
6606
 
6607
+ // src/lib/slug-sanitizer.ts
6608
+ function sanitizeSlugInput(value) {
6609
+ return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-");
6610
+ }
6611
+ function normalizeSlug(value) {
6612
+ return sanitizeSlugInput(value).replace(/^-+|-+$/g, "");
6613
+ }
6614
+ var init_slug_sanitizer = __esm({
6615
+ "src/lib/slug-sanitizer.ts"() {
6616
+ __name(sanitizeSlugInput, "sanitizeSlugInput");
6617
+ __name(normalizeSlug, "normalizeSlug");
6618
+ }
6619
+ });
6620
+
6407
6621
  // src/components/Admin/FormBuilder.tsx
6408
6622
  var FormBuilder_exports = {};
6409
6623
  __export(FormBuilder_exports, {
@@ -6482,18 +6696,6 @@ function FormBuilder({ formId, duplicateFromId }) {
6482
6696
  loadId,
6483
6697
  loadFormData
6484
6698
  ]);
6485
- useEffect(() => {
6486
- if (formData.name && !formData.slug) {
6487
- const generatedSlug = formData.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
6488
- setFormData((prev) => ({
6489
- ...prev,
6490
- slug: generatedSlug
6491
- }));
6492
- }
6493
- }, [
6494
- formData.name,
6495
- formData.slug
6496
- ]);
6497
6699
  const addField = /* @__PURE__ */ __name(() => {
6498
6700
  const newField = {
6499
6701
  id: `field_${Date.now()}`,
@@ -6598,6 +6800,7 @@ function FormBuilder({ formId, duplicateFromId }) {
6598
6800
  setErrors([]);
6599
6801
  const payload = {
6600
6802
  ...formData,
6803
+ slug: normalizeSlug(formData.slug),
6601
6804
  published: isPublishing ? true : formData.published,
6602
6805
  fields: formData.fields.map((field) => {
6603
6806
  const numericId = typeof field.id === "number" ? field.id : /^\d+$/.test(String(field.id)) ? parseInt(String(field.id), 10) : void 0;
@@ -6728,8 +6931,12 @@ function FormBuilder({ formId, duplicateFromId }) {
6728
6931
  value: formData.slug,
6729
6932
  onChange: /* @__PURE__ */ __name((e) => setFormData((prev) => ({
6730
6933
  ...prev,
6731
- slug: e.target.value
6934
+ slug: sanitizeSlugInput(e.target.value)
6732
6935
  })), "onChange"),
6936
+ onBlur: /* @__PURE__ */ __name(() => setFormData((prev) => ({
6937
+ ...prev,
6938
+ slug: normalizeSlug(prev.slug)
6939
+ })), "onBlur"),
6733
6940
  placeholder: "form-slug"
6734
6941
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
6735
6942
  className: "block text-sm font-medium text-gray-700 mb-1"
@@ -6918,6 +7125,7 @@ var init_FormBuilder = __esm({
6918
7125
  init_switch();
6919
7126
  init_badge();
6920
7127
  init_FieldConfiguration();
7128
+ init_slug_sanitizer();
6921
7129
  __name(FormBuilder, "FormBuilder");
6922
7130
  }
6923
7131
  });
@@ -7967,9 +8175,10 @@ function BlogEditor({ existingBlog, duplicateSource }) {
7967
8175
  setIsSaving(false);
7968
8176
  return;
7969
8177
  }
8178
+ const normalizedSlug = normalizeSlug(slug);
7970
8179
  const body = {
7971
8180
  title,
7972
- slug,
8181
+ slug: normalizedSlug,
7973
8182
  content,
7974
8183
  published: isPublishing,
7975
8184
  tags,
@@ -8239,7 +8448,8 @@ function BlogEditor({ existingBlog, duplicateSource }) {
8239
8448
  type: "text",
8240
8449
  placeholder: "blog-post-url",
8241
8450
  value: slug,
8242
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
8451
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
8452
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
8243
8453
  className: "h-8 text-sm"
8244
8454
  }), /* @__PURE__ */ React.createElement("p", {
8245
8455
  className: "text-xs text-gray-500 mt-1"
@@ -8332,6 +8542,7 @@ var init_BlogEditorPage = __esm({
8332
8542
  init_JoditRichText();
8333
8543
  init_CategoryAutocomplete();
8334
8544
  init_UserAutocomplete();
8545
+ init_slug_sanitizer();
8335
8546
  __name(BlogEditor, "BlogEditor");
8336
8547
  }
8337
8548
  });
@@ -26918,14 +27129,55 @@ function OrderPlacementPage({ editOrderId }) {
26918
27129
  status: "available"
26919
27130
  });
26920
27131
  if (q.length >= 2) params.set("search", q);
26921
- const res = await fetch(`/api/products?${params}`);
27132
+ const [res, epRes, evRes] = await Promise.all([
27133
+ fetch(`/api/products?${params}`),
27134
+ fetch("/api/event_products?limit=500").catch(() => null),
27135
+ fetch("/api/events?limit=500").catch(() => null)
27136
+ ]);
26922
27137
  if (!res.ok) {
26923
27138
  setProductHits([]);
26924
27139
  return;
26925
27140
  }
26926
27141
  const body = await res.json();
26927
27142
  const hits = Array.isArray(body.data) ? body.data : [];
26928
- hits.forEach((p) => productCache.current.set(p.id, p));
27143
+ const eventMap = /* @__PURE__ */ new Map();
27144
+ if (evRes && evRes.ok) {
27145
+ const evBody = await evRes.json().catch(() => ({}));
27146
+ const evList = Array.isArray(evBody.data) ? evBody.data : [];
27147
+ for (const ev of evList) {
27148
+ if (ev && ev.id) eventMap.set(Number(ev.id), ev);
27149
+ }
27150
+ }
27151
+ const productEventLimits = /* @__PURE__ */ new Map();
27152
+ if (epRes && epRes.ok) {
27153
+ const epBody = await epRes.json().catch(() => ({}));
27154
+ const epList = Array.isArray(epBody.data) ? epBody.data : [];
27155
+ for (const ep of epList) {
27156
+ const ev = eventMap.get(Number(ep.eventId));
27157
+ if (ev) {
27158
+ const maxGroup = typeof ev.maxGroupPurchaseQuantity === "number" && Number.isFinite(ev.maxGroupPurchaseQuantity) && ev.maxGroupPurchaseQuantity > 0 ? Math.floor(ev.maxGroupPurchaseQuantity) : null;
27159
+ const allowsGroup = ev.allowGroupOrders !== false;
27160
+ let cap;
27161
+ if (maxGroup !== null && maxGroup > 1) {
27162
+ cap = maxGroup;
27163
+ } else if (!allowsGroup) {
27164
+ cap = 1;
27165
+ } else if (maxGroup !== null) {
27166
+ cap = maxGroup;
27167
+ } else {
27168
+ cap = 999;
27169
+ }
27170
+ productEventLimits.set(Number(ep.productId), cap);
27171
+ }
27172
+ }
27173
+ }
27174
+ hits.forEach((p) => {
27175
+ const cap = productEventLimits.get(p.id);
27176
+ if (cap !== void 0) {
27177
+ p.maxPurchaseLimit = cap;
27178
+ }
27179
+ productCache.current.set(p.id, p);
27180
+ });
26929
27181
  setProductHits(hits);
26930
27182
  } catch {
26931
27183
  setProductHits([]);
@@ -27202,6 +27454,7 @@ function OrderPlacementPage({ editOrderId }) {
27202
27454
  toast.message("Select one or more products in the list first");
27203
27455
  return;
27204
27456
  }
27457
+ let reachedLimit = false;
27205
27458
  setLines((prev) => {
27206
27459
  const next = [
27207
27460
  ...prev
@@ -27209,29 +27462,47 @@ function OrderPlacementPage({ editOrderId }) {
27209
27462
  for (const id of stagedIds) {
27210
27463
  const hit = productCache.current.get(id);
27211
27464
  if (!hit) continue;
27465
+ const maxAllowed = typeof hit.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27212
27466
  const idx = next.findIndex((l) => l.productId === id);
27213
27467
  if (idx >= 0) {
27214
27468
  const row = next[idx];
27215
- next[idx] = {
27216
- ...row,
27217
- quantity: row.quantity + 1
27218
- };
27219
- } else next.push({
27220
- key: String(id),
27221
- productId: id,
27222
- label: hit.name ?? `Product #${id}`,
27223
- sku: hit.sku,
27224
- quantity: 1
27225
- });
27469
+ if (row.quantity >= maxAllowed) {
27470
+ reachedLimit = true;
27471
+ } else {
27472
+ next[idx] = {
27473
+ ...row,
27474
+ quantity: Math.min(maxAllowed, row.quantity + 1)
27475
+ };
27476
+ }
27477
+ } else {
27478
+ next.push({
27479
+ key: String(id),
27480
+ productId: id,
27481
+ label: hit.name ?? `Product #${id}`,
27482
+ sku: hit.sku,
27483
+ quantity: 1
27484
+ });
27485
+ }
27226
27486
  }
27227
27487
  return next;
27228
27488
  });
27229
27489
  setStagedIds(/* @__PURE__ */ new Set());
27230
- toast.success("Added to cart");
27490
+ if (reachedLimit) {
27491
+ toast.error("One or more items reached their maximum purchase limit");
27492
+ } else {
27493
+ toast.success("Added to cart");
27494
+ }
27231
27495
  }
27232
27496
  __name(addStagedToCart, "addStagedToCart");
27233
27497
  function updateQty(key, quantity) {
27234
- const q = Math.min(99999, Math.max(1, Math.floor(quantity) || 1));
27498
+ const line = lines.find((l) => l.key === key);
27499
+ const hit = line ? productCache.current.get(line.productId) : null;
27500
+ const maxAllowed = typeof hit?.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27501
+ const parsed = Math.max(1, Math.floor(quantity) || 1);
27502
+ const q = Math.min(maxAllowed, parsed);
27503
+ if (parsed > maxAllowed) {
27504
+ toast.error(`Maximum purchase limit for "${line?.label ?? "this product"}" is ${maxAllowed}`);
27505
+ }
27235
27506
  setLines((prev) => prev.map((l) => l.key === key ? {
27236
27507
  ...l,
27237
27508
  quantity: q
@@ -27281,6 +27552,13 @@ function OrderPlacementPage({ editOrderId }) {
27281
27552
  toast.error("Add at least one product to the cart");
27282
27553
  return;
27283
27554
  }
27555
+ for (const line of lines) {
27556
+ const hit = productCache.current.get(line.productId);
27557
+ if (hit?.maxPurchaseLimit && line.quantity > hit.maxPurchaseLimit) {
27558
+ toast.error(`Quantity for "${line.label}" cannot exceed maximum allowed limit of ${hit.maxPurchaseLimit}`);
27559
+ return;
27560
+ }
27561
+ }
27284
27562
  const unknown = preview?.lines.filter((l) => !l.found) ?? [];
27285
27563
  if (unknown.length > 0) {
27286
27564
  toast.error(`One or more items could not be validated: ${unknown.map((u) => u.productName).join(", ")}`);
@@ -27943,6 +28221,7 @@ function OrderPlacementPage({ editOrderId }) {
27943
28221
  }, /* @__PURE__ */ React.createElement(Input, {
27944
28222
  type: "number",
27945
28223
  min: 1,
28224
+ max: productCache.current.get(line.productId)?.maxPurchaseLimit ?? void 0,
27946
28225
  className: "h-8 w-14 ml-auto text-right",
27947
28226
  value: line.quantity,
27948
28227
  onChange: /* @__PURE__ */ __name((e) => updateQty(line.key, Number(e.target.value)), "onChange"),
@@ -30543,9 +30822,10 @@ function SaveButton({ pageId, pageData, existingSeoId, onSeoIdChange, onSaved, c
30543
30822
  }
30544
30823
  }
30545
30824
  }
30825
+ const normalizedSlug = normalizeSlug(pageData.slug);
30546
30826
  const payload = {
30547
30827
  title: pageData.title,
30548
- slug: pageData.slug,
30828
+ slug: normalizedSlug,
30549
30829
  content,
30550
30830
  published: pageData.published
30551
30831
  };
@@ -30785,7 +31065,8 @@ function PageBuilderPage({ pageId }) {
30785
31065
  className: "block text-xs font-medium text-gray-600 mb-1"
30786
31066
  }, "Slug *"), /* @__PURE__ */ React26__default.createElement(Input, {
30787
31067
  value: slug,
30788
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
31068
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
31069
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
30789
31070
  placeholder: "page-url-slug",
30790
31071
  className: "h-8 text-sm"
30791
31072
  })))), /* @__PURE__ */ React26__default.createElement(RightSidebar, {
@@ -30822,6 +31103,7 @@ var init_PageBuilderPage = __esm({
30822
31103
  init_admin_config_context();
30823
31104
  init_registry();
30824
31105
  init_ImageOrUrlField();
31106
+ init_slug_sanitizer();
30825
31107
  __name(createSelectable, "createSelectable");
30826
31108
  __name(buildEditorResolver, "buildEditorResolver");
30827
31109
  __name(getIcon, "getIcon");
@@ -31055,7 +31337,8 @@ function BrandEditPage({ brandId }) {
31055
31337
  ]);
31056
31338
  return;
31057
31339
  }
31058
- if (!slug.trim()) {
31340
+ const normalizedSlug = normalizeSlug(slug);
31341
+ if (!normalizedSlug) {
31059
31342
  setErrors([
31060
31343
  "Slug is required"
31061
31344
  ]);
@@ -31063,10 +31346,10 @@ function BrandEditPage({ brandId }) {
31063
31346
  }
31064
31347
  setSaving(true);
31065
31348
  try {
31066
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
31349
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
31067
31350
  const payload = {
31068
31351
  name: name.trim(),
31069
- slug: slug.trim(),
31352
+ slug: normalizedSlug,
31070
31353
  description: description || null,
31071
31354
  logo: logo || null,
31072
31355
  active,
@@ -31156,7 +31439,9 @@ function BrandEditPage({ brandId }) {
31156
31439
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
31157
31440
  type: "text",
31158
31441
  value: slug,
31159
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
31442
+ placeholder: "brand-slug",
31443
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
31444
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
31160
31445
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm",
31161
31446
  required: true
31162
31447
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -31216,6 +31501,7 @@ var init_BrandEditPage = __esm({
31216
31501
  init_DetailPageHeader();
31217
31502
  init_vendor_scope();
31218
31503
  init_ImageOrUrlField();
31504
+ init_slug_sanitizer();
31219
31505
  isCreate = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
31220
31506
  __name(BrandEditPage, "BrandEditPage");
31221
31507
  }
@@ -32680,11 +32966,21 @@ function ProductEditPage({ productId }) {
32680
32966
  const pcData = await pcRes.json();
32681
32967
  const pcs = Array.isArray(pcData.data) ? pcData.data : [];
32682
32968
  if (pcs.length > 0) {
32683
- setTaxRows(pcs.map((p) => ({
32684
- taxId: p.taxId,
32685
- rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
32686
- })));
32687
- const firstRefund = pcs[0].refundPolicyId;
32969
+ const validTaxPcs = pcs.filter((p) => p.taxId != null && Number(p.taxId) > 0);
32970
+ if (validTaxPcs.length > 0) {
32971
+ setTaxRows(validTaxPcs.map((p) => ({
32972
+ taxId: p.taxId,
32973
+ rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
32974
+ })));
32975
+ } else if (!cancelled) {
32976
+ setTaxRows([
32977
+ {
32978
+ taxId: "",
32979
+ rate: ""
32980
+ }
32981
+ ]);
32982
+ }
32983
+ const firstRefund = pcs.find((p) => p.refundPolicyId != null)?.refundPolicyId;
32688
32984
  setRefundPolicyId(firstRefund != null ? Number(firstRefund) : null);
32689
32985
  } else if (!cancelled) {
32690
32986
  setTaxRows([
@@ -32693,6 +32989,7 @@ function ProductEditPage({ productId }) {
32693
32989
  rate: ""
32694
32990
  }
32695
32991
  ]);
32992
+ setRefundPolicyId(null);
32696
32993
  }
32697
32994
  }
32698
32995
  const paRes = await fetch(`/api/product_attributes?productId=${sourceProductId}&limit=100`);
@@ -33069,14 +33366,7 @@ function ProductEditPage({ productId }) {
33069
33366
  if (a == null || b == null) return true;
33070
33367
  return Math.abs(a - b) > 1e-6;
33071
33368
  }, "ratesDiffer");
33072
- const wantedConfig = /* @__PURE__ */ new Map();
33073
- for (const row of taxRows) {
33074
- if (row.taxId === "") continue;
33075
- wantedConfig.set(row.taxId, {
33076
- rate: parseTaxRate(row.rate),
33077
- refundPolicyId
33078
- });
33079
- }
33369
+ const validTaxRows = taxRows.filter((row) => row.taxId !== "");
33080
33370
  console.log("[product_config rows to save]:", taxRows, "refundPolicyId:", refundPolicyId);
33081
33371
  const pcListRes = await fetch(`/api/product_config?productId=${savedId}&limit=200`);
33082
33372
  const pcListData = pcListRes.ok ? await pcListRes.json() : {
@@ -33084,17 +33374,60 @@ function ProductEditPage({ productId }) {
33084
33374
  };
33085
33375
  console.log("[product existing pc rows]:", pcListData);
33086
33376
  const existingPc = Array.isArray(pcListData.data) ? pcListData.data : [];
33087
- for (const ep of existingPc) {
33088
- if (!wantedConfig.has(ep.taxId)) {
33089
- await fetch(`/api/product_config/${ep.id}`, {
33090
- method: "DELETE"
33091
- });
33377
+ if (validTaxRows.length > 0) {
33378
+ const wantedTaxIds = new Set(validTaxRows.map((r) => Number(r.taxId)));
33379
+ for (const ep of existingPc) {
33380
+ if (ep.taxId == null || !wantedTaxIds.has(Number(ep.taxId))) {
33381
+ await fetch(`/api/product_config/${ep.id}`, {
33382
+ method: "DELETE"
33383
+ });
33384
+ }
33092
33385
  }
33093
- }
33094
- const survivors = existingPc.filter((ep) => wantedConfig.has(ep.taxId));
33095
- for (const [taxId, cfg] of wantedConfig) {
33096
- const ep = survivors.find((e) => e.taxId === taxId);
33097
- if (!ep) {
33386
+ for (const row of validTaxRows) {
33387
+ const taxId = Number(row.taxId);
33388
+ const rate = parseTaxRate(row.rate);
33389
+ const ep = existingPc.find((e) => e.taxId != null && Number(e.taxId) === taxId);
33390
+ if (!ep) {
33391
+ await fetch("/api/product_config", {
33392
+ method: "POST",
33393
+ headers: {
33394
+ "Content-Type": "application/json"
33395
+ },
33396
+ body: JSON.stringify({
33397
+ productId: Number(savedId),
33398
+ taxId,
33399
+ rate,
33400
+ refundPolicyId
33401
+ })
33402
+ });
33403
+ } else {
33404
+ const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33405
+ const er = Number.isFinite(existingRate) ? existingRate : null;
33406
+ const policyChanged = ep.refundPolicyId !== refundPolicyId;
33407
+ if (ratesDiffer(er, rate) || policyChanged) {
33408
+ await fetch(`/api/product_config/${ep.id}`, {
33409
+ method: "PUT",
33410
+ headers: {
33411
+ "Content-Type": "application/json"
33412
+ },
33413
+ body: JSON.stringify({
33414
+ rate,
33415
+ refundPolicyId
33416
+ })
33417
+ });
33418
+ }
33419
+ }
33420
+ }
33421
+ } else if (refundPolicyId != null) {
33422
+ const nullTaxRow = existingPc.find((e) => e.taxId == null);
33423
+ for (const ep of existingPc) {
33424
+ if (ep.id !== nullTaxRow?.id) {
33425
+ await fetch(`/api/product_config/${ep.id}`, {
33426
+ method: "DELETE"
33427
+ });
33428
+ }
33429
+ }
33430
+ if (!nullTaxRow) {
33098
33431
  await fetch("/api/product_config", {
33099
33432
  method: "POST",
33100
33433
  headers: {
@@ -33102,27 +33435,27 @@ function ProductEditPage({ productId }) {
33102
33435
  },
33103
33436
  body: JSON.stringify({
33104
33437
  productId: Number(savedId),
33105
- taxId,
33106
- rate: cfg.rate,
33107
- refundPolicyId: cfg.refundPolicyId
33438
+ taxId: null,
33439
+ rate: null,
33440
+ refundPolicyId
33441
+ })
33442
+ });
33443
+ } else if (nullTaxRow.refundPolicyId !== refundPolicyId) {
33444
+ await fetch(`/api/product_config/${nullTaxRow.id}`, {
33445
+ method: "PUT",
33446
+ headers: {
33447
+ "Content-Type": "application/json"
33448
+ },
33449
+ body: JSON.stringify({
33450
+ refundPolicyId
33108
33451
  })
33109
33452
  });
33110
- } else {
33111
- const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33112
- const er = Number.isFinite(existingRate) ? existingRate : null;
33113
- const policyChanged = ep.refundPolicyId !== cfg.refundPolicyId;
33114
- if (ratesDiffer(er, cfg.rate) || policyChanged) {
33115
- await fetch(`/api/product_config/${ep.id}`, {
33116
- method: "PUT",
33117
- headers: {
33118
- "Content-Type": "application/json"
33119
- },
33120
- body: JSON.stringify({
33121
- rate: cfg.rate,
33122
- refundPolicyId: cfg.refundPolicyId
33123
- })
33124
- });
33125
- }
33453
+ }
33454
+ } else {
33455
+ for (const ep of existingPc) {
33456
+ await fetch(`/api/product_config/${ep.id}`, {
33457
+ method: "DELETE"
33458
+ });
33126
33459
  }
33127
33460
  }
33128
33461
  if (hasVariants) {
@@ -34038,7 +34371,8 @@ function CollectionEditPage({ collectionId }) {
34038
34371
  ]);
34039
34372
  return;
34040
34373
  }
34041
- if (!slug.trim()) {
34374
+ const normalizedSlug = normalizeSlug(slug);
34375
+ if (!normalizedSlug) {
34042
34376
  setErrors([
34043
34377
  "Slug is required"
34044
34378
  ]);
@@ -34046,10 +34380,10 @@ function CollectionEditPage({ collectionId }) {
34046
34380
  }
34047
34381
  setSaving(true);
34048
34382
  try {
34049
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
34383
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
34050
34384
  const payload = {
34051
34385
  name: name.trim(),
34052
- slug: slug.trim(),
34386
+ slug: normalizedSlug,
34053
34387
  hsn: hsn.trim() || null,
34054
34388
  categoryId: categoryId || null,
34055
34389
  brandId: brandId || null,
@@ -34218,7 +34552,9 @@ function CollectionEditPage({ collectionId }) {
34218
34552
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
34219
34553
  type: "text",
34220
34554
  value: slug,
34221
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
34555
+ placeholder: "collection-slug",
34556
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
34557
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
34222
34558
  className: inputCls4,
34223
34559
  required: true
34224
34560
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -34454,6 +34790,7 @@ var init_CollectionEditPage = __esm({
34454
34790
  init_admin_config_context();
34455
34791
  init_category_related_product_labels();
34456
34792
  init_ImageOrUrlField();
34793
+ init_slug_sanitizer();
34457
34794
  isCreate3 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
34458
34795
  emptySlide = /* @__PURE__ */ __name(() => ({
34459
34796
  url: "",
@@ -35633,7 +35970,9 @@ var init_event_entity_types = __esm({
35633
35970
  // src/admin/pages/EventEditPage.tsx
35634
35971
  var EventEditPage_exports = {};
35635
35972
  __export(EventEditPage_exports, {
35636
- default: () => EventEditPage
35973
+ default: () => EventEditPage,
35974
+ normalizeSlug: () => normalizeSlug2,
35975
+ sanitizeSlugInput: () => sanitizeSlugInput2
35637
35976
  });
35638
35977
  function RequiredLabel({ children }) {
35639
35978
  return /* @__PURE__ */ React.createElement("label", {
@@ -35642,6 +35981,12 @@ function RequiredLabel({ children }) {
35642
35981
  className: "text-red-600"
35643
35982
  }, "*"));
35644
35983
  }
35984
+ function sanitizeSlugInput2(value) {
35985
+ return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-");
35986
+ }
35987
+ function normalizeSlug2(value) {
35988
+ return sanitizeSlugInput2(value).replace(/^-+|-+$/g, "");
35989
+ }
35645
35990
  function toIsoOrNull(value, timezone) {
35646
35991
  const trimmed = value.trim();
35647
35992
  if (!trimmed) return null;
@@ -35732,7 +36077,7 @@ function EventEditPage({ eventId }) {
35732
36077
  const [supportContact, setSupportContact] = useState("");
35733
36078
  const [allowGroupOrders, setAllowGroupOrders] = useState(false);
35734
36079
  const [sendIndividualTicketPDFToAttendees, setSendIndividualTicketPDFToAttendees] = useState(false);
35735
- const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = useState("");
36080
+ const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = useState("1");
35736
36081
  const [sponsors, setSponsors] = useState([]);
35737
36082
  const [agenda, setAgenda] = useState([]);
35738
36083
  const [workshops, setWorkshops] = useState([]);
@@ -35858,9 +36203,10 @@ function EventEditPage({ eventId }) {
35858
36203
  }
35859
36204
  setOfficialWebsiteUrl(data.officialWebsiteUrl ?? "");
35860
36205
  setSupportContact(data.supportContact ?? "");
35861
- setAllowGroupOrders(data.allowGroupOrders ?? false);
36206
+ const loadedAllowGroup = data.allowGroupOrders ?? false;
36207
+ setAllowGroupOrders(loadedAllowGroup);
35862
36208
  setSendIndividualTicketPDFToAttendees(data.sendIndividualTicketPDFToAttendees ?? false);
35863
- setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : "");
36209
+ setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : loadedAllowGroup ? "10" : "1");
35864
36210
  setSponsors(parseNamedListFromApi(data.sponsors, "sponsor"));
35865
36211
  setAgenda(parseNamedListFromApi(data.agenda, "agenda"));
35866
36212
  setWorkshops(parseNamedListFromApi(data.workshops, "workshop"));
@@ -35906,8 +36252,13 @@ function EventEditPage({ eventId }) {
35906
36252
  ]);
35907
36253
  const buildPayload = /* @__PURE__ */ __name(() => {
35908
36254
  const nextErrors = [];
36255
+ const normalizedSlug = normalizeSlug2(slug);
35909
36256
  if (!name.trim()) nextErrors.push("Name is required");
35910
- if (!slug.trim()) nextErrors.push("Slug is required");
36257
+ if (!normalizedSlug) {
36258
+ nextErrors.push("Slug is required");
36259
+ } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalizedSlug)) {
36260
+ nextErrors.push("Slug must contain only lowercase letters, numbers, and hyphens without leading or trailing hyphens or slashes");
36261
+ }
35911
36262
  if (!startDate.trim()) nextErrors.push("Event Start Date & Time is required");
35912
36263
  if (!endDate.trim()) nextErrors.push("Event End Date & Time is required");
35913
36264
  if (startDate.trim() && !timezone.trim()) {
@@ -35933,6 +36284,20 @@ function EventEditPage({ eventId }) {
35933
36284
  nextErrors.push("Event End Date & Time must be after Event Start Date & Time");
35934
36285
  }
35935
36286
  }
36287
+ if (registrationOpensAt.trim() && registrationClosesAt.trim()) {
36288
+ const regOpen = new Date(toIsoOrNull(registrationOpensAt, timezone) ?? "");
36289
+ const regClose = new Date(toIsoOrNull(registrationClosesAt, timezone) ?? "");
36290
+ if (!Number.isNaN(regOpen.getTime()) && !Number.isNaN(regClose.getTime()) && regClose < regOpen) {
36291
+ nextErrors.push("Registration Closes Date & Time must be after Registration Opens Date & Time");
36292
+ }
36293
+ }
36294
+ if (registrationClosesAt.trim() && endDate.trim()) {
36295
+ const regClose = new Date(toIsoOrNull(registrationClosesAt, timezone) ?? "");
36296
+ const end = new Date(toIsoOrNull(endDate, timezone) ?? "");
36297
+ if (!Number.isNaN(regClose.getTime()) && !Number.isNaN(end.getTime()) && regClose > end) {
36298
+ nextErrors.push("Registration cannot close after Event End Date & Time");
36299
+ }
36300
+ }
35936
36301
  if (additionalVenueDetails.length > ADDITIONAL_VENUE_MAX_CHARS) {
35937
36302
  nextErrors.push(`Additional Venue Details must be ${ADDITIONAL_VENUE_MAX_CHARS} characters or fewer`);
35938
36303
  }
@@ -35942,7 +36307,7 @@ function EventEditPage({ eventId }) {
35942
36307
  }
35943
36308
  const payload = {
35944
36309
  name: name.trim(),
35945
- slug: slug.trim(),
36310
+ slug: normalizedSlug,
35946
36311
  description: description.trim() || null,
35947
36312
  isActive: create && vendorPortal && approvalOn ? false : isActive,
35948
36313
  comingSoon,
@@ -35970,7 +36335,7 @@ function EventEditPage({ eventId }) {
35970
36335
  supportContact: supportContact.trim() || null,
35971
36336
  allowGroupOrders,
35972
36337
  sendIndividualTicketPDFToAttendees,
35973
- maxGroupPurchaseQuantity: maxGroupPurchaseQuantity.trim() ? Number(maxGroupPurchaseQuantity) : null,
36338
+ maxGroupPurchaseQuantity: allowGroupOrders ? Number(maxGroupPurchaseQuantity) > 0 ? Number(maxGroupPurchaseQuantity) : 10 : 1,
35974
36339
  sponsors: namedListToPayload(sponsors, {
35975
36340
  legacySponsorKeys: true
35976
36341
  }),
@@ -36238,9 +36603,16 @@ function EventEditPage({ eventId }) {
36238
36603
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36239
36604
  className: labelCls6
36240
36605
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
36606
+ id: "event-slug",
36241
36607
  type: "text",
36242
36608
  value: slug,
36243
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
36609
+ placeholder: "event-slug",
36610
+ onChange: /* @__PURE__ */ __name((e) => {
36611
+ setSlug(sanitizeSlugInput2(e.target.value));
36612
+ }, "onChange"),
36613
+ onBlur: /* @__PURE__ */ __name(() => {
36614
+ setSlug((prev) => normalizeSlug2(prev));
36615
+ }, "onBlur"),
36244
36616
  className: inputCls6
36245
36617
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36246
36618
  className: labelCls6
@@ -36500,7 +36872,16 @@ function EventEditPage({ eventId }) {
36500
36872
  }, "Max group purchase quantity"), /* @__PURE__ */ React.createElement("input", {
36501
36873
  type: "number",
36502
36874
  value: maxGroupPurchaseQuantity,
36503
- onChange: /* @__PURE__ */ __name((e) => setMaxGroupPurchaseQuantity(e.target.value), "onChange"),
36875
+ onChange: /* @__PURE__ */ __name((e) => {
36876
+ const val = e.target.value;
36877
+ setMaxGroupPurchaseQuantity(val);
36878
+ const num = Number(val);
36879
+ if (Number.isFinite(num) && num > 1) {
36880
+ setAllowGroupOrders(true);
36881
+ } else if (Number.isFinite(num) && num <= 1) {
36882
+ setAllowGroupOrders(false);
36883
+ }
36884
+ }, "onChange"),
36504
36885
  className: inputCls6,
36505
36886
  min: 1
36506
36887
  })), /* @__PURE__ */ React.createElement("div", {
@@ -36510,7 +36891,18 @@ function EventEditPage({ eventId }) {
36510
36891
  }, /* @__PURE__ */ React.createElement("input", {
36511
36892
  type: "checkbox",
36512
36893
  checked: allowGroupOrders,
36513
- onChange: /* @__PURE__ */ __name((e) => setAllowGroupOrders(e.target.checked), "onChange"),
36894
+ onChange: /* @__PURE__ */ __name((e) => {
36895
+ const checked = e.target.checked;
36896
+ setAllowGroupOrders(checked);
36897
+ if (!checked) {
36898
+ setMaxGroupPurchaseQuantity("1");
36899
+ } else {
36900
+ const current = Number(maxGroupPurchaseQuantity);
36901
+ if (!Number.isFinite(current) || current <= 1) {
36902
+ setMaxGroupPurchaseQuantity("10");
36903
+ }
36904
+ }
36905
+ }, "onChange"),
36514
36906
  className: "h-4 w-4 rounded border-gray-300"
36515
36907
  }), /* @__PURE__ */ React.createElement("span", {
36516
36908
  className: "text-sm text-gray-900"
@@ -36584,6 +36976,8 @@ var init_EventEditPage = __esm({
36584
36976
  labelCls6 = "block text-xs font-medium text-gray-600 mb-1";
36585
36977
  inputCls6 = "w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm";
36586
36978
  __name(RequiredLabel, "RequiredLabel");
36979
+ __name(sanitizeSlugInput2, "sanitizeSlugInput");
36980
+ __name(normalizeSlug2, "normalizeSlug");
36587
36981
  ENTITY_TYPE_OPTIONS = [
36588
36982
  ...EVENT_ENTITY_TYPE_OPTIONS
36589
36983
  ];
@@ -36962,7 +37356,7 @@ function ComboEditPage({ comboId }) {
36962
37356
  const resolvedEventId = trimmedEventId && /^\d+$/.test(trimmedEventId) ? Number(trimmedEventId) : null;
36963
37357
  const payload = {
36964
37358
  name: trimmedName,
36965
- slug: slug.trim() || void 0,
37359
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
36966
37360
  desc: desc || null,
36967
37361
  eventId: resolvedEventId,
36968
37362
  price,
@@ -37057,8 +37451,9 @@ function ComboEditPage({ comboId }) {
37057
37451
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
37058
37452
  type: "text",
37059
37453
  value: slug,
37060
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37061
- placeholder: "Auto generate from name",
37454
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
37455
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
37456
+ placeholder: "combo-slug",
37062
37457
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
37063
37458
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
37064
37459
  className: "block text-xs font-medium text-gray-600 mb-1"
@@ -37224,6 +37619,7 @@ var init_ComboEditPage = __esm({
37224
37619
  init_DetailPageHeader();
37225
37620
  init_inventory_validation();
37226
37621
  init_admin_config_context();
37622
+ init_slug_sanitizer();
37227
37623
  isCreate5 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
37228
37624
  __name(formatDateTimeLocal, "formatDateTimeLocal");
37229
37625
  __name(fetchEventDefaultCurrency, "fetchEventDefaultCurrency");
@@ -37455,7 +37851,7 @@ function VendorEditPage({ vendorId }) {
37455
37851
  const vendorPayload = /* @__PURE__ */ __name(() => ({
37456
37852
  name: name.trim(),
37457
37853
  legalName: legalName.trim() || null,
37458
- slug: slug.trim() || void 0,
37854
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37459
37855
  businessType: businessType || null,
37460
37856
  description: description.trim() || null,
37461
37857
  website: website.trim() || null,
@@ -37797,7 +38193,7 @@ function VendorEditPage({ vendorId }) {
37797
38193
  signal: abortController.signal,
37798
38194
  body: JSON.stringify({
37799
38195
  ...vendorPayload(),
37800
- slug: slug.trim(),
38196
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37801
38197
  metadata: (() => {
37802
38198
  const next = {
37803
38199
  ...metadata ?? {}
@@ -37920,8 +38316,9 @@ function VendorEditPage({ vendorId }) {
37920
38316
  }, "Slug ", create ? "(optional)" : "*"), /* @__PURE__ */ React26__default.createElement(Input, {
37921
38317
  id: "vendorSlug",
37922
38318
  value: slug,
37923
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37924
- placeholder: create ? "auto from name" : void 0,
38319
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
38320
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
38321
+ placeholder: "vendor-slug",
37925
38322
  className: `mt-1 ${fieldClass}`
37926
38323
  })), /* @__PURE__ */ React26__default.createElement("div", null, /* @__PURE__ */ React26__default.createElement(FieldLabel, {
37927
38324
  htmlFor: "businessType"
@@ -38262,6 +38659,7 @@ var init_VendorEditPage = __esm({
38262
38659
  init_checkbox();
38263
38660
  init_dialog();
38264
38661
  init_vendor_profile();
38662
+ init_slug_sanitizer();
38265
38663
  init_vendor_list_config();
38266
38664
  init_vendor_access_denied();
38267
38665
  init_vendor_access_denied();