@mohasinac/appkit 2.7.10 → 2.7.12

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 (148) hide show
  1. package/dist/_internal/server/jobs/core/assignSpinPrize.js +8 -7
  2. package/dist/_internal/server/jobs/core/bundleStockSync.js +11 -6
  3. package/dist/_internal/server/jobs/core/countersReconcile.js +3 -2
  4. package/dist/_internal/server/jobs/core/onProductStockChange.js +12 -7
  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-schemas.d.ts +32 -32
  13. package/dist/_internal/shared/features/events/schema.d.ts +4 -4
  14. package/dist/_internal/shared/features/products/schema.d.ts +20 -20
  15. package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
  16. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  17. package/dist/configs/next.js +17 -1
  18. package/dist/constants/field-names.d.ts +813 -0
  19. package/dist/constants/field-names.js +918 -0
  20. package/dist/constants/sort.d.ts +7 -0
  21. package/dist/constants/sort.js +5 -0
  22. package/dist/constants/table-keys.d.ts +48 -0
  23. package/dist/constants/table-keys.js +46 -0
  24. package/dist/features/account/components/AddressesIndexListing.js +9 -6
  25. package/dist/features/admin/components/AdminAllEventEntriesView.js +4 -1
  26. package/dist/features/admin/components/AdminBidsView.js +4 -1
  27. package/dist/features/admin/components/AdminBlogView.js +9 -2
  28. package/dist/features/admin/components/AdminBrandsView.js +9 -2
  29. package/dist/features/admin/components/AdminCarouselView.js +4 -1
  30. package/dist/features/admin/components/AdminCartsView.js +4 -1
  31. package/dist/features/admin/components/AdminCategoriesView.js +9 -2
  32. package/dist/features/admin/components/AdminContactView.js +4 -1
  33. package/dist/features/admin/components/AdminCouponsView.js +4 -1
  34. package/dist/features/admin/components/AdminFaqsView.js +5 -1
  35. package/dist/features/admin/components/AdminFeaturesView.js +5 -2
  36. package/dist/features/admin/components/AdminHistoryView.js +5 -1
  37. package/dist/features/admin/components/AdminNewsletterView.js +4 -1
  38. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  39. package/dist/features/admin/components/AdminOrdersView.js +14 -12
  40. package/dist/features/admin/components/AdminPayoutsView.js +4 -1
  41. package/dist/features/admin/components/AdminPrizeDrawsView.js +4 -1
  42. package/dist/features/admin/components/AdminProductsView.js +14 -3
  43. package/dist/features/admin/components/AdminReturnRequestsView.js +4 -1
  44. package/dist/features/admin/components/AdminReviewsView.js +4 -1
  45. package/dist/features/admin/components/AdminScammersView.js +4 -1
  46. package/dist/features/admin/components/AdminSessionsView.js +4 -1
  47. package/dist/features/admin/components/AdminStoreAddressesView.js +5 -1
  48. package/dist/features/admin/components/AdminStoresView.js +10 -3
  49. package/dist/features/admin/components/AdminSublistingCategoriesView.js +4 -1
  50. package/dist/features/admin/components/AdminSupportTicketsView.js +4 -1
  51. package/dist/features/admin/components/AdminTeamView.js +4 -1
  52. package/dist/features/admin/components/AdminUsersView.js +9 -12
  53. package/dist/features/admin/components/AdminViewCards.d.ts +12 -0
  54. package/dist/features/admin/components/AdminViewCards.js +53 -0
  55. package/dist/features/admin/components/AdminWishlistsView.js +4 -1
  56. package/dist/features/auctions/repository/bid.repository.js +1 -6
  57. package/dist/features/auctions/schemas/index.d.ts +4 -4
  58. package/dist/features/blog/actions/blog-actions.d.ts +16 -16
  59. package/dist/features/blog/components/BlogFilters.d.ts +28 -28
  60. package/dist/features/blog/components/BlogFilters.js +18 -15
  61. package/dist/features/blog/components/BlogIndexListing.js +24 -13
  62. package/dist/features/blog/schemas/index.d.ts +10 -10
  63. package/dist/features/categories/components/CategoriesIndexListing.js +30 -19
  64. package/dist/features/categories/schemas/index.d.ts +2 -2
  65. package/dist/features/events/components/AdminEventsView.js +5 -1
  66. package/dist/features/events/components/EventFilters.d.ts +24 -24
  67. package/dist/features/events/components/EventFilters.js +16 -13
  68. package/dist/features/events/components/EventStatusBadge.js +1 -0
  69. package/dist/features/events/components/EventsIndexListing.js +36 -24
  70. package/dist/features/events/components/EventsListPageView.js +13 -10
  71. package/dist/features/events/schemas/firestore.d.ts +1 -0
  72. package/dist/features/events/schemas/firestore.js +1 -0
  73. package/dist/features/events/schemas/index.d.ts +1 -1
  74. package/dist/features/events/types/index.d.ts +1 -1
  75. package/dist/features/faq/actions/faq-actions.d.ts +10 -10
  76. package/dist/features/faq/schemas/index.d.ts +4 -4
  77. package/dist/features/layout/TitleBarLayout.js +1 -1
  78. package/dist/features/orders/components/OrderFilters.d.ts +20 -20
  79. package/dist/features/orders/components/OrderFilters.js +33 -30
  80. package/dist/features/orders/repository/orders.repository.js +1 -15
  81. package/dist/features/orders/schemas/index.d.ts +2 -2
  82. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +44 -36
  83. package/dist/features/pre-orders/schemas/index.d.ts +3 -3
  84. package/dist/features/products/components/AuctionsIndexListing.js +63 -30
  85. package/dist/features/products/components/BidHistory.d.ts +0 -1
  86. package/dist/features/products/components/BidHistory.js +1 -1
  87. package/dist/features/products/components/PrizeDrawsIndexListing.js +44 -37
  88. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  89. package/dist/features/products/components/ProductFilters.d.ts +20 -20
  90. package/dist/features/products/components/ProductFilters.js +62 -59
  91. package/dist/features/products/components/ProductsIndexListing.js +37 -29
  92. package/dist/features/products/components/ProductsIndexPageView.js +18 -10
  93. package/dist/features/products/hooks/useProducts.js +2 -0
  94. package/dist/features/products/repository/products.repository.js +2 -22
  95. package/dist/features/products/schemas/index.d.ts +28 -28
  96. package/dist/features/products/schemas/product-features.validators.d.ts +12 -12
  97. package/dist/features/products/types/index.d.ts +2 -0
  98. package/dist/features/promotions/components/CouponsIndexListing.js +21 -17
  99. package/dist/features/promotions/schemas/index.d.ts +4 -4
  100. package/dist/features/reviews/components/ReviewDetailShell.js +6 -5
  101. package/dist/features/reviews/components/ReviewFilters.d.ts +16 -16
  102. package/dist/features/reviews/components/ReviewFilters.js +10 -7
  103. package/dist/features/reviews/components/ReviewsIndexListing.js +25 -14
  104. package/dist/features/reviews/components/ReviewsList.d.ts +5 -2
  105. package/dist/features/reviews/components/ReviewsList.js +11 -5
  106. package/dist/features/reviews/schemas/firestore.d.ts +1 -0
  107. package/dist/features/reviews/schemas/index.d.ts +8 -8
  108. package/dist/features/reviews/types/index.d.ts +1 -0
  109. package/dist/features/search/schemas/index.d.ts +2 -2
  110. package/dist/features/seller/components/SellerAuctionsView.js +5 -1
  111. package/dist/features/seller/components/SellerBidsView.js +4 -1
  112. package/dist/features/seller/components/SellerCouponsView.js +4 -1
  113. package/dist/features/seller/components/SellerOffersView.js +5 -1
  114. package/dist/features/seller/components/SellerOrdersView.js +4 -1
  115. package/dist/features/seller/components/SellerPayoutsView.js +5 -1
  116. package/dist/features/seller/components/SellerPreOrdersView.js +4 -1
  117. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -1
  118. package/dist/features/seller/components/SellerProductsView.js +4 -1
  119. package/dist/features/seller/schemas/index.d.ts +10 -10
  120. package/dist/features/stores/api/route.js +10 -1
  121. package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
  122. package/dist/features/stores/components/StorePreOrdersListing.js +2 -1
  123. package/dist/features/stores/components/StoreReviewsListing.js +1 -1
  124. package/dist/features/stores/components/StoresIndexListing.js +45 -27
  125. package/dist/features/stores/schemas/firestore.d.ts +1 -0
  126. package/dist/features/stores/schemas/index.d.ts +4 -4
  127. package/dist/index.d.ts +8 -1
  128. package/dist/index.js +12 -1
  129. package/dist/react/hooks/useUrlTable.js +2 -1
  130. package/dist/seed/bids-seed-data.js +45 -44
  131. package/dist/seed/categories-seed-data.js +41 -40
  132. package/dist/seed/coupons-seed-data.js +41 -40
  133. package/dist/seed/products-auctions-seed-data.js +54 -53
  134. package/dist/seed/products-preorders-seed-data.js +25 -24
  135. package/dist/seed/products-prize-draws-seed-data.js +24 -23
  136. package/dist/seed/products-standard-seed-data.js +294 -293
  137. package/dist/seed/reviews-seed-data.js +61 -60
  138. package/dist/seed/scammers-seed-data.js +3 -2
  139. package/dist/seed/stores-seed-data.js +9 -8
  140. package/dist/seed/support-tickets-seed-data.js +13 -12
  141. package/dist/seed/users-seed-data.js +28 -27
  142. package/dist/tailwind-utilities.css +1 -1
  143. package/dist/ui/components/ListingToolbar.d.ts +7 -4
  144. package/dist/ui/components/ListingToolbar.js +4 -3
  145. package/dist/ui/components/SiteLogo.js +1 -1
  146. package/dist/utils/sieve-builder.d.ts +41 -0
  147. package/dist/utils/sieve-builder.js +66 -0
  148. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { useState, useCallback, useMemo } from "react";
