@mohasinac/appkit 2.7.11 → 2.7.13

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.
Files changed (152) hide show
  1. package/dist/_internal/server/jobs/core/assignSpinPrize.js +8 -7
  2. package/dist/_internal/server/jobs/core/bundleStockSync.js +17 -20
  3. package/dist/_internal/server/jobs/core/countersReconcile.js +3 -2
  4. package/dist/_internal/server/jobs/core/onProductStockChange.js +18 -20
  5. package/dist/_internal/server/jobs/core/onProductWrite.js +3 -2
  6. package/dist/_internal/server/jobs/core/prizeRevealClose.js +7 -5
  7. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +11 -6
  8. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +13 -7
  9. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +9 -4
  10. package/dist/_internal/server/jobs/core/triggerEventRaffle.js +3 -2
  11. package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
  12. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +3 -6
  13. package/dist/_internal/shared/features/categories/bundle-copy.js +3 -6
  14. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +32 -32
  15. package/dist/_internal/shared/features/events/schema.d.ts +4 -4
  16. package/dist/_internal/shared/features/products/schema.d.ts +20 -20
  17. package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
  18. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  19. package/dist/constants/field-names.d.ts +824 -0
  20. package/dist/constants/field-names.js +929 -0
  21. package/dist/constants/sort.d.ts +7 -0
  22. package/dist/constants/sort.js +5 -0
  23. package/dist/constants/table-keys.d.ts +48 -0
  24. package/dist/constants/table-keys.js +46 -0
  25. package/dist/features/account/components/AddressesIndexListing.js +9 -6
  26. package/dist/features/admin/components/AdminAllEventEntriesView.js +4 -1
  27. package/dist/features/admin/components/AdminBidsView.js +4 -1
  28. package/dist/features/admin/components/AdminBlogView.js +9 -2
  29. package/dist/features/admin/components/AdminBrandsView.js +9 -2
  30. package/dist/features/admin/components/AdminBundlesView.js +0 -1
  31. package/dist/features/admin/components/AdminCarouselView.js +4 -1
  32. package/dist/features/admin/components/AdminCartsView.js +4 -1
  33. package/dist/features/admin/components/AdminCategoriesView.js +9 -2
  34. package/dist/features/admin/components/AdminContactView.js +4 -1
  35. package/dist/features/admin/components/AdminCouponsView.js +4 -1
  36. package/dist/features/admin/components/AdminFaqsView.js +5 -1
  37. package/dist/features/admin/components/AdminFeaturesView.js +5 -2
  38. package/dist/features/admin/components/AdminHistoryView.js +5 -1
  39. package/dist/features/admin/components/AdminNewsletterView.js +4 -1
  40. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  41. package/dist/features/admin/components/AdminOrdersView.js +14 -12
  42. package/dist/features/admin/components/AdminPayoutsView.js +4 -1
  43. package/dist/features/admin/components/AdminPrizeDrawsView.js +4 -1
  44. package/dist/features/admin/components/AdminProductsView.js +14 -3
  45. package/dist/features/admin/components/AdminReturnRequestsView.js +4 -1
  46. package/dist/features/admin/components/AdminReviewsView.js +4 -1
  47. package/dist/features/admin/components/AdminScammersView.js +4 -1
  48. package/dist/features/admin/components/AdminSessionsView.js +4 -1
  49. package/dist/features/admin/components/AdminStoreAddressesView.js +5 -1
  50. package/dist/features/admin/components/AdminStoresView.js +10 -3
  51. package/dist/features/admin/components/AdminSublistingCategoriesView.js +4 -1
  52. package/dist/features/admin/components/AdminSupportTicketsView.js +4 -1
  53. package/dist/features/admin/components/AdminTeamView.js +4 -1
  54. package/dist/features/admin/components/AdminUsersView.js +9 -12
  55. package/dist/features/admin/components/AdminViewCards.d.ts +12 -0
  56. package/dist/features/admin/components/AdminViewCards.js +53 -0
  57. package/dist/features/admin/components/AdminWishlistsView.js +4 -1
  58. package/dist/features/auctions/repository/bid.repository.js +1 -6
  59. package/dist/features/auctions/schemas/index.d.ts +4 -4
  60. package/dist/features/blog/actions/blog-actions.d.ts +16 -16
  61. package/dist/features/blog/components/BlogFilters.d.ts +28 -28
  62. package/dist/features/blog/components/BlogFilters.js +18 -15
  63. package/dist/features/blog/components/BlogIndexListing.js +20 -16
  64. package/dist/features/blog/schemas/index.d.ts +10 -10
  65. package/dist/features/categories/components/BundleDetailView.js +0 -1
  66. package/dist/features/categories/components/CategoriesIndexListing.js +25 -21
  67. package/dist/features/categories/components/CategoryBundlesListing.js +2 -4
  68. package/dist/features/categories/schemas/firestore.d.ts +1 -1
  69. package/dist/features/categories/schemas/index.d.ts +2 -2
  70. package/dist/features/events/components/AdminEventsView.js +5 -1
  71. package/dist/features/events/components/EventFilters.d.ts +24 -24
  72. package/dist/features/events/components/EventFilters.js +16 -13
  73. package/dist/features/events/components/EventStatusBadge.js +1 -0
  74. package/dist/features/events/components/EventsIndexListing.js +31 -26
  75. package/dist/features/events/components/EventsListPageView.js +13 -10
  76. package/dist/features/events/schemas/firestore.d.ts +1 -0
  77. package/dist/features/events/schemas/firestore.js +1 -0
  78. package/dist/features/events/schemas/index.d.ts +1 -1
  79. package/dist/features/events/types/index.d.ts +1 -1
  80. package/dist/features/faq/actions/faq-actions.d.ts +10 -10
  81. package/dist/features/faq/schemas/index.d.ts +4 -4
  82. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -3
  83. package/dist/features/orders/components/OrderFilters.d.ts +20 -20
  84. package/dist/features/orders/components/OrderFilters.js +33 -30
  85. package/dist/features/orders/repository/orders.repository.js +1 -15
  86. package/dist/features/orders/schemas/index.d.ts +2 -2
  87. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +35 -31
  88. package/dist/features/pre-orders/repository/preorders.repository.js +8 -4
  89. package/dist/features/pre-orders/schemas/index.d.ts +3 -3
  90. package/dist/features/products/components/AuctionsIndexListing.js +34 -30
  91. package/dist/features/products/components/BidHistory.d.ts +0 -1
  92. package/dist/features/products/components/BidHistory.js +1 -1
  93. package/dist/features/products/components/PrizeDrawsIndexListing.js +44 -40
  94. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  95. package/dist/features/products/components/ProductFilters.d.ts +20 -20
  96. package/dist/features/products/components/ProductFilters.js +62 -59
  97. package/dist/features/products/components/ProductsIndexListing.js +28 -24
  98. package/dist/features/products/components/ProductsIndexPageView.js +18 -10
  99. package/dist/features/products/hooks/useProducts.js +2 -0
  100. package/dist/features/products/repository/products.repository.js +2 -22
  101. package/dist/features/products/schemas/index.d.ts +28 -28
  102. package/dist/features/products/schemas/product-features.validators.d.ts +12 -12
  103. package/dist/features/products/types/index.d.ts +2 -0
  104. package/dist/features/promotions/components/CouponsIndexListing.js +57 -32
  105. package/dist/features/promotions/schemas/index.d.ts +4 -4
  106. package/dist/features/reviews/components/ReviewDetailShell.js +6 -5
  107. package/dist/features/reviews/components/ReviewFilters.d.ts +16 -16
  108. package/dist/features/reviews/components/ReviewFilters.js +10 -7
  109. package/dist/features/reviews/components/ReviewsIndexListing.js +21 -17
  110. package/dist/features/reviews/components/ReviewsList.d.ts +5 -2
  111. package/dist/features/reviews/components/ReviewsList.js +11 -5
  112. package/dist/features/reviews/schemas/firestore.d.ts +1 -0
  113. package/dist/features/reviews/schemas/index.d.ts +8 -8
  114. package/dist/features/reviews/types/index.d.ts +1 -0
  115. package/dist/features/search/schemas/index.d.ts +2 -2
  116. package/dist/features/seller/components/SellerAuctionsView.js +5 -1
  117. package/dist/features/seller/components/SellerBidsView.js +4 -1
  118. package/dist/features/seller/components/SellerCouponsView.js +4 -1
  119. package/dist/features/seller/components/SellerOffersView.js +5 -1
  120. package/dist/features/seller/components/SellerOrdersView.js +4 -1
  121. package/dist/features/seller/components/SellerPayoutsView.js +5 -1
  122. package/dist/features/seller/components/SellerPreOrdersView.js +4 -1
  123. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -1
  124. package/dist/features/seller/components/SellerProductsView.js +4 -1
  125. package/dist/features/seller/schemas/index.d.ts +12 -12
  126. package/dist/features/stores/api/route.js +10 -1
  127. package/dist/features/stores/components/StoreReviewsListing.js +1 -1
  128. package/dist/features/stores/components/StoresIndexListing.js +24 -20
  129. package/dist/features/stores/repository/stores.repository.js +8 -5
  130. package/dist/features/stores/schemas/firestore.d.ts +1 -0
  131. package/dist/features/stores/schemas/index.d.ts +4 -4
  132. package/dist/index.d.ts +8 -1
  133. package/dist/index.js +12 -1
  134. package/dist/providers/firebase-client/auth.d.ts +7 -0
  135. package/dist/providers/firebase-client/auth.js +15 -8
  136. package/dist/react/hooks/useUrlTable.js +2 -1
  137. package/dist/seed/bids-seed-data.js +45 -44
  138. package/dist/seed/categories-seed-data.js +41 -40
  139. package/dist/seed/coupons-seed-data.js +41 -40
  140. package/dist/seed/products-auctions-seed-data.js +54 -53
  141. package/dist/seed/products-preorders-seed-data.js +25 -24
  142. package/dist/seed/products-prize-draws-seed-data.js +24 -23
  143. package/dist/seed/products-standard-seed-data.js +294 -293
  144. package/dist/seed/reviews-seed-data.js +61 -60
  145. package/dist/seed/scammers-seed-data.js +3 -2
  146. package/dist/seed/stores-seed-data.js +9 -8
  147. package/dist/seed/support-tickets-seed-data.js +13 -12
  148. package/dist/seed/users-seed-data.js +28 -27
  149. package/dist/tailwind-utilities.css +1 -1
  150. package/dist/utils/sieve-builder.d.ts +41 -0
  151. package/dist/utils/sieve-builder.js +66 -0
  152. package/package.json +2 -2
