@mohasinac/appkit 4.11.3 → 4.12.0

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 (103) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +36 -3
  2. package/dist/_internal/server/features/checkout/actions.js +122 -44
  3. package/dist/_internal/server/features/orders/adapters.js +13 -0
  4. package/dist/_internal/server/features/products/list-public.d.ts +28 -0
  5. package/dist/_internal/server/features/products/list-public.js +92 -0
  6. package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
  7. package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
  8. package/dist/_internal/shared/checkout/lanes.js +28 -0
  9. package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
  10. package/dist/_internal/shared/features/checkout/config.js +8 -0
  11. package/dist/_internal/shared/fees/calculator.d.ts +34 -2
  12. package/dist/_internal/shared/fees/calculator.js +45 -1
  13. package/dist/client.d.ts +7 -1
  14. package/dist/client.js +4 -1
  15. package/dist/constants/api-endpoints.d.ts +6 -0
  16. package/dist/constants/api-endpoints.js +2 -0
  17. package/dist/features/admin/components/AdminAddressesView.js +18 -10
  18. package/dist/features/admin/components/AdminArtView.d.ts +9 -0
  19. package/dist/features/admin/components/AdminArtView.js +11 -35
  20. package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
  21. package/dist/features/admin/components/AdminClassifiedView.js +11 -35
  22. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
  23. package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
  24. package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
  25. package/dist/features/admin/components/AdminLiveView.js +11 -41
  26. package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
  27. package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
  28. package/dist/features/admin/components/AdminOrdersView.js +14 -1
  29. package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
  30. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  31. package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
  32. package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
  33. package/dist/features/admin/components/AdminStickersView.js +11 -35
  34. package/dist/features/admin/hooks/useAdminListing.js +7 -2
  35. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  36. package/dist/features/admin/schemas/firestore.js +1 -0
  37. package/dist/features/auctions/actions/bid-actions.js +2 -2
  38. package/dist/features/cart/components/CartDrawer.d.ts +9 -1
  39. package/dist/features/cart/components/CartDrawer.js +4 -3
  40. package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
  41. package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
  42. package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
  43. package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
  44. package/dist/features/cart/components/index.d.ts +4 -0
  45. package/dist/features/cart/components/index.js +2 -0
  46. package/dist/features/cart/repository/cart.repository.d.ts +20 -2
  47. package/dist/features/cart/repository/cart.repository.js +49 -0
  48. package/dist/features/cart/schemas/firestore.d.ts +26 -0
  49. package/dist/features/layout/BottomActions.js +37 -4
  50. package/dist/features/layout/BottomActionsContext.d.ts +12 -0
  51. package/dist/features/layout/BottomActionsContext.js +5 -0
  52. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
  53. package/dist/features/layout/hooks/useBottomActions.js +11 -1
  54. package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
  55. package/dist/features/orders/components/OrderAddonBadges.js +28 -0
  56. package/dist/features/orders/components/index.d.ts +1 -0
  57. package/dist/features/orders/components/index.js +1 -0
  58. package/dist/features/orders/repository/orders.repository.d.ts +32 -0
  59. package/dist/features/orders/repository/orders.repository.js +14 -0
  60. package/dist/features/orders/types/index.d.ts +22 -0
  61. package/dist/features/products/components/ArtStickersListView.js +5 -4
  62. package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
  63. package/dist/features/products/components/ProductsIndexListing.js +87 -11
  64. package/dist/features/products/components/ProductsIndexPageView.js +14 -2
  65. package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
  66. package/dist/features/products/config/listing-type-listing-config.js +74 -0
  67. package/dist/features/products/hooks/useProducts.js +14 -0
  68. package/dist/features/products/types/index.d.ts +15 -0
  69. package/dist/features/seller/components/SellerOrdersView.js +2 -1
  70. package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
  71. package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
  72. package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
  73. package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
  74. package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
  75. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
  76. package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
  77. package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
  78. package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
  79. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
  80. package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
  81. package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
  82. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
  83. package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
  84. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
  85. package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
  86. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
  87. package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
  88. package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
  89. package/dist/features/stores/components/StoreProductsListing.js +3 -2
  90. package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
  91. package/dist/features/stores/components/StoreProductsPageView.js +16 -21
  92. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +602 -0
  93. package/dist/next/routing/route-map.d.ts +2 -0
  94. package/dist/next/routing/route-map.js +5 -0
  95. package/dist/seed/reviews-seed-data.js +52 -0
  96. package/dist/server-entry.d.ts +1 -0
  97. package/dist/server-entry.js +1 -0
  98. package/dist/styles.css +12 -1
  99. package/dist/tailwind-utilities.css +1 -1
  100. package/dist/ui/components/Checkbox.d.ts +10 -1
  101. package/dist/ui/components/Checkbox.js +14 -2
  102. package/dist/ui/components/Checkbox.style.css +11 -0
  103. package/package.json +1 -1