4
4
  import { Columns, Heart, ShoppingCart, SlidersHorizontal, X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useProducts } from "../../products/hooks/useProducts";
7
- import { Pagination, useToast, BulkActionsBar, ListingToolbar } from "../../../ui";
7
+ import { Pagination, useToast, BulkActionBar, ListingToolbar } from "../../../ui";
8
8
  import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../../products/constants/action-defs";
9
9
  import { CompareOverlay } from "../../products/components/CompareOverlay";
10
10
  import { ROUTES } from "../../../next";
@@ -16,23 +16,27 @@ import { pushCartOp, pushWishlistOp } from "../../cart/utils/pending-ops";
16
16
  import { useCategoryTree, categoriesToFacetOptions } from "../../categories/hooks/useCategoryTree";
17
17
  import { useBrands } from "../../products/hooks/useBrands";
18
18
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
19
+ import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
20
+ import { sortBy } from "../../../constants/sort";
21
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
22
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
19
23
  const PREORDER_SORT_OPTIONS = [
20
- { value: "-createdAt", label: "Newest First" },
21
- { value: "createdAt", label: "Oldest First" },
22
- { value: "preOrderDeliveryDate", label: "Delivery: Soonest" },
23
- { value: "-preOrderDeliveryDate", label: "Delivery: Furthest" },
24
- { value: "price", label: "Price: Low to High" },
25
- { value: "-price", label: "Price: High to Low" },
24
+ { value: sortBy(PRODUCT_FIELDS.CREATED_AT), label: "Newest First" },
25
+ { value: sortBy(PRODUCT_FIELDS.CREATED_AT, "ASC"), label: "Oldest First" },
26
+ { value: sortBy(PRODUCT_FIELDS.PRE_ORDER_DELIVERY_DATE, "ASC"), label: "Delivery: Soonest" },
27
+ { value: sortBy(PRODUCT_FIELDS.PRE_ORDER_DELIVERY_DATE), label: "Delivery: Furthest" },
28
+ { value: sortBy(PRODUCT_FIELDS.PRICE, "ASC"), label: "Price: Low to High" },
29
+ { value: sortBy(PRODUCT_FIELDS.PRICE), label: "Price: High to Low" },
26
30
  ];