@@ -5,27 +5,30 @@ import { FilterFacetSection } from "../../filters/FilterFacetSection";
5
5
  import { RangeFilter } from "../../filters/RangeFilter";
6
6
  import { SwitchFilter } from "../../filters/SwitchFilter";
7
7
  import { Div } from "../../../ui";
8
+ import { TABLE_KEYS } from "../../../constants/table-keys";
9
+ import { BLOG_FIELDS } from "../../../constants/field-names";
10
+ import { sortBy } from "../../../constants/sort";
8
11
  export const BLOG_FILTER_KEYS = {
9
- admin: ["status", "category", "isFeatured"],
10
- seller: ["status", "category", "isFeatured"],
11
- public: ["category"],
12
+ admin: [TABLE_KEYS.STATUS, TABLE_KEYS.CATEGORY, TABLE_KEYS.IS_FEATURED],
13
+ seller: [TABLE_KEYS.STATUS, TABLE_KEYS.CATEGORY, TABLE_KEYS.IS_FEATURED],
14
+ public: [TABLE_KEYS.CATEGORY],
12
15
  };
13
16
  export const BLOG_ADMIN_SORT_OPTIONS = [
14
- { value: "-createdAt", label: "Newest First" },
15
- { value: "createdAt", label: "Oldest First" },
16
- { value: "title", label: "Title A–Z" },
17
- { value: "-title", label: "Title Z–A" },
18
- { value: "-views", label: "Most Viewed" },
19
- { value: "-readTimeMinutes", label: "Longest Read" },
20
- { value: "-publishedAt", label: "Published: Newest" },
21
- { value: "publishedAt", label: "Published: Oldest" },
17
+ { value: sortBy(BLOG_FIELDS.CREATED_AT), label: "Newest First" },
18
+ { value: sortBy(BLOG_FIELDS.CREATED_AT, "ASC"), label: "Oldest First" },
19
+ { value: sortBy(BLOG_FIELDS.TITLE, "ASC"), label: "Title A–Z" },
20
+ { value: sortBy(BLOG_FIELDS.TITLE), label: "Title Z–A" },
21
+ { value: sortBy(BLOG_FIELDS.VIEWS), label: "Most Viewed" },
22
+ { value: sortBy(BLOG_FIELDS.READ_TIME_MINUTES), label: "Longest Read" },
23
+ { value: sortBy(BLOG_FIELDS.PUBLISHED_AT), label: "Published: Newest" },
24
+ { value: sortBy(BLOG_FIELDS.PUBLISHED_AT, "ASC"), label: "Published: Oldest" },
22
25
  ];