@@ -1,33 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
- import { productRepository } from "../../../repositories";
4
2
  import { Container, Main, Heading, Section, Text } from "../../../ui";
5
3
  import { AdSlot } from "../../homepage/components/AdSlot";
6
- import { parseListingSearchParams } from "../../../utils/listing-params";
4
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
5
+ import { sortBy } from "../../../constants/sort";
6
+ import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
7
7
  import { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
8
- const DEFAULT_PAGE = 1;
8
+ const LISTING_TYPES = ["prize-draw"];
9
9
  const DEFAULT_PAGE_SIZE = 24;
10
- const DEFAULT_SORT = "-createdAt";
11
- function sp(params, key) {
12
- const v = params[key];
13
- return Array.isArray(v) ? v[0] ?? "" : v ?? "";
14
- }
15
- function buildPrizeDrawFilters(params) {
16
- const parts = ["status==published", "listingType==prize-draw"];
17
- const minPrice = sp(params, "minPrice");
18
- const maxPrice = sp(params, "maxPrice");
19
- if (minPrice)
20
- parts.push(sieveFilter("pricePerEntry", SIEVE_OP.GTE, minPrice));
21
- if (maxPrice)
22
- parts.push(sieveFilter("pricePerEntry", SIEVE_OP.LTE, maxPrice));
23
- const store = sp(params, "storeId");
24
- if (store)
25
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, store));
26
- const status = sp(params, "prizeRevealStatus");
27
- if (status)
28
- parts.push(sieveFilter("prizeRevealStatus", SIEVE_OP.EQ, status));
29
- return parts.join(",");
30
- }
10
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
31
11
  /**
32
12
  * Public listing page (SB4-F). Server-fetches published prize-draw products,
33
13
  * hydrates the client `PrizeDrawsIndexListing` which renders the filter
@@ -38,13 +18,18 @@ function buildPrizeDrawFilters(params) {
38
18
  * already gone (matches `PrizeDrawCollage`'s `hideWonState` prop).
39
19
  */
40
20
  export async function PrizeDrawsListingView({ searchParams = {}, }) {
41
- const std = parseListingSearchParams(searchParams);
42
- const sort = std.sorts ?? DEFAULT_SORT;
43
- const page = std.page ?? DEFAULT_PAGE;
44
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
45
- const filters = buildPrizeDrawFilters(searchParams);
46
- const result = await productRepository
47
- .list({ filters, sorts: sort, page, pageSize })
48
- .catch(() => null);
21
+ // Routed through the shared query (2026-08-21). The hand-rolled builder this
22
+ // replaces had its own Root Cause #30 divergence: it applied the price range
23
+ // to `pricePerEntry`, while the client's refetch sends minPrice/maxPrice,
24
+ // which the shared query applies to `price`. Both fields carry the same
25
+ // value today, so the split was invisible — until one of them drifted.
26
+ // `price` wins because it is also what PRIZE_DRAW_SORT_OPTIONS sorts by for
27
+ // "Entry: Low to High".
28
+ const result = await listPublicProducts(parsePublicProductParams(searchParams, {
29
+ listingTypes: LISTING_TYPES,
30
+ pageSize: DEFAULT_PAGE_SIZE,
31
+ sorts: DEFAULT_SORT,
32
+ ...defaultTogglesForListingTypes(LISTING_TYPES),
33
+ }));
49
34
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2", color: "primary", size: "3xl", weight: "semibold", children: "Prize Draws" }), _jsx(Text, { className: "mb-6 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Fair-RNG draws for sealed Pok\u00E9mon, Hot Wheels Super Treasure Hunts, Gundam kits and more. Every winner picked by crypto.randomInt \u2014 proof on GitHub." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(PrizeDrawsIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
50
35
  }
@@ -12,7 +12,7 @@ import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-d
12
12
  import { CompareOverlay } from "./CompareOverlay";
13
13
  import { ROUTES } from "../../../next";
14
14
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
15
- import { ProductGrid, ProductFilters, PRODUCT_PUBLIC_SORT_OPTIONS } from ".";
15
+ import { ProductGrid, ProductFilters } from ".";
16
16
  import { useGuestCart } from "../../cart/hooks/useGuestCart";
17
17
  import { getGuestCartItems } from "../../cart/utils/guest-cart";
18
18
  import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
@@ -28,6 +28,9 @@ import { sortBy } from "../../../constants/sort";
28
28
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
29
29
  import { useBottomActions } from "../../layout";
30
30
  import { GENERIC_PRODUCT_LISTING_TYPES, PRODUCT_TYPE_FILTER_TABS } from "../constants/listing-tabs";