27
- const FILTER_KEYS = ["category", "brand", "minPrice", "maxPrice", "storeId", "preOrderProductionStatus", "dateFrom", "dateTo"];
31
+ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_PRICE, TABLE_KEYS.MAX_PRICE, TABLE_KEYS.STORE_ID, TABLE_KEYS.PREORDER_STATUS, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
28
32
  export function PreOrdersIndexListing({ initialData, categorySlug, brandName }) {
29
- const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
33
+ const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
30
34
  const { showToast } = useToast();
31
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
35
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
32
36
  const [filterOpen, setFilterOpen] = useState(false);
33
37
  const [compareIds, setCompareIds] = useState([]);
34
- const showClosed = table.get("showClosed") === "true";
35
- const [view, setView] = useState(table.get("view") || "grid");
38
+ const showClosed = table.get(TABLE_KEYS.SHOW_CLOSED) === "true";
39
+ const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
36
40
  const localCart = useGuestCart();
37
41
  const localWishlist = useGuestWishlist();
38
42
  const { categories } = useCategoryTree();
@@ -81,45 +85,49 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
81
85
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
82
86
  }, []);
83
87
  const resetAll = useCallback(() => {
84
- const updates = { q: "", sort: "", showClosed: "" };
88
+ const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_CLOSED]: "" };
85
89
  for (const k of FILTER_KEYS)
86
90
  updates[k] = "";
87
91
  table.setMany(updates);
88
92
  setSearchInput("");
89
93
  }, [table]);
90
94
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
91
- const hasActiveState = !!table.get("q") ||
92
- table.get("showClosed") === "true" ||
93
- table.get("sort") !== "-createdAt" ||
95
+ const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
96
+ table.get(TABLE_KEYS.SHOW_CLOSED) === "true" ||
97
+ table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
94
98
  activeFilterCount > 0;