23
26
  export const BLOG_SELLER_SORT_OPTIONS = BLOG_ADMIN_SORT_OPTIONS;
24
27
  export const BLOG_PUBLIC_SORT_OPTIONS = [
25
- { value: "-publishedAt", label: "Published: Newest" },
26
- { value: "publishedAt", label: "Published: Oldest" },
27
- { value: "-views", label: "Most Viewed" },
28
- { value: "title", label: "Title A–Z" },
28
+ { value: sortBy(BLOG_FIELDS.PUBLISHED_AT), label: "Published: Newest" },
29
+ { value: sortBy(BLOG_FIELDS.PUBLISHED_AT, "ASC"), label: "Published: Oldest" },
30
+ { value: sortBy(BLOG_FIELDS.VIEWS), label: "Most Viewed" },
31
+ { value: sortBy(BLOG_FIELDS.TITLE, "ASC"), label: "Title A–Z" },
29
32
  ];
30
33
  // Backward-compatible alias.
31
34
  export const BLOG_SORT_OPTIONS = BLOG_ADMIN_SORT_OPTIONS;
@@ -8,18 +8,22 @@ import { Pagination, ListingToolbar } from "../../../ui";
8
8
  import { ROUTES } from "../../../next";
9
9
  import { BlogCard } from "./BlogListView";
10
10
  import { BlogFilters, BLOG_PUBLIC_SORT_OPTIONS } from "./BlogFilters";
11
+ import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
12
+ import { sortBy } from "../../../constants/sort";
13
+ import { BLOG_FIELDS } from "../../../constants/field-names";
11
14
  const PAGE_SIZE = 24;
12
- const FILTER_KEYS = ["category", "dateFrom", "dateTo"];
15
+ const DEFAULT_SORT = sortBy(BLOG_FIELDS.PUBLISHED_AT);
16
+ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
13
17
  export function BlogIndexListing({ initialData }) {
14
- const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "-publishedAt" } });
15
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
18
+ const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
19
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
16
20
  const [filterOpen, setFilterOpen] = useState(false);