31
+ import { parseSelectedListingTypes } from "../utils/listing-type";
32
+ import { commonSortOptionsFor, hideDefaultsFor } from "../../../_internal/shared/listing-types/_registry";
33
+ import { TextLink } from "../../../ui";
31
34
  const __P = {
32
35
  p3: "p-[var(--appkit-space-3)]",
33
36
  };
@@ -46,7 +49,40 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
46
49
  const [compareIds, setCompareIds] = useState([]);
47
50
  const showSold = table.get(TABLE_KEYS.SHOW_SOLD) === "true";
48
51
  const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
49
- const productType = table.get(TABLE_KEYS.LISTING_TYPE) || "All";
52
+ // ── Type filter (multi-select) ───────────────────────────────────────────
53
+ // The URL value is a pipe-joined set; empty means "every type this page
54
+ // spans". Unknown tokens are dropped rather than passed through to a
55
+ // Firestore `==` that could never match (Root Cause #33).
56
+ const typeParam = table.get(TABLE_KEYS.LISTING_TYPE);
57
+ const selectedTypes = parseSelectedListingTypes(typeParam).filter((t) => listingTypes.includes(t));
58
+ const effectiveTypes = (selectedTypes.length > 0 ? selectedTypes : listingTypes);
59
+ // Sorts valid across every selected type. Pick Auctions alone and you get
60
+ // "Ending Soon"; pick Auctions + Products and you get only what both
61
+ // support, because a sort on `auctionEndDate` would order the products
62
+ // arbitrarily (and needs an index nobody declares for that shape).
63
+ const sortOptions = commonSortOptionsFor(effectiveTypes, "public");
64
+ const sortValue = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
65
+ // Guard against a sort carried over from a previous selection that the new
66
+ // one can't satisfy — that combination throws FAILED_PRECONDITION in
67
+ // Firestore and surfaces as a bare empty page (Root Cause #2).
68
+ const effectiveSort = sortOptions.some((o) => o.value === sortValue)
69
+ ? sortValue
70
+ : DEFAULT_SORT;
71
+ // Which "Show X" toggles are meaningful for the current selection. A mixed
72
+ // set can need both (products sell out, auctions end).
73
+ const hideDefaults = hideDefaultsFor(effectiveTypes);
74
+ const showsSoldToggle = hideDefaults.includes("sold");
75
+ const showsEndedToggle = hideDefaults.includes("ended") || hideDefaults.includes("closed");
76
+ const showEnded = table.get(TABLE_KEYS.SHOW_ENDED) === "true";
77
+ // "Full <type> filters →" — a single selected type with a dedicated browse
78
+ // page gets a link to it, since that page carries facets this generic one
79
+ // can't (bid range, delivery-date window, reveal status).
80
+ const dedicatedPage = selectedTypes.length === 1 && pluginFor(selectedTypes[0]).browseRoute
81
+ ? {
82
+ href: pluginFor(selectedTypes[0]).browseRoute,
83
+ label: `Full ${pluginFor(selectedTypes[0]).pluralLabel} filters →`,
84
+ }
85
+ : null;
50
86
  const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
51
87
  const openFilters = useCallback(() => {
52
88
  onFilterReset();
@@ -57,11 +93,19 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
57
93
  setFilterOpen(false);
58
94
  }, [onFilterApply]);
59
95
  const resetAll = useCallback(() => {
60
- onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_SOLD]: "" });
96
+ onResetAll({
97
+ [TABLE_KEYS.QUERY]: "",
98
+ [TABLE_KEYS.SORT]: "",
99
+ [TABLE_KEYS.SHOW_SOLD]: "",
100
+ [TABLE_KEYS.SHOW_ENDED]: "",
101
+ [TABLE_KEYS.LISTING_TYPE]: "",
102
+ });
61
103
  setSearchInput("");
62
104
  }, [onResetAll]);
63
105
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
64
106
  table.get(TABLE_KEYS.SHOW_SOLD) === "true" ||
107
+ table.get(TABLE_KEYS.SHOW_ENDED) === "true" ||
108
+ !!typeParam ||
65
109
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
66
110
  filterActiveCount > 0;
67
111
  const localCart = useGuestCart();
@@ -81,13 +125,27 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
81
125
  brand: table.get(TABLE_KEYS.BRAND) || undefined,
82
126
  storeId: table.get(TABLE_KEYS.STORE_ID) || undefined,
83
127
  freeShipping: table.get(TABLE_KEYS.FREE_SHIPPING) === "true" ? true : undefined,
84
- sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
128
+ // These three are in FILTER_KEYS (so they count toward the filter badge)
129
+ // but were never put on the wire — the drawer sections were inert.
130
+ tags: table.get(TABLE_KEYS.TAGS) || undefined,
131
+ sublistingCategory: table.get(TABLE_KEYS.SUBLISTING_CATEGORY) || undefined,
132
+ features: table.get(TABLE_KEYS.FEATURES) || undefined,
133
+ sort: effectiveSort,
85
134
  page: table.getNumber(TABLE_KEYS.PAGE, 1),