95
99
  const params = {
96
- q: table.get("q") || undefined,
97
- category: table.get("category") || undefined,
100
+ q: table.get(TABLE_KEYS.QUERY) || undefined,
101
+ category: table.get(TABLE_KEYS.CATEGORY) || undefined,
98
102
  categorySlug: categorySlug || undefined,
99
- brand: brandName || table.get("brand") || undefined,
100
- minPrice: table.get("minPrice") ? Number(table.get("minPrice")) : undefined,
101
- maxPrice: table.get("maxPrice") ? Number(table.get("maxPrice")) : undefined,
102
- storeId: table.get("storeId") || undefined,
103
- preOrderProductionStatus: (table.get("preOrderProductionStatus") || undefined),
104
- dateFrom: table.get("dateFrom") || undefined,
105
- dateTo: table.get("dateTo") || undefined,
106
- sort: table.get("sort") || "-createdAt",
107
- page: table.getNumber("page", 1),
108
- perPage: table.getNumber("pageSize", 24),
103
+ brand: brandName || table.get(TABLE_KEYS.BRAND) || undefined,
104
+ minPrice: table.get(TABLE_KEYS.MIN_PRICE) ? Number(table.get(TABLE_KEYS.MIN_PRICE)) : undefined,
105
+ maxPrice: table.get(TABLE_KEYS.MAX_PRICE) ? Number(table.get(TABLE_KEYS.MAX_PRICE)) : undefined,
106
+ storeId: table.get(TABLE_KEYS.STORE_ID) || undefined,
107
+ preOrderProductionStatus: (table.get(TABLE_KEYS.PREORDER_STATUS) || undefined),
108
+ dateFrom: table.get(TABLE_KEYS.DATE_FROM) || undefined,
109
+ dateTo: table.get(TABLE_KEYS.DATE_TO) || undefined,
110
+ sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
111
+ page: table.getNumber(TABLE_KEYS.PAGE, 1),
112
+ perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, 24),
109
113
  listingType: "pre-order",
110
- status: showClosed ? undefined : "published",
114
+ // Hide out-of-stock pre-orders by default. Uses stockQuantity>0 (always-present field).
115
+ // Quota over-sign is intentional — never block pre-orders by availability.
116
+ inStock: showClosed ? undefined : true,
111
117
  };
112
118
  const { products: preOrders, totalPages, page, isLoading } = useProducts(params, { initialData });
113
119
  const commitSearch = useCallback(() => {
114
- table.set("q", searchInput.trim());
120
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
115
121
  }, [searchInput, table]);
116
122
  const handleSearchKeyDown = (e) => {
117
123
  if (e.key === "Enter")
118
124
  commitSearch();
119
125
  };
120
126
  const handleViewToggle = (next) => {
127
+ if (next === VIEW_MODE.TABLE)
128
+ return;
121
129
  setView(next);
122
- table.set("view", next);
130
+ table.set(TABLE_KEYS.VIEW, next);
123
131
  };
124
132
  const wishlistActions = {
125
133
  addToWishlist: (productId) => {
@@ -147,9 +155,9 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
147
155
  }, [localCart, showToast]);
148
156
  const selection = useBulkSelection({ items: preOrders, keyExtractor: (p) => p.id });
149
157
  const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4";