17
- const [view, setView] = useState(table.get("view") || "grid");
21
+ const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
18
22
  const handleViewToggle = (next) => {
19
- if (next === "table")
23
+ if (next === VIEW_MODE.TABLE)
20
24
  return;
21
25
  setView(next);
22
- table.set("view", next);
26
+ table.set(TABLE_KEYS.VIEW, next);
23
27
  };
24
28
  // Pending filter state — buffered until "Apply Filters" clicked
25
29
  const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
@@ -63,27 +67,27 @@ export function BlogIndexListing({ initialData }) {
63
67
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
64
68
  }, []);
65
69
  const resetAll = useCallback(() => {
66
- const updates = { q: "", sort: "" };
70
+ const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" };
67
71
  for (const k of FILTER_KEYS)
68
72
  updates[k] = "";
69
73
  table.setMany(updates);
70
74
  setSearchInput("");
71
75
  }, [table]);
72
76
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
73
- const hasActiveState = !!table.get("q") ||
74
- table.get("sort") !== "-publishedAt" ||
77
+ const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
78
+ table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
75
79
  activeFilterCount > 0;
76
80
  const params = {
77
- q: table.get("q") || undefined,
78
- category: (table.get("category") || undefined),
79
- sort: table.get("sort") || "-publishedAt",
80
- page: table.getNumber("page", 1),
81
- perPage: table.getNumber("pageSize", PAGE_SIZE),
81
+ q: table.get(TABLE_KEYS.QUERY) || undefined,
82
+ category: (table.get(TABLE_KEYS.CATEGORY) || undefined),
83
+ sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
84
+ page: table.getNumber(TABLE_KEYS.PAGE, 1),
85
+ perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, PAGE_SIZE),
82
86
  };
83
87
  const { posts, total, totalPages, isLoading } = useBlogPosts(params, { initialData });
84
88
  const currentPage = table.getNumber("page", 1);
85
89
  const commitSearch = useCallback(() => {
86
- table.set("q", searchInput.trim());
90
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
87
91
  }, [searchInput, table]);