86
135
  perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, 24),
87
- listingType: productType === "All" ? listingTypes.join("|") : productType,
88
- // Hide sold-out items by default. Uses stockQuantity>0 (always-present field)
89
- // instead of status=="published" because sellers don't actively transition status.
90
- inStock: showSold ? undefined : true,
136
+ // Pipe-joined OR-group sievejs parses it as a same-field OR and the
137
+ // Firebase adapter upgrades it to a `.where(…, "in", …)` query.
138
+ listingType: effectiveTypes.join("|"),
139
+ // Hide sold-out items by default, but only when a sold-out-able type is in
140
+ // play — an auction has no meaningful stock, so applying it to an
141
+ // auctions-only view would hide every live auction.
142
+ inStock: showsSoldToggle && !showSold ? true : undefined,
143
+ // Same shape for time-boxed types. `dateFrom` only takes effect when
144
+ // exactly one type is selected (there is no single end-date field shared
145
+ // across types), which `dateFieldFor` already enforces server-side.
146
+ dateFrom: showsEndedToggle && !showEnded && selectedTypes.length === 1
147
+ ? new Date().toISOString()
148
+ : undefined,
91
149
  };
92
150
  const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
93
151
  const selection = useBulkSelection({ items: products, keyExtractor: (p) => p.id });
@@ -200,14 +258,32 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
200
258
  },
201
259
  },
202
260
  ] } } : {});
203
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRODUCT_PUBLIC_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: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
204
- { label: "Show sold", active: showSold, onChange: (next) => table.set(TABLE_KEYS.SHOW_SOLD, next ? "true" : "") },
261
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: effectiveSort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
262
+ // Each toggle appears only when it means something for the selected
263
+ // types — "Show ended" against a Products-only view would be noise,
264
+ // and "Show sold" against an Auctions-only view is meaningless.
265
+ ...(showsSoldToggle
266
+ ? [{ label: "Show sold", active: showSold, onChange: (next) => table.set(TABLE_KEYS.SHOW_SOLD, next ? "true" : "") }]
267
+ : []),
268
+ ...(showsEndedToggle
269
+ ? [{ label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") }]
270
+ : []),
205
271
  // Inline quick-filter: the highest-frequency drawer-only facet promoted
206
272
  // to the sticky toolbar so it doesn't require open-drawer → check → Apply
207
273
  // → close for every toggle. The full facet set (category/price/brand/…)
208
274
  // remains in <FilterDrawer> below.
209
275
  { label: "Free shipping", active: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (next) => table.set(TABLE_KEYS.FREE_SHIPPING, next ? "true" : "") },
210
- ] }), _jsx(Div, { padding: "y-sm", children: _jsx(FilterChipGroup, { label: "Type", tabs: typeTabs, value: productType, onChange: (v) => table.set(TABLE_KEYS.LISTING_TYPE, v === "All" ? "" : v) }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
276
+ ] }), _jsx(Div, { padding: "y-sm", children: _jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsx(FilterChipGroup, { multiple: true, label: "Type", tabs: typeTabs, value: selectedTypes.join("|"), onChange: (next) => {
277
+ // Clear `sort` alongside the type change: a sort valid for the
278
+ // old selection (say -auctionEndDate) can be invalid for the new
279
+ // one, and useUrlTable's single router.replace means we must set
280
+ // both in ONE call — a follow-up setPage/set would read stale
281
+ // searchParams and overwrite this update (Root Cause #13).
282
+ table.setMany({
283
+ [TABLE_KEYS.LISTING_TYPE]: next,
284
+ [TABLE_KEYS.SORT]: "",
285
+ });
286
+ } }), dedicatedPage && (_jsx(TextLink, { href: dedicatedPage.href, size: "sm", weight: "medium", children: dedicatedPage.label }))] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
211
287
  {
212
288
  id: ACTION_ID.ADD_TO_CART,
213
289
  label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
@@ -4,19 +4,31 @@ import { Container, Div, Heading, Main, Section, Text, TextLink } from "../../..
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { ProductsIndexListing } from "./ProductsIndexListing";
6
6
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
7
+ import { TABLE_KEYS } from "../../../constants/table-keys";
7
8
  import { sortBy } from "../../../constants/sort";
8
9
  import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs";
9
- import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
10
+ import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
11
+ import { parseSelectedListingTypes } from "../utils/listing-type";
10
12
  const DEFAULT_PAGE_SIZE = 24;
11
13
  const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
12
14
  export async function ProductsIndexPageView({ searchParams = {} }) {
15
+ // The "Show sold" / "Show ended" defaults depend on WHICH types are in play,
16
+ // and this page now spans all nine. Both sides derive them from the same
17
+ // helper — a hardcoded `hideSoldByDefault: true` here would disagree with
18
+ // the client the moment the user ticks Auctions, and `staleTime: Infinity`
19
+ // would freeze that disagreement (Root Cause #30).
20
+ const raw = searchParams[TABLE_KEYS.LISTING_TYPE];
21
+ const selectedTypes = parseSelectedListingTypes(Array.isArray(raw) ? raw[0] : raw);
22
+ const effectiveTypes = selectedTypes.length > 0 ? selectedTypes : GENERIC_PRODUCT_LISTING_TYPES;
23
+ const { hideSoldByDefault, hideEndedByDefault } = defaultTogglesForListingTypes(effectiveTypes);
13
24
  // See ArtStickersListView — one shared filter implementation with
14
25
  // /api/products, so SSR and the client refetch agree by construction.
15
26
  const products = await listPublicProducts(parsePublicProductParams(searchParams, {
16
27
  listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
17
28
  pageSize: DEFAULT_PAGE_SIZE,
18
29
  sorts: DEFAULT_SORT,
19
- hideSoldByDefault: true,
30
+ hideSoldByDefault,
31
+ hideEndedByDefault,
20
32
  }));
21
33
  return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Products" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Discover amazing products and deals" }), _jsx(Div, { className: "mt-3", children: _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.AUCTIONS), rounded: "full", paddingX: "sm", paddingY: "2xs", size: "xs", weight: "medium", layout: "inline-flex", align: "center", gap: "sm", className: "border border-primary/30 bg-primary/10 text-primary-700 dark:text-primary-400 hover:bg-primary/15 transition-colors", children: "\uD83C\uDFF7\uFE0F Looking for unique deals? Browse Auctions \u2192" }) })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: products }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
