@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.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.71" ;
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
  });
@@ -24441,7 +24652,8 @@ function DetailPageHeader({ title, subtitle, backHref, backLabel = "Back", close
24441
24652
  size: "icon",
24442
24653
  className: "h-8 w-8 border border-gray-600 bg-transparent text-white hover:bg-gray-700",
24443
24654
  onClick: item.onClick,
24444
- title: item.label
24655
+ title: item.label,
24656
+ disabled: item.disabled
24445
24657
  }, /* @__PURE__ */ React.createElement(Icon2, {
24446
24658
  className: "h-4 w-4"
24447
24659
  }), /* @__PURE__ */ React.createElement("span", {
@@ -24455,7 +24667,8 @@ function DetailPageHeader({ title, subtitle, backHref, backLabel = "Back", close
24455
24667
  key: i,
24456
24668
  variant: item.variant ?? "outline",
24457
24669
  size: "sm",
24458
- onClick: item.onClick
24670
+ onClick: item.onClick,
24671
+ disabled: item.disabled
24459
24672
  }, /* @__PURE__ */ React.createElement(Icon2, {
24460
24673
  className: "h-4 w-4 mr-1"
24461
24674
  }), item.label);
@@ -26916,14 +27129,55 @@ function OrderPlacementPage({ editOrderId }) {
26916
27129
  status: "available"
26917
27130
  });
26918
27131
  if (q.length >= 2) params.set("search", q);
26919
- 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
+ ]);
26920
27137
  if (!res.ok) {
26921
27138
  setProductHits([]);
26922
27139
  return;
26923
27140
  }
26924
27141
  const body = await res.json();
26925
27142
  const hits = Array.isArray(body.data) ? body.data : [];
26926
- 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
+ });
26927
27181
  setProductHits(hits);
26928
27182
  } catch {
26929
27183
  setProductHits([]);
@@ -27200,6 +27454,7 @@ function OrderPlacementPage({ editOrderId }) {
27200
27454
  toast.message("Select one or more products in the list first");
27201
27455
  return;
27202
27456
  }
27457
+ let reachedLimit = false;
27203
27458
  setLines((prev) => {
27204
27459
  const next = [
27205
27460
  ...prev
@@ -27207,29 +27462,47 @@ function OrderPlacementPage({ editOrderId }) {
27207
27462
  for (const id of stagedIds) {
27208
27463
  const hit = productCache.current.get(id);
27209
27464
  if (!hit) continue;
27465
+ const maxAllowed = typeof hit.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27210
27466
  const idx = next.findIndex((l) => l.productId === id);
27211
27467
  if (idx >= 0) {
27212
27468
  const row = next[idx];
27213
- next[idx] = {
27214
- ...row,
27215
- quantity: row.quantity + 1
27216
- };
27217
- } else next.push({
27218
- key: String(id),
27219
- productId: id,
27220
- label: hit.name ?? `Product #${id}`,
27221
- sku: hit.sku,
27222
- quantity: 1
27223
- });
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
+ }
27224
27486
  }
27225
27487
  return next;
27226
27488
  });
27227
27489
  setStagedIds(/* @__PURE__ */ new Set());
27228
- 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
+ }
27229
27495
  }
27230
27496
  __name(addStagedToCart, "addStagedToCart");