88
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-publishedAt", sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-video bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-5 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] })] }, i))) })) : posts.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No posts found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: posts.map((post) => (_jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: posts.map((post) => (_jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(BlogFilters, { table: pendingTable, variant: "public" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
92
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-video bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-5 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] })] }, i))) })) : posts.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No posts found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: posts.map((post) => (_jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: posts.map((post) => (_jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(BlogFilters, { table: pendingTable, variant: "public" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
89
93
  }
@@ -158,14 +158,14 @@ export declare const blogPostSchema: z.ZodObject<{
158
158
  updatedAt?: string | undefined;
159
159
  content?: string | undefined;
160
160
  tags?: string[] | undefined;
161
- publishedAt?: string | undefined;
162
161
  isFeatured?: boolean | undefined;
163
- views?: number | undefined;
162
+ publishedAt?: string | undefined;
164
163
  excerpt?: string | undefined;
165
164
  authorId?: string | undefined;
166
165
  authorName?: string | undefined;
167
- authorAvatar?: string | undefined;
168
166
  readTimeMinutes?: number | undefined;
167
+ views?: number | undefined;
168
+ authorAvatar?: string | undefined;
169
169
  metaTitle?: string | undefined;
170
170
  metaDescription?: string | undefined;
171
171
  }, {
@@ -178,8 +178,8 @@ export declare const blogPostSchema: z.ZodObject<{
178
178
  updatedAt?: string | undefined;
179
179
  content?: string | undefined;
180
180
  tags?: string[] | undefined;
181
- publishedAt?: string | undefined;
182
181
  isFeatured?: boolean | undefined;
182
+ publishedAt?: string | undefined;
183
183
  coverImage?: string | {
184
184
  type: "video" | "image" | "file";
185
185
  url: string;
@@ -188,8 +188,11 @@ export declare const blogPostSchema: z.ZodObject<{
188
188
  thumbnailUrl?: string | undefined;
189
189
  youtubeId?: string | undefined;
190
190
  } | null | undefined;
191
- views?: number | undefined;
192
191
  excerpt?: string | undefined;
192
+ authorId?: string | undefined;
193
+ authorName?: string | undefined;
194
+ readTimeMinutes?: number | undefined;
195
+ views?: number | undefined;
193
196
  contentImages?: {
194
197
  type: "video" | "image" | "file";
195
198
  url: string;
@@ -206,10 +209,7 @@ export declare const blogPostSchema: z.ZodObject<{
206
209
  thumbnailUrl?: string | undefined;
207
210
  youtubeId?: string | undefined;
208
211
  }[] | undefined;
209
- authorId?: string | undefined;
210
- authorName?: string | undefined;
211
212
  authorAvatar?: string | undefined;
212
- readTimeMinutes?: number | undefined;
213
213
  metaTitle?: string | undefined;
214
214
  metaDescription?: string | undefined;
215
215
  }>;
@@ -228,14 +228,14 @@ export declare const blogListParamsSchema: z.ZodObject<{
228
228
  q?: string | undefined;
229
229
  page?: number | undefined;
230
230
  category?: "news" | "tips" | "guides" | "updates" | "community" | undefined;
231
- featured?: boolean | undefined;
232
231
  tags?: string | undefined;
232
+ featured?: boolean | undefined;
233
233
  }, {
234
234
  sort?: string | undefined;
235
235
  perPage?: number | undefined;
236
236
  q?: string | undefined;
237
237
  page?: number | undefined;
238
238
  category?: "news" | "tips" | "guides" | "updates" | "community" | undefined;
239
- featured?: boolean | undefined;
240
239
  tags?: string | undefined;
240
+ featured?: boolean | undefined;
241
241
  }>;
@@ -7,7 +7,6 @@ import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/fe
7
7
  import { BundleBuyNowCta } from "./BundleBuyNowCta";
8
8
  const STOCK_BADGE_TEXT = {
9
9
  in_stock: BUNDLE_COPY.stockBadge.in_stock,
10
- partial: BUNDLE_COPY.stockBadge.partial,
11
10
  out_of_stock: BUNDLE_COPY.stockBadge.out_of_stock,
12
11
  };
13
12
  const PLACEHOLDER_EMOJI = "📦";
@@ -8,26 +8,30 @@ import { Pagination, ListingToolbar } from "../../../ui";
8
8
  import { CategoryCard } from "./CategoryGrid";
9
9
  import { CategoryFilters } from "./CategoryFilters";
10
10
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
11
+ import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
12
+ import { sortBy } from "../../../constants/sort";
13
+ import { CATEGORY_FIELDS } from "../../../constants/field-names";
11
14
  const PAGE_SIZE = 24;
12
- const FILTER_KEYS = ["isFeatured", "isBrand", "rootOnly", "tier", "minItemCount", "maxItemCount"];
15
+ const DEFAULT_SORT = sortBy(CATEGORY_FIELDS.NAME, "ASC");
16
+ const FILTER_KEYS = [TABLE_KEYS.IS_FEATURED, "isBrand", "rootOnly", "tier", "minItemCount", "maxItemCount"];
13
17
  const SORT_OPTIONS = [
14
- { value: "name", label: "Name A–Z" },
15
- { value: "-name", label: "Name Z–A" },
18
+ { value: sortBy(CATEGORY_FIELDS.NAME, "ASC"), label: "Name A–Z" },
19
+ { value: sortBy(CATEGORY_FIELDS.NAME), label: "Name Z–A" },
16
20
  { value: "-productCount", label: "Most Products" },
17
21
  ];
18
22
  export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
19
- const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "name" } });
20
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
23
+ const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
24
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
21
25
  const [filterOpen, setFilterOpen] = useState(false);
22
- const [view, setView] = useState(table.get("view") || "grid");
26
+ const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
23
27
  const handleViewToggle = (next) => {
24
- if (next === "table")
28
+ if (next === VIEW_MODE.TABLE)
25
29
  return;
26
30
  setView(next);
27
- table.set("view", next);
31
+ table.set(TABLE_KEYS.VIEW, next);
28
32
  };
29
- const sort = table.get("sort") || "name";
30
- const page = table.getNumber("page", 1);
33
+ const sort = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
34
+ const page = table.getNumber(TABLE_KEYS.PAGE, 1);
31
35
  // Pending filter state — buffered until "Apply Filters" clicked
32
36
  const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
33
37
  const pendingTable = useMemo(() => ({
@@ -70,35 +74,35 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
70
74
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
71
75
  }, []);
72
76
  const resetAll = useCallback(() => {
73
- const updates = { q: "", sort: "" };
77
+ const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" };
74
78
  for (const k of FILTER_KEYS)
75
79
  updates[k] = "";
76
80
  table.setMany(updates);
77
81
  setSearchInput("");
78
82
  }, [table]);
79
83
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
80
- const hasActiveState = !!table.get("q") ||
81
- table.get("sort") !== "name" ||
84
+ const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
85
+ table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
82
86
  activeFilterCount > 0;
83
87
  const commitSearch = useCallback(() => {
84
- table.set("q", searchInput.trim());
88
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
85
89
  }, [searchInput, table]);
86
90
  const clearSearch = () => {
87
91
  setSearchInput("");
88
- table.set("q", "");
92
+ table.set(TABLE_KEYS.QUERY, "");
89
93
  };
90
94
  // Tab state — "all" | "categories" | "brands" (not used when brandsOnly=true)
91
- const activeTab = brandsOnly ? "brands" : (table.get("tab") || "all");
95
+ const activeTab = brandsOnly ? "brands" : (table.get(TABLE_KEYS.TAB) || "all");
92
96
  const isBrandParam = brandsOnly ? true :
93
97
  activeTab === "brands" ? true :
94
98
  activeTab === "categories" ? false :
95
99
  undefined;
96
100
  const switchTab = (key) => {
97
- table.set("tab", key);
101
+ table.set(TABLE_KEYS.TAB, key);
98
102
  };
99
103
  const { categories, total, totalPages, isLoading } = useCategoriesFiltered({
100
- q: table.get("q") || undefined,
101
- isFeatured: table.get("isFeatured") === "true" || undefined,
104
+ q: table.get(TABLE_KEYS.QUERY) || undefined,
105
+ isFeatured: table.get(TABLE_KEYS.IS_FEATURED) === "true" || undefined,
102
106
  isBrand: isBrandParam,
103
107
  rootOnly: table.get("rootOnly") === "true" || undefined,
104
108
  tier: table.get("tier") ? Number(table.get("tier")) : undefined,
@@ -108,7 +112,7 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
108
112
  page,
109
113
  pageSize: PAGE_SIZE,
110
114
  });
111
- const activeSearch = table.get("q") || "";
115
+ const activeSearch = table.get(TABLE_KEYS.QUERY) || "";
112
116
  const TABS = [
113
117
  { key: "all", label: "All" },
114
118
  { key: "categories", label: "Categories" },
@@ -119,7 +123,7 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
119
123
  activeTab === tab.key
120
124
  ? "border-primary text-primary dark:text-primary-400 dark:border-primary-400"
121
125
  : "border-transparent text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200",
122
- ].join(" "), children: tab.label }, tab.key))) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-[4/3] bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3.5 space-y-2", children: [_jsx("div", { className: "h-3.5 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" })] })] }, i))) })) : categories.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: activeSearch
126
+ ].join(" "), children: tab.label }, tab.key))) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-[4/3] bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3.5 space-y-2", children: [_jsx("div", { className: "h-3.5 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" })] })] }, i))) })) : categories.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: activeSearch
123
127
  ? `No ${activeTab === "brands" || brandsOnly ? "brands" : "categories"} matching "${activeSearch}"`