22
34
  }
@@ -0,0 +1,31 @@
1
+ import type { JsonArray } from "../../../schemas/types";
2
+ import type { ListingType } from "../types";
3
+ import type { ListingViewConfig } from "../../admin/components/DataListingView";
4
+ export interface ListingTypeListingRow {
5
+ id: string;
6
+ primary: string;
7
+ secondary: string;
8
+ status: string;
9
+ updatedAt: string;
10
+ image?: string;
11
+ }
12
+ interface ListingTypeListingResponse {
13
+ items?: JsonArray;
14
+ total?: number;
15
+ }
16
+ export interface BuildListingTypeListingOptions {
17
+ /** Override the page heading; defaults to the type's registered plural label. */
18
+ title?: string;
19
+ searchPlaceholder?: string;
20
+ emptyLabel?: string;
21
+ }
22
+ /**
23
+ * Admin browse config for one listing type.
24
+ *
25
+ * `status` is a real filter here (unlike the five hand-written copies, which
26
+ * offered none) because an admin's main reason to open a per-type list is to
27
+ * find the drafts and in-review items — exactly the rows a status filter
28
+ * surfaces.
29
+ */
30
+ export declare function buildListingTypeListingConfig(type: ListingType, opts?: BuildListingTypeListingOptions): ListingViewConfig<ListingTypeListingResponse, ListingTypeListingRow>;
31
+ export {};
@@ -0,0 +1,74 @@
1
+ /*
2
+ * WHY: Five admin views (Art, Stickers, Classified, Digital Codes, Live Items)
3
+ * were five copies of ONE config that differed only by listing type,
4
+ * title and placeholder text. Five copies is well past the Duplication
5
+ * Framework's Rule of Three, and it showed: every one carried
6
+ * `filterKeys: []` (no filter drawer at all despite filterable data) and
7
+ * its own hand-written three-option sort array, so a fix to any of it had
8
+ * to be applied five times and never was.
9
+ *
10
+ * WHAT: `buildListingTypeListingConfig(type, opts)` — one config, driven by
11
+ * the listing-type plugin registry. Title, sort set and per-type facet
12
+ * keys all come from `pluginFor(type)`, so a new listing type gets a
13
+ * working admin browse page with no new file.
14
+ *
15
+ * EXPORTS: ListingTypeListingRow, BuildListingTypeListingOptions,
16
+ * buildListingTypeListingConfig
17
+ *
18
+ * @tag domain:products
19
+ * @tag layer:config
20
+ * @tag pattern:factory
21
+ * @tag access:client
22
+ * @tag consumers:AdminArtView,AdminStickersView,AdminClassifiedView,AdminDigitalCodesView,AdminLiveView
23
+ * @tag sideEffects:none
24
+ */
25
+ import { SIEVE_OP, sieveAnd, sieveFilter } from "../../../utils/sieve-builder";
26
+ import { sortBy } from "../../../constants/sort";
27
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
28
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
29
+ import { ROUTES } from "../../../next/routing/route-map";
30
+ import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
31
+ import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../../admin/hooks/useAdminListingData";
32
+ /**
33
+ * Admin browse config for one listing type.
34
+ *
35
+ * `status` is a real filter here (unlike the five hand-written copies, which
36
+ * offered none) because an admin's main reason to open a per-type list is to
37
+ * find the drafts and in-review items — exactly the rows a status filter
38
+ * surfaces.
39
+ */
40
+ export function buildListingTypeListingConfig(type, opts = {}) {
41
+ const plugin = pluginFor(type);
42
+ const label = opts.title ?? plugin.pluralLabel;
43
+ return {
44
+ portal: "admin",
45
+ title: label,
46
+ searchPlaceholder: opts.searchPlaceholder ?? `Search ${label.toLowerCase()} by name or seller`,
47
+ emptyLabel: opts.emptyLabel ?? `No ${label.toLowerCase()} listings`,
48
+ filterKeys: ["status"],
49
+ defaultSort: sortBy(PRODUCT_FIELDS.CREATED_AT),
50
+ // Keyed on the canonical type so two types can never share a cache entry.
51
+ queryKey: ["admin", "listing-type", type],
52
+ endpoint: ADMIN_ENDPOINTS.PRODUCTS,
53
+ // The type's own sort set, not a copy-pasted three-option array.
54
+ sortOptions: [...plugin.sortOptions],
55
+ mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
56
+ id: toStringValue(item.id, `${type}-${index}`),
57
+ primary: toStringValue(item.title ?? item.productTitle, "Untitled listing"),
58
+ secondary: [
59
+ toStringValue(item.sellerName, "Unknown seller"),
60
+ toCurrency(item.price),
61
+ ].join(" · "),
62
+ status: toStringValue(item.status, "draft"),
63
+ updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
64
+ image: toStringValue(item.mainImage, "") || undefined,
65
+ })),
66
+ getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
67
+ buildFilters: (state) => sieveAnd(sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, type), state.status && state.status !== "All"
68
+ ? sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, state.status)
69
+ : "") || undefined,
70
+ // These are products filtered by listingType — reuse the real admin
71
+ // product editor rather than leaving rows non-navigable.
72
+ rowHrefTemplate: String(ROUTES.ADMIN.PRODUCTS_EDIT("{id}")),
73
+ };
74
+ }
@@ -52,6 +52,20 @@ export function useProducts(params = {}, opts) {
52
52
  sp.set("brand", params.brand);
53
53
  if (params.freeShipping !== undefined)
54
54
  sp.set("freeShipping", String(params.freeShipping));
55
+ // Pipe-joined multi-select facets. Omitted here until 2026-08-21, so the
56
+ // Tags / Sublisting Type / Features drawer sections were inert.
57
+ if (params.tags)
58
+ sp.set("tags", params.tags);
59
+ if (params.sublistingCategory)
60
+ sp.set("sublistingCategory", params.sublistingCategory);
61
+ if (params.features)
62
+ sp.set("features", params.features);
63
+ // Per-type facets travel under their own TABLE_KEY names, which is what
64
+ // parsePublicProductParams reads them back from.
65
+ for (const [key, value] of Object.entries(params.typeFacets ?? {})) {
66
+ if (value)
67
+ sp.set(key, value);
68
+ }
55
69
  const qs = sp.toString();
56
70
  const query = useQuery({
57
71
  queryKey: ["products", qs],
@@ -202,4 +202,19 @@ export interface ProductListParams {
202
202
  brand?: string;
203
203
  /** Shipping / free shipping */
204
204
  freeShipping?: boolean;
205
+ /**
206
+ * Pipe-joined multi-select facets. All three were rendered by
207
+ * `<ProductFilters>` and counted toward the active-filter badge, but no
208
+ * caller ever put them on the wire — ticking one inflated the badge and
209
+ * changed nothing (fixed 2026-08-21).
210
+ */
211
+ tags?: string;
212
+ sublistingCategory?: string;
213
+ features?: string;
214
+ /**
215
+ * Per-listing-type facets keyed by TABLE_KEY (classified city/negotiable,
216
+ * digital-code deliveryMethod, live species/sex/jurisdiction). The server
217
+ * resolves each to its nested Firestore path and ignores unknown keys.
218
+ */
219
+ typeFacets?: Record<string, string>;
205
220
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { OrderAddonBadges } from "../../orders/components/OrderAddonBadges";
3
4
  import { normalizeError } from "../../../errors/normalize";
4
5
  import { Row, SIEVE_OP, sieveFilter } from "@mohasinac/appkit/client";
5
6
  import { sortBy } from "@mohasinac/appkit/client";
@@ -167,7 +168,7 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
167
168
  };
168
169
  const addr = order?.shippingAddress ?? {};
169
170
  const addrLine = [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
170
- return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title ?? "Order item", size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toCurrency(item.price ?? 0) })] }, i))) })] })), order.giftWrapAddon && (_jsxs(Div, { className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)]", surface: "subtle", padding: "inline", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "\uD83C\uDF81 Gift wrap requested" }), order.giftWrapMessage && (_jsxs(Text, { size: "sm", className: "mt-1 text-[var(--appkit-color-text-secondary)]", children: ["\u201C", order.giftWrapMessage, "\u201D"] }))] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toCurrency(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-1", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Payment" }), isManualPaymentMethod(order.paymentMethod) && (_jsx(Badge, { variant: sellerPaymentBadge(order).variant, children: sellerPaymentBadge(order).label }))] }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod }), isManualPaymentMethod(order.paymentMethod) && order.paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", className: "mt-1", children: ["UTR: ", order.paymentTransactionId] }))] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toCurrency(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
171
+ return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title ?? "Order item", size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toCurrency(item.price ?? 0) })] }, i))) })] })), _jsx(OrderAddonBadges, { order: order, variant: "detail" }), order.giftWrapAddon && (_jsxs(Div, { className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)]", surface: "subtle", padding: "inline", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "\uD83C\uDF81 Gift wrap requested" }), order.giftWrapMessage && (_jsxs(Text, { size: "sm", className: "mt-1 text-[var(--appkit-color-text-secondary)]", children: ["\u201C", order.giftWrapMessage, "\u201D"] }))] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toCurrency(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-1", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Payment" }), isManualPaymentMethod(order.paymentMethod) && (_jsx(Badge, { variant: sellerPaymentBadge(order).variant, children: sellerPaymentBadge(order).label }))] }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod }), isManualPaymentMethod(order.paymentMethod) && order.paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", className: "mt-1", children: ["UTR: ", order.paymentTransactionId] }))] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toCurrency(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
171
172
  ? `Paid ${toRelativeDate(inst.paidAt)}`
172
173
  : inst.dueDate
173
174
  ? `Due ${toRelativeDate(inst.dueDate)}`
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ type SearchParams = Record<string, string | string[]>;
3
+ export interface StoreArtStickersPageViewProps {
4
+ storeSlug: string;
5
+ searchParams?: SearchParams;
6
+ }
7
+ /**
8
+ * A store's combined Art & Stickers tab.
9
+ *
10
+ * This page did not exist until 2026-08-21: `STORE_PAGE_TABS` had an
11
+ * "Art & Stickers" entry with a real count query behind it, but its href
12
+ * pointed at `STORE_PRODUCTS` — a page that filters `listingType == standard`.
13
+ * So the tab advertised a non-zero count and then rendered none of those items,
14
+ * with no error. Same shape as the category/brand tabs that rendered blank
15
+ * panels (see `audit-tab-body-coverage.mjs`), one level up.
16
+ *
17
+ * Mirrors the public `/art` page in spanning both types at once, since neither
18
+ * has enough inventory to justify a tab of its own.
19
+ */
20
+ export declare function StoreArtStickersPageView({ storeSlug, searchParams, }: StoreArtStickersPageViewProps): Promise<React.JSX.Element | null>;
21
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
3
+ import { sortBy } from "../../../constants/sort";
4
+ import { listStoreProducts } from "../../../_internal/server/features/products/list-public";
5
+ import { ART_STICKERS_LISTING_TYPES } from "../../products/constants/listing-tabs";
6
+ import { getStoreBySlug } from "./StoreDetailLayoutView";
7
+ import { StoreProductsListing } from "./StoreProductsListing";
8
+ const DEFAULT_PAGE_SIZE = 24;
9
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
10
+ /**
11
+ * A store's combined Art & Stickers tab.
12
+ *
13
+ * This page did not exist until 2026-08-21: `STORE_PAGE_TABS` had an
14
+ * "Art & Stickers" entry with a real count query behind it, but its href
15
+ * pointed at `STORE_PRODUCTS` — a page that filters `listingType == standard`.
16
+ * So the tab advertised a non-zero count and then rendered none of those items,
17
+ * with no error. Same shape as the category/brand tabs that rendered blank
18
+ * panels (see `audit-tab-body-coverage.mjs`), one level up.
19
+ *
20
+ * Mirrors the public `/art` page in spanning both types at once, since neither
21
+ * has enough inventory to justify a tab of its own.
22
+ */
23
+ export async function StoreArtStickersPageView({ storeSlug, searchParams, }) {
24
+ const store = await getStoreBySlug(storeSlug);
25
+ const storeId = store?.id;
26
+ if (typeof storeId !== "string" || !storeId)
27
+ return null;
28
+ const result = await listStoreProducts(storeId, ART_STICKERS_LISTING_TYPES, searchParams ?? {}, { pageSize: DEFAULT_PAGE_SIZE, sorts: DEFAULT_SORT });
29
+ return (_jsx(StoreProductsListing, { storeId: storeId, listingTypes: ART_STICKERS_LISTING_TYPES, initialData: result ?? undefined }));
30
+ }
@@ -1,5 +1,8 @@
1
1
  import React from "react";
2
+ type SearchParams = Record<string, string | string[]>;
2
3
  export interface StoreAuctionsPageViewProps {
3
4
  storeSlug: string;
5
+ searchParams?: SearchParams;
4
6
  }
5
- export declare function StoreAuctionsPageView({ storeSlug }: StoreAuctionsPageViewProps): Promise<React.JSX.Element | null>;
7
+ export declare function StoreAuctionsPageView({ storeSlug, searchParams }: StoreAuctionsPageViewProps): Promise<React.JSX.Element | null>;
8
+ export {};
@@ -1,24 +1,24 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { sieveFilter, sieveAnd, SIEVE_OP } from "@mohasinac/appkit";
3
- import { sortBy } from "@mohasinac/appkit";
4
- import { productRepository } from "../../../repositories";
2
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
3
+ import { sortBy } from "../../../constants/sort";
4
+ import { listStoreProducts } from "../../../_internal/server/features/products/list-public";
5
5
  import { getStoreBySlug } from "./StoreDetailLayoutView";
6
6
  import { StoreAuctionsListing } from "./StoreAuctionsListing";
7
- export async function StoreAuctionsPageView({ storeSlug }) {
7
+ const LISTING_TYPES = ["auction"];
8
+ const DEFAULT_PAGE_SIZE = 24;
9
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC");
10
+ export async function StoreAuctionsPageView({ storeSlug, searchParams }) {
8
11
  const store = await getStoreBySlug(storeSlug);
9
12
  const storeId = store?.id;
10
- const result = storeId
11
- ? await productRepository
12
- .list({
13
- filters: sieveAnd(sieveFilter("storeId", SIEVE_OP.EQ, storeId), sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "auction")),
14
- sorts: sortBy("auctionEndDate", "ASC"),
15
- page: 1,
16
- pageSize: 24,
17
- })
18
- .catch(() => null)
19
- : null;
20
- if (!storeId) {
13
+ if (typeof storeId !== "string" || !storeId)
21
14
  return null;
22
- }
23
- return (_jsx(StoreAuctionsListing, { storeId: storeId, initialData: result ?? undefined }));
15
+ // Shared query — see listStoreProducts. This view used to call
16
+ // productRepository.list() with an inline filter string and swallow every
17
+ // failure via a bare .catch(() => null), so a missing index rendered as an
18
+ // empty store tab with nothing logged (Root Cause #30's family).
19
+ const result = await listStoreProducts(storeId, LISTING_TYPES, searchParams ?? {}, {
20
+ pageSize: DEFAULT_PAGE_SIZE,
21
+ sorts: DEFAULT_SORT,
22
+ });
23
+ return _jsx(StoreAuctionsListing, { storeId: storeId, initialData: result ?? undefined });
24
24
  }
@@ -37,6 +37,13 @@ export function StoreClassifiedsListing({ storeId, initialData }) {
37
37
  perPage: table.getNumber("pageSize", 24),
38
38
  storeId: storeId || undefined,
39
39
  listingType: "classified",
40
+ // Per-type facets — declared in FILTER_KEYS (so they counted toward the
41
+ // filter badge) and rendered in the drawer, but never sent until 2026-08-21.
42
+ typeFacets: {
43
+ [TABLE_KEYS.CITY]: table.get(TABLE_KEYS.CITY),
44
+ [TABLE_KEYS.NEGOTIABLE]: table.get(TABLE_KEYS.NEGOTIABLE),
45
+ [TABLE_KEYS.ACCEPTS_SHIPPING]: table.get(TABLE_KEYS.ACCEPTS_SHIPPING),
46
+ },
40
47
  };
41
48
  const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
42
49
  const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-[var(--appkit-space-4)]";
@@ -1,5 +1,8 @@
1
1
  import React from "react";
2
+ type SearchParams = Record<string, string | string[]>;
2
3
  export interface StoreClassifiedsPageViewProps {
3
4
  storeSlug: string;
5
+ searchParams?: SearchParams;
4
6
  }
5
- export declare function StoreClassifiedsPageView({ storeSlug }: StoreClassifiedsPageViewProps): Promise<React.JSX.Element | null>;
7
+ export declare function StoreClassifiedsPageView({ storeSlug, searchParams }: StoreClassifiedsPageViewProps): Promise<React.JSX.Element | null>;
8
+ export {};