27231
27497
  function updateQty(key, quantity) {
27232
- 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
+ }
27233
27506
  setLines((prev) => prev.map((l) => l.key === key ? {
27234
27507
  ...l,
27235
27508
  quantity: q
@@ -27279,6 +27552,13 @@ function OrderPlacementPage({ editOrderId }) {
27279
27552
  toast.error("Add at least one product to the cart");
27280
27553
  return;
27281
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
+ }
27282
27562
  const unknown = preview?.lines.filter((l) => !l.found) ?? [];
27283
27563
  if (unknown.length > 0) {
27284
27564
  toast.error(`One or more items could not be validated: ${unknown.map((u) => u.productName).join(", ")}`);
@@ -27941,6 +28221,7 @@ function OrderPlacementPage({ editOrderId }) {
27941
28221
  }, /* @__PURE__ */ React.createElement(Input, {
27942
28222
  type: "number",
27943
28223
  min: 1,
28224
+ max: productCache.current.get(line.productId)?.maxPurchaseLimit ?? void 0,
27944
28225
  className: "h-8 w-14 ml-auto text-right",
27945
28226
  value: line.quantity,
27946
28227
  onChange: /* @__PURE__ */ __name((e) => updateQty(line.key, Number(e.target.value)), "onChange"),
@@ -30541,9 +30822,10 @@ function SaveButton({ pageId, pageData, existingSeoId, onSeoIdChange, onSaved, c
30541
30822
  }
30542
30823
  }
30543
30824
  }
30825
+ const normalizedSlug = normalizeSlug(pageData.slug);
30544
30826
  const payload = {
30545
30827
  title: pageData.title,
30546
- slug: pageData.slug,
30828
+ slug: normalizedSlug,
30547
30829
  content,
30548
30830
  published: pageData.published
30549
30831
  };
@@ -30783,7 +31065,8 @@ function PageBuilderPage({ pageId }) {
30783
31065
  className: "block text-xs font-medium text-gray-600 mb-1"
30784
31066
  }, "Slug *"), /* @__PURE__ */ React26__default.createElement(Input, {
30785
31067
  value: slug,
30786
- 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"),
30787
31070
  placeholder: "page-url-slug",
30788
31071
  className: "h-8 text-sm"
30789
31072
  })))), /* @__PURE__ */ React26__default.createElement(RightSidebar, {
@@ -30820,6 +31103,7 @@ var init_PageBuilderPage = __esm({
30820
31103
  init_admin_config_context();
30821
31104
  init_registry();
30822
31105
  init_ImageOrUrlField();
31106
+ init_slug_sanitizer();
30823
31107
  __name(createSelectable, "createSelectable");
30824
31108
  __name(buildEditorResolver, "buildEditorResolver");
30825
31109
  __name(getIcon, "getIcon");
@@ -31053,7 +31337,8 @@ function BrandEditPage({ brandId }) {
31053
31337
  ]);
31054
31338
  return;
31055
31339
  }
31056
- if (!slug.trim()) {
31340
+ const normalizedSlug = normalizeSlug(slug);
31341
+ if (!normalizedSlug) {
31057
31342
  setErrors([
31058
31343
  "Slug is required"
31059
31344
  ]);
@@ -31061,10 +31346,10 @@ function BrandEditPage({ brandId }) {
31061
31346
  }
31062
31347
  setSaving(true);
31063
31348
  try {
31064
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
31349
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
31065
31350
  const payload = {
31066
31351
  name: name.trim(),
31067
- slug: slug.trim(),
31352
+ slug: normalizedSlug,
31068
31353
  description: description || null,
31069
31354
  logo: logo || null,
31070
31355
  active,
@@ -31154,7 +31439,9 @@ function BrandEditPage({ brandId }) {
31154
31439
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
31155
31440
  type: "text",
31156
31441
  value: slug,
31157
- 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"),
31158
31445
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm",
31159
31446
  required: true
31160
31447
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -31214,6 +31501,7 @@ var init_BrandEditPage = __esm({
31214
31501
  init_DetailPageHeader();
31215
31502
  init_vendor_scope();
31216
31503
  init_ImageOrUrlField();
31504
+ init_slug_sanitizer();
31217
31505
  isCreate = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
31218
31506
  __name(BrandEditPage, "BrandEditPage");
31219
31507
  }
@@ -32678,11 +32966,21 @@ function ProductEditPage({ productId }) {
32678
32966
  const pcData = await pcRes.json();
32679
32967
  const pcs = Array.isArray(pcData.data) ? pcData.data : [];
32680
32968
  if (pcs.length > 0) {
32681
- setTaxRows(pcs.map((p) => ({
32682
- taxId: p.taxId,
32683
- rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
32684
- })));
32685
- 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;
32686
32984
  setRefundPolicyId(firstRefund != null ? Number(firstRefund) : null);
32687
32985
  } else if (!cancelled) {
32688
32986
  setTaxRows([
@@ -32691,6 +32989,7 @@ function ProductEditPage({ productId }) {
32691
32989
  rate: ""
32692
32990
  }
32693
32991
  ]);
32992
+ setRefundPolicyId(null);
32694
32993
  }
32695
32994
  }
32696
32995
  const paRes = await fetch(`/api/product_attributes?productId=${sourceProductId}&limit=100`);
@@ -33067,14 +33366,7 @@ function ProductEditPage({ productId }) {
33067
33366
  if (a == null || b == null) return true;
33068
33367
  return Math.abs(a - b) > 1e-6;
33069
33368
  }, "ratesDiffer");
33070
- const wantedConfig = /* @__PURE__ */ new Map();
33071
- for (const row of taxRows) {
33072
- if (row.taxId === "") continue;
33073
- wantedConfig.set(row.taxId, {
33074
- rate: parseTaxRate(row.rate),
33075
- refundPolicyId
33076
- });
33077
- }
33369
+ const validTaxRows = taxRows.filter((row) => row.taxId !== "");
33078
33370
  console.log("[product_config rows to save]:", taxRows, "refundPolicyId:", refundPolicyId);
33079
33371
  const pcListRes = await fetch(`/api/product_config?productId=${savedId}&limit=200`);
33080
33372
  const pcListData = pcListRes.ok ? await pcListRes.json() : {
@@ -33082,17 +33374,60 @@ function ProductEditPage({ productId }) {
33082
33374
  };
33083
33375
  console.log("[product existing pc rows]:", pcListData);
33084
33376
  const existingPc = Array.isArray(pcListData.data) ? pcListData.data : [];
33085
- for (const ep of existingPc) {
33086
- if (!wantedConfig.has(ep.taxId)) {
33087
- await fetch(`/api/product_config/${ep.id}`, {
33088
- method: "DELETE"
33089
- });
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
+ }
33090
33385
  }
33091
- }
33092
- const survivors = existingPc.filter((ep) => wantedConfig.has(ep.taxId));
33093
- for (const [taxId, cfg] of wantedConfig) {
33094
- const ep = survivors.find((e) => e.taxId === taxId);
33095
- 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) {
33096
33431
  await fetch("/api/product_config", {
33097
33432
  method: "POST",
33098
33433
  headers: {
@@ -33100,27 +33435,27 @@ function ProductEditPage({ productId }) {
33100
33435
  },
33101
33436
  body: JSON.stringify({
33102
33437
  productId: Number(savedId),
33103
- taxId,
33104
- rate: cfg.rate,
33105
- refundPolicyId: cfg.refundPolicyId
33438
+ taxId: null,
33439
+ rate: null,
33440
+ refundPolicyId
33106
33441
  })
33107
33442
  });
33108
- } else {
33109
- const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33110
- const er = Number.isFinite(existingRate) ? existingRate : null;
33111
- const policyChanged = ep.refundPolicyId !== cfg.refundPolicyId;
33112
- if (ratesDiffer(er, cfg.rate) || policyChanged) {
33113
- await fetch(`/api/product_config/${ep.id}`, {
33114
- method: "PUT",
33115
- headers: {
33116
- "Content-Type": "application/json"
33117
- },
33118
- body: JSON.stringify({
33119
- rate: cfg.rate,
33120
- refundPolicyId: cfg.refundPolicyId
33121
- })
33122
- });
33123
- }
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
33451
+ })
33452
+ });
33453
+ }
33454
+ } else {
33455
+ for (const ep of existingPc) {
33456
+ await fetch(`/api/product_config/${ep.id}`, {
33457
+ method: "DELETE"
33458
+ });
33124
33459
  }
33125
33460
  }
33126
33461
  if (hasVariants) {
@@ -34036,7 +34371,8 @@ function CollectionEditPage({ collectionId }) {
34036
34371
  ]);
34037
34372
  return;
34038
34373
  }
34039
- if (!slug.trim()) {
34374
+ const normalizedSlug = normalizeSlug(slug);
34375
+ if (!normalizedSlug) {
34040
34376
  setErrors([
34041
34377
  "Slug is required"
34042
34378
  ]);
@@ -34044,10 +34380,10 @@ function CollectionEditPage({ collectionId }) {
34044
34380
  }
34045
34381
  setSaving(true);
34046
34382
  try {
34047
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
34383
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
34048
34384
  const payload = {
34049
34385
  name: name.trim(),
34050
- slug: slug.trim(),
34386
+ slug: normalizedSlug,
34051
34387
  hsn: hsn.trim() || null,
34052
34388
  categoryId: categoryId || null,
34053
34389
  brandId: brandId || null,
@@ -34216,7 +34552,9 @@ function CollectionEditPage({ collectionId }) {
34216
34552
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
34217
34553
  type: "text",
34218
34554
  value: slug,
34219
- 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"),
34220
34558
  className: inputCls4,
34221
34559
  required: true
34222
34560
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -34452,6 +34790,7 @@ var init_CollectionEditPage = __esm({
34452
34790
  init_admin_config_context();
34453
34791
  init_category_related_product_labels();
34454
34792
  init_ImageOrUrlField();
34793
+ init_slug_sanitizer();
34455
34794
  isCreate3 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
34456
34795
  emptySlide = /* @__PURE__ */ __name(() => ({
34457
34796
  url: "",
@@ -35631,7 +35970,9 @@ var init_event_entity_types = __esm({
35631
35970
  // src/admin/pages/EventEditPage.tsx
35632
35971
  var EventEditPage_exports = {};
35633
35972
  __export(EventEditPage_exports, {
35634
- default: () => EventEditPage
35973
+ default: () => EventEditPage,
35974
+ normalizeSlug: () => normalizeSlug2,
35975
+ sanitizeSlugInput: () => sanitizeSlugInput2
35635
35976
  });
35636
35977
  function RequiredLabel({ children }) {
35637
35978
  return /* @__PURE__ */ React.createElement("label", {
@@ -35640,6 +35981,12 @@ function RequiredLabel({ children }) {
35640
35981
  className: "text-red-600"
35641
35982
  }, "*"));
35642
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
+ }
35643
35990
  function toIsoOrNull(value, timezone) {
35644
35991
  const trimmed = value.trim();
35645
35992
  if (!trimmed) return null;
@@ -35730,7 +36077,7 @@ function EventEditPage({ eventId }) {
35730
36077
  const [supportContact, setSupportContact] = useState("");
35731
36078
  const [allowGroupOrders, setAllowGroupOrders] = useState(false);
35732
36079
  const [sendIndividualTicketPDFToAttendees, setSendIndividualTicketPDFToAttendees] = useState(false);
35733
- const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = useState("");
36080
+ const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = useState("1");
35734
36081
  const [sponsors, setSponsors] = useState([]);
35735
36082
  const [agenda, setAgenda] = useState([]);
35736
36083
  const [workshops, setWorkshops] = useState([]);
@@ -35856,9 +36203,10 @@ function EventEditPage({ eventId }) {
35856
36203
  }
35857
36204
  setOfficialWebsiteUrl(data.officialWebsiteUrl ?? "");
35858
36205
  setSupportContact(data.supportContact ?? "");
35859
- setAllowGroupOrders(data.allowGroupOrders ?? false);
36206
+ const loadedAllowGroup = data.allowGroupOrders ?? false;
36207
+ setAllowGroupOrders(loadedAllowGroup);
35860
36208
  setSendIndividualTicketPDFToAttendees(data.sendIndividualTicketPDFToAttendees ?? false);
35861
- setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : "");
36209
+ setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : loadedAllowGroup ? "10" : "1");
35862
36210
  setSponsors(parseNamedListFromApi(data.sponsors, "sponsor"));
35863
36211
  setAgenda(parseNamedListFromApi(data.agenda, "agenda"));
35864
36212
  setWorkshops(parseNamedListFromApi(data.workshops, "workshop"));
@@ -35904,8 +36252,13 @@ function EventEditPage({ eventId }) {
35904
36252
  ]);
35905
36253
  const buildPayload = /* @__PURE__ */ __name(() => {
35906
36254
  const nextErrors = [];
36255
+ const normalizedSlug = normalizeSlug2(slug);
35907
36256
  if (!name.trim()) nextErrors.push("Name is required");
35908
- 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
+ }
35909
36262
  if (!startDate.trim()) nextErrors.push("Event Start Date & Time is required");
35910
36263
  if (!endDate.trim()) nextErrors.push("Event End Date & Time is required");
35911
36264
  if (startDate.trim() && !timezone.trim()) {
@@ -35931,6 +36284,20 @@ function EventEditPage({ eventId }) {
35931
36284
  nextErrors.push("Event End Date & Time must be after Event Start Date & Time");
35932
36285
  }
35933
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
+ }
35934
36301
  if (additionalVenueDetails.length > ADDITIONAL_VENUE_MAX_CHARS) {
35935
36302
  nextErrors.push(`Additional Venue Details must be ${ADDITIONAL_VENUE_MAX_CHARS} characters or fewer`);
35936
36303
  }
@@ -35940,7 +36307,7 @@ function EventEditPage({ eventId }) {
35940
36307
  }
35941
36308
  const payload = {
35942
36309
  name: name.trim(),
35943
- slug: slug.trim(),
36310
+ slug: normalizedSlug,
35944
36311
  description: description.trim() || null,
35945
36312
  isActive: create && vendorPortal && approvalOn ? false : isActive,
35946
36313
  comingSoon,
@@ -35968,7 +36335,7 @@ function EventEditPage({ eventId }) {
35968
36335
  supportContact: supportContact.trim() || null,
35969
36336
  allowGroupOrders,
35970
36337
  sendIndividualTicketPDFToAttendees,
35971
- maxGroupPurchaseQuantity: maxGroupPurchaseQuantity.trim() ? Number(maxGroupPurchaseQuantity) : null,
36338
+ maxGroupPurchaseQuantity: allowGroupOrders ? Number(maxGroupPurchaseQuantity) > 0 ? Number(maxGroupPurchaseQuantity) : 10 : 1,
35972
36339
  sponsors: namedListToPayload(sponsors, {
35973
36340
  legacySponsorKeys: true
35974
36341
  }),
@@ -36236,9 +36603,16 @@ function EventEditPage({ eventId }) {
36236
36603
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36237
36604
  className: labelCls6
36238
36605
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
36606
+ id: "event-slug",
36239
36607
  type: "text",
36240
36608
  value: slug,
36241
- 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"),
36242
36616
  className: inputCls6
36243
36617
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36244
36618
  className: labelCls6
@@ -36498,7 +36872,16 @@ function EventEditPage({ eventId }) {
36498
36872
  }, "Max group purchase quantity"), /* @__PURE__ */ React.createElement("input", {
36499
36873
  type: "number",
36500
36874
  value: maxGroupPurchaseQuantity,
36501
- 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"),
36502
36885
  className: inputCls6,
36503
36886
  min: 1
36504
36887
  })), /* @__PURE__ */ React.createElement("div", {
@@ -36508,7 +36891,18 @@ function EventEditPage({ eventId }) {
36508
36891
  }, /* @__PURE__ */ React.createElement("input", {
36509
36892
  type: "checkbox",
36510
36893
  checked: allowGroupOrders,
36511
- 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"),
36512
36906
  className: "h-4 w-4 rounded border-gray-300"
36513
36907
  }), /* @__PURE__ */ React.createElement("span", {
36514
36908
  className: "text-sm text-gray-900"
@@ -36582,6 +36976,8 @@ var init_EventEditPage = __esm({
36582
36976
  labelCls6 = "block text-xs font-medium text-gray-600 mb-1";
36583
36977
  inputCls6 = "w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm";
36584
36978
  __name(RequiredLabel, "RequiredLabel");
36979
+ __name(sanitizeSlugInput2, "sanitizeSlugInput");
36980
+ __name(normalizeSlug2, "normalizeSlug");
36585
36981
  ENTITY_TYPE_OPTIONS = [
36586
36982
  ...EVENT_ENTITY_TYPE_OPTIONS
36587
36983
  ];
@@ -36960,7 +37356,7 @@ function ComboEditPage({ comboId }) {
36960
37356
  const resolvedEventId = trimmedEventId && /^\d+$/.test(trimmedEventId) ? Number(trimmedEventId) : null;
36961
37357
  const payload = {
36962
37358
  name: trimmedName,
36963
- slug: slug.trim() || void 0,
37359
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
36964
37360
  desc: desc || null,
36965
37361
  eventId: resolvedEventId,
36966
37362
  price,
@@ -37055,8 +37451,9 @@ function ComboEditPage({ comboId }) {
37055
37451
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
37056
37452
  type: "text",
37057
37453
  value: slug,
37058
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37059
- 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",
37060
37457
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
37061
37458
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
37062
37459
  className: "block text-xs font-medium text-gray-600 mb-1"
@@ -37222,6 +37619,7 @@ var init_ComboEditPage = __esm({
37222
37619
  init_DetailPageHeader();
37223
37620
  init_inventory_validation();
37224
37621
  init_admin_config_context();
37622
+ init_slug_sanitizer();
37225
37623
  isCreate5 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
37226
37624
  __name(formatDateTimeLocal, "formatDateTimeLocal");
37227
37625
  __name(fetchEventDefaultCurrency, "fetchEventDefaultCurrency");
@@ -37285,6 +37683,7 @@ function VendorEditPage({ vendorId }) {
37285
37683
  const create = isCreate6(vendorId);
37286
37684
  const [loading, setLoading] = useState(!create);
37287
37685
  const [saving, setSaving] = useState(false);
37686
+ const createInFlightRef = useRef(false);
37288
37687
  const [errors, setErrors] = useState([]);
37289
37688
  const [name, setName] = useState("");
37290
37689
  const [legalName, setLegalName] = useState("");
@@ -37452,7 +37851,7 @@ function VendorEditPage({ vendorId }) {
37452
37851
  const vendorPayload = /* @__PURE__ */ __name(() => ({
37453
37852
  name: name.trim(),
37454
37853
  legalName: legalName.trim() || null,
37455
- slug: slug.trim() || void 0,
37854
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37456
37855
  businessType: businessType || null,
37457
37856
  description: description.trim() || null,
37458
37857
  website: website.trim() || null,
@@ -37643,7 +38042,7 @@ function VendorEditPage({ vendorId }) {
37643
38042
  router.push(listReturnUrl);
37644
38043
  }, "closeInviteDialog");
37645
38044
  const handleCreate = /* @__PURE__ */ __name(async () => {
37646
- setSaving(true);
38045
+ if (createInFlightRef.current) return;
37647
38046
  setErrors([]);
37648
38047
  const createMissing = [];
37649
38048
  if (!name.trim()) createMissing.push("Store name is required");
@@ -37651,14 +38050,12 @@ function VendorEditPage({ vendorId }) {
37651
38050
  if (!ownerEmail.trim()) createMissing.push("Owner email is required");
37652
38051
  if (createMissing.length) {
37653
38052
  setErrors(createMissing);
37654
- setSaving(false);
37655
38053
  return;
37656
38054
  }
37657
38055
  if (!termsAccepted) {
37658
38056
  setErrors([
37659
38057
  "You must confirm that the vendor has accepted the terms and conditions"
37660
38058
  ]);
37661
- setSaving(false);
37662
38059
  return;
37663
38060
  }
37664
38061
  const formattedOwnerPhone = formatPhoneWithCountryCode(ownerPhoneCode, ownerPhoneNum);
@@ -37669,7 +38066,6 @@ function VendorEditPage({ vendorId }) {
37669
38066
  setErrors([
37670
38067
  taxError
37671
38068
  ]);
37672
- setSaving(false);
37673
38069
  return;
37674
38070
  }
37675
38071
  const personErr = validatePersonKyc({
@@ -37682,25 +38078,22 @@ function VendorEditPage({ vendorId }) {
37682
38078
  setErrors([
37683
38079
  personErr
37684
38080
  ]);
37685
- setSaving(false);
37686
38081
  return;
37687
38082
  }
37688
38083
  if (!ownerAddressLine1.trim()) {
37689
38084
  setErrors([
37690
38085
  "Owner address line 1 is required"
37691
38086
  ]);
37692
- setSaving(false);
37693
38087
  return;
37694
38088
  }
37695
- const abortController = new AbortController();
37696
- const timeoutId = setTimeout(() => abortController.abort(), 15e3);
38089
+ createInFlightRef.current = true;
38090
+ setSaving(true);
37697
38091
  try {
37698
38092
  const res = await fetch("/api/admin/vendors/onboard", {
37699
38093
  method: "POST",
37700
38094
  headers: {
37701
38095
  "Content-Type": "application/json"
37702
38096
  },
37703
- signal: abortController.signal,
37704
38097
  body: JSON.stringify({
37705
38098
  activation: "invite",
37706
38099
  sendOwnerEmail: false,
@@ -37721,7 +38114,6 @@ function VendorEditPage({ vendorId }) {
37721
38114
  }
37722
38115
  })
37723
38116
  });
37724
- clearTimeout(timeoutId);
37725
38117
  const data = await res.json().catch(() => ({}));
37726
38118
  if (!res.ok) {
37727
38119
  setErrors([
@@ -37747,17 +38139,11 @@ function VendorEditPage({ vendorId }) {
37747
38139
  inviteLink
37748
38140
  });
37749
38141
  } catch (err) {
37750
- if (err instanceof DOMException && err.name === "AbortError") {
37751
- setErrors([
37752
- "Request timed out. Your entered data is preserved. Please click submit again."
37753
- ]);
37754
- } else {
37755
- setErrors([
37756
- err instanceof Error && err.message || "Request failed"
37757
- ]);
37758
- }
38142
+ setErrors([
38143
+ err instanceof Error && err.message || "Request failed"
38144
+ ]);
37759
38145
  } finally {
37760
- clearTimeout(timeoutId);
38146
+ createInFlightRef.current = false;
37761
38147
  setSaving(false);
37762
38148
  }
37763
38149
  }, "handleCreate");
@@ -37807,7 +38193,7 @@ function VendorEditPage({ vendorId }) {
37807
38193
  signal: abortController.signal,
37808
38194
  body: JSON.stringify({
37809
38195
  ...vendorPayload(),
37810
- slug: slug.trim(),
38196
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37811
38197
  metadata: (() => {
37812
38198
  const next = {
37813
38199
  ...metadata ?? {}
@@ -37885,13 +38271,15 @@ function VendorEditPage({ vendorId }) {
37885
38271
  {
37886
38272
  label: saving ? "Creating\u2026" : "Create vendor",
37887
38273
  icon: Save,
37888
- onClick: handleCreate
38274
+ onClick: handleCreate,
38275
+ disabled: saving
37889
38276
  }
37890
38277
  ] : [
37891
38278
  {
37892
38279
  label: saving ? "Saving\u2026" : "Save",
37893
38280
  icon: Save,
37894
- onClick: handleSave
38281
+ onClick: handleSave,
38282
+ disabled: saving
37895
38283
  },
37896
38284
  {
37897
38285
  label: active ? "Deactivate" : "Activate",
@@ -37928,8 +38316,9 @@ function VendorEditPage({ vendorId }) {
37928
38316
  }, "Slug ", create ? "(optional)" : "*"), /* @__PURE__ */ React26__default.createElement(Input, {
37929
38317
  id: "vendorSlug",
37930
38318
  value: slug,
37931
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37932
- 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",
37933
38322
  className: `mt-1 ${fieldClass}`
37934
38323
  })), /* @__PURE__ */ React26__default.createElement("div", null, /* @__PURE__ */ React26__default.createElement(FieldLabel, {
37935
38324
  htmlFor: "businessType"
@@ -38270,6 +38659,7 @@ var init_VendorEditPage = __esm({
38270
38659
  init_checkbox();
38271
38660
  init_dialog();
38272
38661
  init_vendor_profile();
38662
+ init_slug_sanitizer();
38273
38663
  init_vendor_list_config();
38274
38664
  init_vendor_access_denied();
38275
38665
  init_vendor_access_denied();