124
128
  : activeTab === "brands" || brandsOnly
125
129
  ? "No brands found"
@@ -19,13 +19,11 @@ const SORT_OPTIONS = [
19
19
  ];
20
20
  const STOCK_BADGE_TEXT = {
21
21
  in_stock: "",
22
- partial: "Partial",
23
- out_of_stock: "Out of stock",
22
+ out_of_stock: "Not active",
24
23
  };
25
24
  const STOCK_BADGE_VARIANT = {
26
25
  in_stock: "success",
27
- partial: "warning",
28
- out_of_stock: "danger",
26
+ out_of_stock: "warning",
29
27
  };
30
28
  const PLACEHOLDER_EMOJI = "📦";
31
29
  const COPY = {
@@ -105,7 +105,7 @@ export interface CategoryDocument {
105
105
  /** Rule resolving the bundle's member products — static list or live query. */
106
106
  bundleQueryRule?: BundleQueryRule;
107
107
  /** Snapshot stock state — recomputed by onProductStockChange. */
108
- bundleStockStatus?: "in_stock" | "partial" | "out_of_stock";
108
+ bundleStockStatus?: "in_stock" | "out_of_stock";
109
109
  /** Timestamp of the last dynamic-rule resolution. */
110
110
  bundleQueryResolvedAt?: Date;
111
111
  /** Hand-picked products list (mirror of bundleQueryRule for static rules); kept for index-friendly queries. */
@@ -128,6 +128,7 @@ export declare const categoryItemSchema: z.ZodObject<{
128
128
  coverImage?: string | undefined;
129
129
  } | undefined;
130
130
  order?: number | undefined;
131
+ isFeatured?: boolean | undefined;
131
132
  description?: string | undefined;
132
133
  seo?: {
133
134
  title?: string | undefined;
@@ -138,7 +139,6 @@ export declare const categoryItemSchema: z.ZodObject<{
138
139
  rootId?: string | undefined;
139
140
  parentIds?: string[] | undefined;
140
141
  isLeaf?: boolean | undefined;
141
- isFeatured?: boolean | undefined;
142
142
  featuredPriority?: number | undefined;
143
143
  childrenIds?: string[] | undefined;
144
144
  metrics?: {
@@ -161,6 +161,7 @@ export declare const categoryItemSchema: z.ZodObject<{
161
161
  coverImage?: string | undefined;
162
162
  } | undefined;
163
163
  order?: number | undefined;
164
+ isFeatured?: boolean | undefined;
164
165
  description?: string | undefined;
165
166
  seo?: {
166
167
  title?: string | undefined;
@@ -171,7 +172,6 @@ export declare const categoryItemSchema: z.ZodObject<{
171
172
  rootId?: string | undefined;
172
173
  parentIds?: string[] | undefined;
173
174
  isLeaf?: boolean | undefined;
174
- isFeatured?: boolean | undefined;
175
175
  featuredPriority?: number | undefined;
176
176
  childrenIds?: string[] | undefined;
177
177
  metrics?: {
@@ -3,6 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import React, { useState, useCallback } from "react";
4
4
  import { Plus, X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
+ import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
+ import { AdminViewCards } from "../../admin/components/AdminViewCards";
6
8
  import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
7
9
  import { Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, SideDrawer } from "../../../ui";
8
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
@@ -34,6 +36,7 @@ const TYPE_OPTIONS = [
34
36
  ];
35
37
  export function AdminEventsView({ children, getRowHref, ...props }) {
36
38
  const hasChildren = React.Children.count(children) > 0;
39
+ const [view, setView] = useState("table");
37
40
  const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
38
41
  const { openCreatePanel, openEditPanel, closePanel, isCreateOpen, isEditOpen, editId } = usePanelUrlSync();
39
42
  const [searchInput, setSearchInput] = useState(table.get("q") || "");
@@ -95,8 +98,9 @@ export function AdminEventsView({ children, getRowHref, ...props }) {
95
98
  });
96
99
  const currentPage = table.getNumber("page", 1);
97
100
  const totalPages = Math.ceil(total / PAGE_SIZE);
101
+ const selection = useBulkSelection({ items: rows, keyExtractor: (r) => r.id });
98
102
  if (hasChildren) {
99
103
  return _jsx(ListingViewShell, { portal: "admin", ...props, children: children });
100
104
  }
101
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events by title or type", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, hideViewToggle: true, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { size: "sm", onClick: openCreatePanel, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Event"] }) }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No events found", onRowClick: (row) => openEditPanel(row.id) })] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: TYPE_OPTIONS, value: pendingFilters.type ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, type: id })) })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] })), _jsx(SideDrawer, { isOpen: isCreateOpen || isEditOpen, onClose: closePanel, title: isCreateOpen ? "Add Event" : "Edit Event", mode: isCreateOpen ? "create" : "edit", children: (isCreateOpen || isEditOpen) && (_jsx(AdminEventEditorView, { eventId: editId ?? undefined, onSaved: closePanel, embedded: true })) })] }));
105
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events by title or type", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { size: "sm", onClick: openCreatePanel, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Event"] }) }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No events found", onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No events found", onRowClick: undefined, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: TYPE_OPTIONS, value: pendingFilters.type ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, type: id })) })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] })), _jsx(SideDrawer, { isOpen: isCreateOpen || isEditOpen, onClose: closePanel, title: isCreateOpen ? "Add Event" : "Edit Event", mode: isCreateOpen ? "create" : "edit", children: (isCreateOpen || isEditOpen) && (_jsx(AdminEventEditorView, { eventId: editId ?? undefined, onSaved: closePanel, embedded: true })) })] }));
102
106
  }