150
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PREORDER_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: preOrders.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set("showClosed", showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), 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: gridClass, 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-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No pre-orders 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: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx("div", { className: gridClass, children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(BulkActionsBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
158
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PREORDER_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: preOrders.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set(TABLE_KEYS.SHOW_CLOSED, showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
151
159
  {
152
- key: ACTION_ID.ADD_TO_CART,
160
+ id: ACTION_ID.ADD_TO_CART,
153
161
  label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
154
162
  icon: _jsx(ShoppingCart, { className: "h-3.5 w-3.5" }),
155
163
  variant: "primary",
@@ -164,7 +172,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
164
172
  },
165
173
  },
166
174
  {
167
- key: ACTION_ID.ADD_TO_WISHLIST,
175
+ id: ACTION_ID.ADD_TO_WISHLIST,
168
176
  label: ACTION_META[ACTION_ID.ADD_TO_WISHLIST].label,
169
177
  icon: _jsx(Heart, { className: "h-3.5 w-3.5" }),
170
178
  variant: "secondary",
@@ -179,7 +187,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
179
187
  },
180
188
  },
181
189
  {
182
- key: ACTION_ID.COMPARE,
190
+ id: ACTION_ID.COMPARE,
183
191
  label: ACTION_META[ACTION_ID.COMPARE].label,
184
192
  icon: _jsx(Columns, { className: "h-3.5 w-3.5" }),
185
193
  variant: "secondary",
@@ -189,7 +197,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
189
197
  setCompareIds(ids);
190
198
  },
191
199
  },
192
- ] }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
200
+ ] }), 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: gridClass, 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-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No pre-orders 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: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx("div", { className: gridClass, children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
193
201
  setCompareIds([]);
194
202
  selection.clearSelection();
195
203
  }, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== 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: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "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(PreOrderFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _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})` : ""] }) })] })] }))] }));
@@ -48,8 +48,8 @@ export declare const preorderItemSchema: z.ZodObject<{
48
48
  id: string;
49
49
  name: string;
50
50
  active: boolean;
51
- slug: string;
52
51
  isFeatured: boolean;
52
+ slug: string;
53
53
  salePrice: number;
54
54
  regularPrice: number;
55
55
  createdAt?: string | undefined;
@@ -61,8 +61,8 @@ export declare const preorderItemSchema: z.ZodObject<{
61
61
  thumbnailUrl?: string | undefined;
62
62
  youtubeId?: string | undefined;
63
63
  }[] | undefined;
64
- images?: string[] | undefined;
65
64
  brand?: string | undefined;
65
+ images?: string[] | undefined;
66
66
  franchise?: string | undefined;
67
67
  preorderShipDate?: string | undefined;
68
68
  }, {
@@ -81,9 +81,9 @@ export declare const preorderItemSchema: z.ZodObject<{
81
81
  youtubeId?: string | undefined;
82
82
  }[] | undefined;
83
83
  active?: boolean | undefined;
84
- images?: string[] | undefined;
85
84
  brand?: string | undefined;
86
85
  isFeatured?: boolean | undefined;
86
+ images?: string[] | undefined;
87
87
  franchise?: string | undefined;
88
88
  preorderShipDate?: string | undefined;
89
89
  }>;
@@ -4,29 +4,34 @@ import { useState, useCallback, useMemo } from "react";
4
4
  import { SlidersHorizontal, X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useProducts } from "../hooks/useProducts";
7
- import { Pagination, useToast, ListingToolbar } from "../../../ui";
7
+ import { Pagination, useToast, ListingToolbar, BulkActionBar } from "../../../ui";
8
+ import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
8
9
  import { MarketplaceAuctionGrid } from "../../auctions/components/MarketplaceAuctionGrid";
9
10
  import { AuctionFilters } from "../../auctions/components/AuctionFilters";
10
11
  import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
11
12
  import { pushWishlistOp } from "../../cart/utils/pending-ops";
12
13
  import { useCategoryTree, categoriesToFacetOptions } from "../../categories/hooks/useCategoryTree";
13
14
  import { useBrands } from "../hooks/useBrands";
15
+ import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
16
+ import { sortBy } from "../../../constants/sort";
17
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
18
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC");
14
19
  const AUCTION_SORT_OPTIONS = [
15
- { value: "auctionEndDate", label: "Ending Soonest" },
16
- { value: "-auctionEndDate", label: "Ending Latest" },
17
- { value: "currentBid", label: "Bid: Low to High" },
18
- { value: "-currentBid", label: "Bid: High to Low" },
19
- { value: "-createdAt", label: "Newly Listed" },
20
- { value: "createdAt", label: "Oldest Listed" },
20
+ { value: sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC"), label: "Ending Soonest" },
21
+ { value: sortBy(PRODUCT_FIELDS.AUCTION_END_DATE), label: "Ending Latest" },
22
+ { value: sortBy(PRODUCT_FIELDS.CURRENT_BID, "ASC"), label: "Bid: Low to High" },
23
+ { value: sortBy(PRODUCT_FIELDS.CURRENT_BID), label: "Bid: High to Low" },
24
+ { value: sortBy(PRODUCT_FIELDS.CREATED_AT), label: "Newly Listed" },
25
+ { value: sortBy(PRODUCT_FIELDS.CREATED_AT, "ASC"), label: "Oldest Listed" },
21
26
  ];
22
- const FILTER_KEYS = ["category", "brand", "minBid", "maxBid", "storeId", "dateFrom", "dateTo", "condition"];
27
+ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_BID, TABLE_KEYS.MAX_BID, TABLE_KEYS.STORE_ID, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
23
28
  export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
24
- const table = useUrlTable({ defaults: { pageSize: "24", sort: "auctionEndDate" } });
29
+ const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
25
30
  const { showToast } = useToast();
26
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
31
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
27
32
  const [filterOpen, setFilterOpen] = useState(false);
28
- const showEnded = table.get("showEnded") === "true";
29
- const [view, setView] = useState(table.get("view") || "grid");
33
+ const showEnded = table.get(TABLE_KEYS.SHOW_ENDED) === "true";
34
+ const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
30
35
  const localWishlist = useGuestWishlist();
31
36
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "auction").map((i) => i.itemId));
32
37
  const { categories } = useCategoryTree();
@@ -74,47 +79,50 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
74
79
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
75
80
  }, []);
76
81
  const resetAll = useCallback(() => {
77
- const updates = { q: "", sort: "", showEnded: "" };
82
+ const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_ENDED]: "" };
78
83
  for (const k of FILTER_KEYS)
79
84
  updates[k] = "";
80
85
  table.setMany(updates);
81
86
  setSearchInput("");
82
87
  }, [table]);
83
88
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
84
- const hasActiveState = !!table.get("q") ||
85
- table.get("showEnded") === "true" ||
86
- table.get("sort") !== "auctionEndDate" ||
89
+ const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
90
+ table.get(TABLE_KEYS.SHOW_ENDED) === "true" ||
91
+ table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
87
92
  activeFilterCount > 0;
88
93
  const params = {
89
- q: table.get("q") || undefined,
90
- category: table.get("category") || undefined,
94
+ q: table.get(TABLE_KEYS.QUERY) || undefined,
95
+ category: table.get(TABLE_KEYS.CATEGORY) || undefined,
91
96
  categorySlug: categorySlug || undefined,
92
- brand: brandName || table.get("brand") || undefined,
93
- minBid: table.get("minBid") ? Number(table.get("minBid")) : undefined,
94
- maxBid: table.get("maxBid") ? Number(table.get("maxBid")) : undefined,
95
- storeId: table.get("storeId") || undefined,
97
+ brand: brandName || table.get(TABLE_KEYS.BRAND) || undefined,
98
+ minBid: table.get(TABLE_KEYS.MIN_BID) ? Number(table.get(TABLE_KEYS.MIN_BID)) : undefined,
99
+ maxBid: table.get(TABLE_KEYS.MAX_BID) ? Number(table.get(TABLE_KEYS.MAX_BID)) : undefined,
100
+ storeId: table.get(TABLE_KEYS.STORE_ID) || undefined,
96
101
  // When showEnded is false (default), force dateFrom=now so only live auctions appear.
97
102
  // When showEnded is true, respect the filter-drawer value (or show all if none set).
98
103
  dateFrom: showEnded
99
- ? (table.get("dateFrom") || undefined)
104
+ ? (table.get(TABLE_KEYS.DATE_FROM) || undefined)
100
105
  : new Date().toISOString(),
101
- dateTo: table.get("dateTo") || undefined,
102
- sort: table.get("sort") || "auctionEndDate",
103
- page: table.getNumber("page", 1),
104
- perPage: table.getNumber("pageSize", 24),
106
+ dateTo: table.get(TABLE_KEYS.DATE_TO) || undefined,
107
+ sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
108
+ page: table.getNumber(TABLE_KEYS.PAGE, 1),
109
+ perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, 24),
105
110
  listingType: "auction",
106
111
  };
107
112
  const { products: auctions, totalPages, page, isLoading } = useProducts(params, { initialData });
113
+ const selection = useBulkSelection({ items: auctions, keyExtractor: (a) => a.id });
108
114
  const commitSearch = useCallback(() => {
109
- table.set("q", searchInput.trim());
115
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
110
116
  }, [searchInput, table]);
111
117
  const handleSearchKeyDown = (e) => {
112
118
  if (e.key === "Enter")
113
119
  commitSearch();
114
120
  };
115
121
  const handleViewToggle = (next) => {
122
+ if (next === VIEW_MODE.TABLE)
123
+ return;
116
124
  setView(next);
117
- table.set("view", next);
125
+ table.set(TABLE_KEYS.VIEW, next);
118
126
  };
119
127
  const wishlistActions = {
120
128
  addToWishlist: (productId) => {
@@ -132,5 +140,30 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
132
140
  isWishlisted: (productId) => wishlistedIds.has(productId),
133
141
  };
134
142
  const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-3 gap-4";
135
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "auctionEndDate", sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show ended" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showEnded, onClick: () => table.set("showEnded", showEnded ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showEnded ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showEnded ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), 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: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : (_jsx(MarketplaceAuctionGrid, { auctions: auctions, variant: view === "list" ? "list" : "grid", gridClassName: view === "list" ? "flex flex-col gap-4" : gridClass, wishlistActions: wishlistActions })) }), 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: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "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(AuctionFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _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})` : ""] }) })] })] }))] }));
143
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show ended" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showEnded, onClick: () => table.set(TABLE_KEYS.SHOW_ENDED, showEnded ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showEnded ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showEnded ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
144
+ {
145
+ id: "watchlist",
146
+ label: "Add to Watchlist",
147
+ variant: "primary",
148
+ onClick: () => {
149
+ const selected = auctions.filter((a) => selection.selectedIdSet.has(a.id));
150
+ selected.forEach((a) => {
151
+ wishlistActions.addToWishlist(a.id);
152
+ });
153
+ selection.clearSelection();
154
+ },
155
+ },
156
+ {
157
+ id: "remove-watchlist",
158
+ label: "Remove from Watchlist",
159
+ variant: "secondary",
160
+ onClick: () => {
161
+ const selected = auctions.filter((a) => selection.selectedIdSet.has(a.id));
162
+ selected.forEach((a) => {
163
+ wishlistActions.removeFromWishlist(a.id);
164
+ });
165
+ selection.clearSelection();
166
+ },
167
+ },
168
+ ] }), 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: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : (_jsx(MarketplaceAuctionGrid, { auctions: auctions, variant: view === "list" ? "list" : "grid", gridClassName: view === "list" ? "flex flex-col gap-4" : gridClass, wishlistActions: wishlistActions })) }), 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: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "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(AuctionFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _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})` : ""] }) })] })] }))] }));
136
169
  }