@@ -6,91 +6,91 @@ export declare const EVENT_FILTER_KEYS: {
6
6
  readonly public: readonly ["type", "status", "dateFrom", "dateTo"];
7
7
  };
8
8
  export declare const EVENT_ADMIN_SORT_OPTIONS: readonly [{
9
- readonly value: "title";
9
+ readonly value: string;
10
10
  readonly label: "Title A–Z";
11
11
  }, {
12
- readonly value: "-title";
12
+ readonly value: string;
13
13
  readonly label: "Title Z–A";
14
14
  }, {
15
- readonly value: "-startsAt";
15
+ readonly value: string;
16
16
  readonly label: "Starts Latest";
17
17
  }, {
18
- readonly value: "startsAt";
18
+ readonly value: string;
19
19
  readonly label: "Starts Soonest";
20
20
  }, {
21
- readonly value: "-endsAt";
21
+ readonly value: string;
22
22
  readonly label: "Ends Latest";
23
23
  }, {
24
- readonly value: "endsAt";
24
+ readonly value: string;
25
25
  readonly label: "Ends Soonest";
26
26
  }, {
27
27
  readonly value: "-stats.totalEntries";
28
28
  readonly label: "Most Entries";
29
29
  }, {
30
- readonly value: "-createdAt";
30
+ readonly value: string;
31
31
  readonly label: "Newest First";
32
32
  }];
33
33
  export declare const EVENT_SELLER_SORT_OPTIONS: readonly [{
34
- readonly value: "title";
34
+ readonly value: string;
35
35
  readonly label: "Title A–Z";
36
36
  }, {
37
- readonly value: "-title";
37
+ readonly value: string;
38
38
  readonly label: "Title Z–A";
39
39
  }, {
40
- readonly value: "-startsAt";
40
+ readonly value: string;
41
41
  readonly label: "Starts Latest";
42
42
  }, {
43
- readonly value: "startsAt";
43
+ readonly value: string;
44
44
  readonly label: "Starts Soonest";
45
45
  }, {
46
- readonly value: "-endsAt";
46
+ readonly value: string;
47
47
  readonly label: "Ends Latest";
48
48
  }, {
49
- readonly value: "endsAt";
49
+ readonly value: string;
50
50
  readonly label: "Ends Soonest";
51
51
  }, {
52
52
  readonly value: "-stats.totalEntries";
53
53
  readonly label: "Most Entries";
54
54
  }, {
55
- readonly value: "-createdAt";
55
+ readonly value: string;
56
56
  readonly label: "Newest First";
57
57
  }];
58
58
  export declare const EVENT_PUBLIC_SORT_OPTIONS: readonly [{
59
- readonly value: "startsAt";
59
+ readonly value: string;
60
60
  readonly label: "Starts Soonest";
61
61
  }, {
62
- readonly value: "-startsAt";
62
+ readonly value: string;
63
63
  readonly label: "Starts Latest";
64
64
  }, {
65
- readonly value: "title";
65
+ readonly value: string;
66
66
  readonly label: "Title A–Z";
67
67
  }, {
68
68
  readonly value: "-stats.totalEntries";
69
69
  readonly label: "Most Entries";
70
70
  }];