@@ -20,7 +20,6 @@ export interface BidHistoryProps {
20
20
  labels?: {
21
21
  title?: string;
22
22
  noHistory?: string;
23
- bidder?: string;
24
23
  amount?: string;
25
24
  time?: string;
26
25
  };
@@ -30,5 +30,5 @@ export function BidHistory({ isLoading = false, isEmpty = false, renderList, ren
30
30
  }
31
31
  if (renderList)
32
32
  return _jsx("div", { className: className, "data-section": "bidhistory-div-420", children: renderList() });
33
- return (_jsxs("div", { className: className, "data-section": "bidhistory-div-421", children: [labels.title && (_jsx("h3", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: labels.title })), _jsx("div", { className: "space-y-2", children: bids.map((bid, i) => renderBid ? (_jsx(React.Fragment, { children: renderBid(bid, i) }, bid.id)) : (_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-zinc-100 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-3 text-sm", "data-section": "bidhistory-div-422", children: [_jsx(Span, { className: "font-medium text-zinc-700 dark:text-zinc-300 truncate max-w-[40%]", children: bid.bidderName ?? bid.bidderId }), _jsx(Span, { className: "font-bold text-primary-600 dark:text-primary-400", children: currency ? formatCurrency(bid.amount, currency) : bid.amount.toLocaleString() }), _jsx(Span, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: formatBidDate(bid.placedAt) })] }, bid.id))) })] }));
33
+ return (_jsxs("div", { className: className, "data-section": "bidhistory-div-421", children: [labels.title && (_jsx("h3", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: labels.title })), _jsx("div", { className: "space-y-2", children: bids.map((bid, i) => renderBid ? (_jsx(React.Fragment, { children: renderBid(bid, i) }, bid.id)) : (_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-zinc-100 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-3 text-sm", "data-section": "bidhistory-div-422", children: [_jsx(Span, { className: "font-bold text-primary-600 dark:text-primary-400", children: currency ? formatCurrency(bid.amount, currency) : bid.amount.toLocaleString() }), _jsx(Span, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: formatBidDate(bid.placedAt) })] }, bid.id))) })] }));
34
34
  }
@@ -9,28 +9,32 @@ import { useCategoryTree, categoriesToFacetOptions } from "../../categories/hook
9
9
  import { useBrands } from "../hooks/useBrands";
10
10
  import { MarketplacePrizeDrawCard } from "./MarketplacePrizeDrawCard";
11
11
  import { ProductFilters } from "./ProductFilters";
12
+ import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
13
+ import { sortBy } from "../../../constants/sort";
14
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
15
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
12
16
  const PRIZE_DRAW_SORT_OPTIONS = [
13
- { value: "-createdAt", label: "Newest First" },
14
- { value: "createdAt", label: "Oldest First" },
15
- { value: "prizeRevealWindowStart", label: "Reveal: Soonest" },
16
- { value: "-prizeRevealWindowStart", label: "Reveal: Furthest" },
17
- { value: "pricePerEntry", label: "Entry: Low to High" },
18
- { value: "-pricePerEntry", label: "Entry: High to Low" },
17
+ { value: sortBy(PRODUCT_FIELDS.CREATED_AT), label: "Newest First" },
18
+ { value: sortBy(PRODUCT_FIELDS.CREATED_AT, "ASC"), label: "Oldest First" },
19
+ { value: sortBy(PRODUCT_FIELDS.PRIZE_REVEAL_WINDOW_START, "ASC"), label: "Reveal: Soonest" },
20
+ { value: sortBy(PRODUCT_FIELDS.PRIZE_REVEAL_WINDOW_START), label: "Reveal: Furthest" },
21
+ { value: sortBy(PRODUCT_FIELDS.PRICE, "ASC"), label: "Entry: Low to High" },
22
+ { value: sortBy(PRODUCT_FIELDS.PRICE), label: "Entry: High to Low" },
19
23
  ];
20
24
  const FILTER_KEYS = [
21
- "category",
22
- "brand",
23
- "minPrice",
24
- "maxPrice",
25
- "storeId",
26
- "prizeRevealStatus",
25
+ TABLE_KEYS.CATEGORY,
26
+ TABLE_KEYS.BRAND,
27
+ TABLE_KEYS.MIN_PRICE,
28
+ TABLE_KEYS.MAX_PRICE,
29
+ TABLE_KEYS.STORE_ID,
30
+ TABLE_KEYS.PRIZE_REVEAL_STATUS,
27
31
  ];
28
32
  export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, storeId: forcedStoreId, }) {
29
- const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
30
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
33
+ const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
34
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
31
35
  const [filterOpen, setFilterOpen] = useState(false);
32
- const showClosed = table.get("showClosed") === "true";
33
- const [view, setView] = useState(table.get("view") || "grid");
36
+ const showClosed = table.get(TABLE_KEYS.SHOW_CLOSED) === "true";
37
+ const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
34
38
  const { categories } = useCategoryTree();
35
39
  const categoryOptions = categoriesToFacetOptions(categories);
36
40
  const { brandOptions } = useBrands();
@@ -75,50 +79,53 @@ export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, s
75
79
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
76
80
  }, []);
77
81
  const resetAll = useCallback(() => {
78
- const updates = { q: "", sort: "", showClosed: "" };
82
+ const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_CLOSED]: "" };
79
83
  for (const k of FILTER_KEYS)
80
84
  updates[k] = "";
81
85
  table.setMany(updates);
82
86
  setSearchInput("");
83
87
  }, [table]);
84
88
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
85
- const hasActiveState = !!table.get("q") ||
89
+ const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
86
90
  showClosed ||
87
- table.get("sort") !== "-createdAt" ||
91
+ table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
88
92
  activeFilterCount > 0;
93
+ const revealFilter = (table.get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined);
89
94
  const params = {
90
- q: table.get("q") || undefined,
91
- category: table.get("category") || undefined,
95
+ q: table.get(TABLE_KEYS.QUERY) || undefined,
96
+ category: table.get(TABLE_KEYS.CATEGORY) || undefined,
92
97
  categorySlug: categorySlug || undefined,
93
- brand: brandName || table.get("brand") || undefined,
94
- minPrice: table.get("minPrice") ? Number(table.get("minPrice")) : undefined,
95
- maxPrice: table.get("maxPrice") ? Number(table.get("maxPrice")) : undefined,
96
- storeId: forcedStoreId || table.get("storeId") || undefined,
97
- sort: table.get("sort") || "-createdAt",
98
- page: table.getNumber("page", 1),
99
- perPage: table.getNumber("pageSize", 24),
98
+ brand: brandName || table.get(TABLE_KEYS.BRAND) || undefined,
99
+ minPrice: table.get(TABLE_KEYS.MIN_PRICE) ? Number(table.get(TABLE_KEYS.MIN_PRICE)) : undefined,
100
+ maxPrice: table.get(TABLE_KEYS.MAX_PRICE) ? Number(table.get(TABLE_KEYS.MAX_PRICE)) : undefined,
101
+ storeId: forcedStoreId || table.get(TABLE_KEYS.STORE_ID) || undefined,
102
+ prizeRevealStatus: revealFilter,
103
+ sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
104
+ page: table.getNumber(TABLE_KEYS.PAGE, 1),
105
+ perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, 24),
100
106
  listingType: "prize-draw",
101
- status: showClosed ? undefined : "published",
102
107
  };
103
108
  const { products: draws, totalPages, page, isLoading } = useProducts(params, { initialData });
104
- // Client-side filter for prizeRevealStatus until the server query supports it.
105
- const revealFilter = table.get("prizeRevealStatus");
106
- const filteredDraws = revealFilter
107
- ? draws.filter((d) => d.prizeRevealStatus === revealFilter)
109
+ // When no explicit reveal-status filter is set, hide closed draws client-side
110
+ // as a UX default (showClosed toggle). Server handles explicit status filters.
111
+ const filteredDraws = !revealFilter && !showClosed
112
+ ? draws.filter((d) => d.prizeRevealStatus !== PRODUCT_FIELDS.PRIZE_REVEAL_STATUS_VALUES.CLOSED)
108
113
  : draws;
109
114
  const commitSearch = useCallback(() => {
110
- table.set("q", searchInput.trim());
115
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
111
116
  }, [searchInput, table]);
112
117
  const handleSearchKeyDown = (e) => {
113
118
  if (e.key === "Enter")
114
119
  commitSearch();
115
120
  };
116
121
  const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4";
117
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PRIZE_DRAW_SORT_OPTIONS, onSortChange: (v) => {
118
- table.set("sort", v);
122
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRIZE_DRAW_SORT_OPTIONS, onSortChange: (v) => {
123
+ table.set(TABLE_KEYS.SORT, v);
119
124
  }, view: view, onViewChange: (v) => {
125
+ if (v === VIEW_MODE.TABLE)
126
+ return;
120
127
  setView(v);
121
- table.set("view", v);
128
+ table.set(TABLE_KEYS.VIEW, v);
122
129
  }, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set("showClosed", showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), 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: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : filteredDraws.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No prize draws found." })) : (_jsx("div", { className: gridClass, children: filteredDraws.map((product) => (_jsx(MarketplacePrizeDrawCard, { product: product, variant: view }, product.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: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "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" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "prizeRevealStatusFilter", className: "block text-xs font-semibold text-zinc-700 dark:text-zinc-300 mb-1.5", children: "Reveal status" }), _jsxs("select", { id: "prizeRevealStatusFilter", value: pendingFilters.prizeRevealStatus ?? "", onChange: (e) => setPendingFilters((p) => ({
123
130
  ...p,
124
131
  prizeRevealStatus: e.target.value,
@@ -259,7 +259,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
259
259
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
260
260
  : undefined, renderGroupSection: groupId
261
261
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
262
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx("ul", { className: "space-y-2", children: ingredients.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 rounded-full bg-primary-400" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx("ol", { className: "space-y-3", children: howToUse.map((step, i) => (_jsxs("li", { className: "flex items-start gap-3 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30 text-xs font-bold text-primary-700 dark:text-primary-300", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
262
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx("ul", { className: "space-y-2", children: ingredients.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 rounded-full bg-primary-400" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx("ol", { className: "space-y-3", children: howToUse.map((step, i) => (_jsxs("li", { className: "flex items-start gap-3 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30 text-xs font-bold text-primary-700 dark:text-primary-300", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
263
263
  id: s.id,
264
264
  label: s.title,
265
265
  content: _jsx(CustomSectionTabContent, { section: s }),