71
71
  export declare const EVENT_SORT_OPTIONS: readonly [{
72
- readonly value: "title";
72
+ readonly value: string;
73
73
  readonly label: "Title A–Z";
74
74
  }, {
75
- readonly value: "-title";
75
+ readonly value: string;
76
76
  readonly label: "Title Z–A";
77
77
  }, {
78
- readonly value: "-startsAt";
78
+ readonly value: string;
79
79
  readonly label: "Starts Latest";
80
80
  }, {
81
- readonly value: "startsAt";
81
+ readonly value: string;
82
82
  readonly label: "Starts Soonest";
83
83
  }, {
84
- readonly value: "-endsAt";
84
+ readonly value: string;
85
85
  readonly label: "Ends Latest";
86
86
  }, {
87
- readonly value: "endsAt";
87
+ readonly value: string;
88
88
  readonly label: "Ends Soonest";
89
89
  }, {
90
90
  readonly value: "-stats.totalEntries";
91
91
  readonly label: "Most Entries";
92
92
  }, {
93
- readonly value: "-createdAt";
93
+ readonly value: string;
94
94
  readonly label: "Newest First";
95
95
  }];
96
96
  export declare function getEventFilterKeys(variant: EventFilterVariant): readonly string[];
@@ -5,26 +5,29 @@ import { FilterFacetSection } from "../../filters/FilterFacetSection";
5
5
  import { RangeFilter } from "../../filters/RangeFilter";
6
6
  import { SwitchFilter } from "../../filters/SwitchFilter";
7
7
  import { Div } from "../../../ui";
8
+ import { TABLE_KEYS } from "../../../constants/table-keys";
9
+ import { EVENT_FIELDS } from "../../../constants/field-names";
10
+ import { sortBy } from "../../../constants/sort";
8
11
  export const EVENT_FILTER_KEYS = {
9
- admin: ["type", "status", "dateFrom", "dateTo"],
10
- seller: ["type", "status", "dateFrom", "dateTo"],
11
- public: ["type", "status", "dateFrom", "dateTo"],
12
+ admin: ["type", TABLE_KEYS.STATUS, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
13
+ seller: ["type", TABLE_KEYS.STATUS, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
14
+ public: ["type", TABLE_KEYS.STATUS, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
12
15
  };
13
16
  export const EVENT_ADMIN_SORT_OPTIONS = [
14
- { value: "title", label: "Title A–Z" },
15
- { value: "-title", label: "Title Z–A" },
16
- { value: "-startsAt", label: "Starts Latest" },
17
- { value: "startsAt", label: "Starts Soonest" },
18
- { value: "-endsAt", label: "Ends Latest" },
19
- { value: "endsAt", label: "Ends Soonest" },
17
+ { value: sortBy(EVENT_FIELDS.TITLE, "ASC"), label: "Title A–Z" },
18
+ { value: sortBy(EVENT_FIELDS.TITLE), label: "Title Z–A" },
19
+ { value: sortBy(EVENT_FIELDS.STARTS_AT), label: "Starts Latest" },
20
+ { value: sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), label: "Starts Soonest" },
21
+ { value: sortBy(EVENT_FIELDS.ENDS_AT), label: "Ends Latest" },
22
+ { value: sortBy(EVENT_FIELDS.ENDS_AT, "ASC"), label: "Ends Soonest" },
20
23
  { value: "-stats.totalEntries", label: "Most Entries" },
21
- { value: "-createdAt", label: "Newest First" },
24
+ { value: sortBy(EVENT_FIELDS.CREATED_AT), label: "Newest First" },
22
25
  ];
23
26
  export const EVENT_SELLER_SORT_OPTIONS = EVENT_ADMIN_SORT_OPTIONS;
24
27
  export const EVENT_PUBLIC_SORT_OPTIONS = [
25
- { value: "startsAt", label: "Starts Soonest" },
26
- { value: "-startsAt", label: "Starts Latest" },
27
- { value: "title", label: "Title A–Z" },
28
+ { value: sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), label: "Starts Soonest" },
29
+ { value: sortBy(EVENT_FIELDS.STARTS_AT), label: "Starts Latest" },
30
+ { value: sortBy(EVENT_FIELDS.TITLE, "ASC"), label: "Title A–Z" },
28
31
  { value: "-stats.totalEntries", label: "Most Entries" },
29
32
  ];
30
33
  // Backward-compatible alias.
@@ -5,6 +5,7 @@ const STATUS_MAP = {
5
5
  active: { label: "Active", color: "bg-green-100 text-green-700" },
6
6
  paused: { label: "Paused", color: "bg-yellow-100 text-yellow-700" },
7
7
  ended: { label: "Ended", color: "bg-red-100 text-red-600" },
8
+ cancelled: { label: "Cancelled", color: "bg-red-100 text-red-700" },
8
9
  };
9
10
  export function EventStatusBadge({ status, className = "", }) {
10
11
  const { label, color } = STATUS_MAP[status] ?? STATUS_MAP.draft;