@mohasinac/appkit 4.11.1 → 4.11.3

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 (212) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  2. package/dist/_internal/server/features/checkout/actions.js +138 -27
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
  5. package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
  6. package/dist/_internal/server/features/orders/adapters.js +7 -0
  7. package/dist/_internal/server/features/products/data.d.ts +12 -3
  8. package/dist/_internal/server/features/products/data.js +55 -4
  9. package/dist/_internal/server/features/products/index.d.ts +2 -1
  10. package/dist/_internal/server/features/products/index.js +2 -1
  11. package/dist/_internal/server/features/products/list-public.d.ts +130 -0
  12. package/dist/_internal/server/features/products/list-public.js +375 -0
  13. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  14. package/dist/_internal/server/features/reviews/data.js +3 -2
  15. package/dist/_internal/server/features/tester/visibility.js +3 -3
  16. package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
  17. package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
  18. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  19. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  20. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  21. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  22. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
  23. package/dist/_internal/server/jobs/handlers/messages.js +5 -0
  24. package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
  25. package/dist/_internal/shared/checkout/lanes.js +106 -0
  26. package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
  27. package/dist/_internal/shared/checkout/order-math.js +30 -6
  28. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  29. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  30. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  31. package/dist/_internal/shared/features/reviews/config.js +8 -1
  32. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  33. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  34. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  35. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  36. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  37. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  38. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  39. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  40. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  41. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  42. package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
  43. package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
  44. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  45. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  46. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  47. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  48. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  49. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  50. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  51. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  52. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  53. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  54. package/dist/client.d.ts +7 -2
  55. package/dist/client.js +8 -2
  56. package/dist/constants/field-names.d.ts +12 -0
  57. package/dist/constants/field-names.js +12 -0
  58. package/dist/constants/table-keys.d.ts +2 -0
  59. package/dist/constants/table-keys.js +2 -0
  60. package/dist/features/account/components/NotificationBell.js +1 -0
  61. package/dist/features/account/components/UserOffersPanel.js +8 -1
  62. package/dist/features/account/components/UserOrdersView.js +27 -5
  63. package/dist/features/admin/actions/notification-actions.js +1 -1
  64. package/dist/features/admin/components/AdminAddressesView.js +3 -1
  65. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  66. package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
  67. package/dist/features/admin/components/AdminProductsView.js +34 -18
  68. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  69. package/dist/features/admin/constants/filter-tabs.js +22 -9
  70. package/dist/features/admin/schemas/firestore.d.ts +2 -1
  71. package/dist/features/admin/schemas/firestore.js +1 -0
  72. package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
  73. package/dist/features/auctions/actions/bid-actions.js +29 -15
  74. package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
  75. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  76. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  77. package/dist/features/auctions/components/AuctionsListView.js +12 -53
  78. package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
  79. package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
  80. package/dist/features/auctions/repository/bid.repository.js +19 -0
  81. package/dist/features/auctions/schemas/firestore.d.ts +11 -1
  82. package/dist/features/auctions/schemas/firestore.js +1 -0
  83. package/dist/features/cart/actions/cart-actions.d.ts +11 -0
  84. package/dist/features/cart/actions/cart-actions.js +24 -0
  85. package/dist/features/cart/repository/cart.repository.d.ts +24 -1
  86. package/dist/features/cart/repository/cart.repository.js +71 -6
  87. package/dist/features/cart/schemas/firestore.d.ts +23 -4
  88. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  89. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  90. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  91. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  92. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  93. package/dist/features/contact/email.js +56 -1
  94. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  95. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  96. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  97. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  98. package/dist/features/layout/AppLayoutShell.js +9 -2
  99. package/dist/features/layout/BottomActions.js +26 -3
  100. package/dist/features/layout/BottomActionsContext.d.ts +14 -0
  101. package/dist/features/layout/BottomActionsContext.js +13 -1
  102. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
  103. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  104. package/dist/features/layout/index.d.ts +1 -1
  105. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  106. package/dist/features/orders/repository/orders.repository.d.ts +4 -15
  107. package/dist/features/orders/repository/orders.repository.js +4 -27
  108. package/dist/features/orders/types/index.d.ts +16 -0
  109. package/dist/features/orders/utils/order-splitter.js +14 -2
  110. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  111. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  112. package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
  113. package/dist/features/products/components/ArtStickersListView.js +10 -49
  114. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  115. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  116. package/dist/features/products/components/ListingBottomActions.js +25 -0
  117. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  118. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  119. package/dist/features/products/components/ProductDetailActions.js +4 -0
  120. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  121. package/dist/features/products/components/ProductsIndexPageView.js +9 -62
  122. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  123. package/dist/features/products/constants/listing-tabs.js +71 -58
  124. package/dist/features/products/constants/sieve.d.ts +104 -1
  125. package/dist/features/products/constants/sieve.js +27 -7
  126. package/dist/features/products/repository/products.repository.d.ts +45 -1
  127. package/dist/features/products/repository/products.repository.js +67 -8
  128. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  129. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  130. package/dist/features/products/utils/listing-type.d.ts +24 -0
  131. package/dist/features/products/utils/listing-type.js +48 -0
  132. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  133. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  134. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  135. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  136. package/dist/features/promotions/actions/index.d.ts +1 -0
  137. package/dist/features/promotions/actions/index.js +1 -0
  138. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  139. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  140. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  141. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  142. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  143. package/dist/features/promotions/components/index.d.ts +2 -0
  144. package/dist/features/promotions/components/index.js +1 -0
  145. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  146. package/dist/features/promotions/constants/coupon-help.js +42 -0
  147. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  148. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  149. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  150. package/dist/features/promotions/schemas/firestore.js +1 -1
  151. package/dist/features/promotions/schemas/index.d.ts +0 -20
  152. package/dist/features/promotions/schemas/index.js +0 -1
  153. package/dist/features/reviews/actions/review-actions.js +5 -1
  154. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  155. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  156. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  157. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  158. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  159. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  160. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  161. package/dist/features/reviews/components/index.d.ts +2 -0
  162. package/dist/features/reviews/components/index.js +1 -0
  163. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  164. package/dist/features/reviews/hooks/useReviews.js +14 -6
  165. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  166. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  167. package/dist/features/reviews/types/index.d.ts +8 -1
  168. package/dist/features/seller/actions/offer-actions.d.ts +6 -0
  169. package/dist/features/seller/actions/offer-actions.js +8 -9
  170. package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
  171. package/dist/features/seller/components/SellerOffersView.js +59 -8
  172. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  173. package/dist/features/seller/components/SellerProductsView.js +21 -18
  174. package/dist/features/seller/repository/offer.repository.d.ts +18 -2
  175. package/dist/features/seller/repository/offer.repository.js +38 -2
  176. package/dist/features/seller/schemas/firestore.d.ts +7 -2
  177. package/dist/features/seller/schemas/firestore.js +3 -0
  178. package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
  179. package/dist/features/seller/schemas/offer-forms.js +28 -0
  180. package/dist/features/stores/actions/store-query-actions.js +1 -1
  181. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  182. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  183. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  184. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  185. package/dist/features/stores/schemas/index.d.ts +2 -2
  186. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  187. package/dist/features/tester/seed-data/index.d.ts +1 -0
  188. package/dist/features/tester/seed-data/index.js +1 -0
  189. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  190. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  191. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
  192. package/dist/index.d.ts +11 -4
  193. package/dist/index.js +15 -3
  194. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  195. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  196. package/dist/schemas/registry.d.ts +0 -12
  197. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  198. package/dist/seed/coupons-seed-data.js +29 -10
  199. package/dist/seed/faq-seed-data.js +2 -2
  200. package/dist/seed/manifest.js +2 -2
  201. package/dist/server-entry.d.ts +1 -0
  202. package/dist/server-entry.js +1 -0
  203. package/dist/server.d.ts +1 -0
  204. package/dist/server.js +3 -0
  205. package/dist/styles.css +44 -39
  206. package/dist/tailwind-utilities.css +1 -1
  207. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  208. package/dist/ui/components/FilterChipGroup.js +33 -4
  209. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  210. package/dist/ui/components/HorizontalScroller.js +9 -5
  211. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  212. package/package.json +1 -1
@@ -482,33 +482,6 @@ class OrderRepository extends BaseRepository {
482
482
  updatedAt: serverTimestamp(),
483
483
  });
484
484
  }
485
- /**
486
- * Cloud Functions: create an auction-won order inside a caller-owned WriteBatch.
487
- * Returns the new DocumentReference synchronously so the caller can chain further batch ops.
488
- */
489
- createFromAuction(batch, input) {
490
- const ref = this.db.collection(this.collection).doc();
491
- batch.create(ref, prepareForFirestore({
492
- id: ref.id,
493
- productId: input.productId,
494
- productTitle: input.productTitle,
495
- userId: input.userId,
496
- userName: input.userName,
497
- userEmail: input.userEmail,
498
- storeId: input.storeId ?? null,
499
- quantity: 1,
500
- unitPrice: input.amount,
501
- totalPrice: input.amount,
502
- currency: input.currency,
503
- status: OrderStatusValues.CONFIRMED,
504
- paymentStatus: "pending",
505
- orderDate: new Date(),
506
- notes: `Won via auction bid — auction product ${input.auctionProductId}`,
507
- createdAt: serverTimestamp(),
508
- updatedAt: serverTimestamp(),
509
- }));
510
- return ref;
511
- }
512
485
  }
513
486
  OrderRepository.SELLER_SIEVE_FIELDS = {
514
487
  id: { canFilter: true, canSort: false },
@@ -543,6 +516,10 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
543
516
  // S-SBUNI-RULES 2026-05-13 — refund + batch fields
544
517
  paymentBatchId: { canFilter: true, canSort: false },
545
518
  contestable: { canFilter: true, canSort: false },
519
+ // Lane segregation on /user/orders — "standard"/"auction"/"offer" etc.
520
+ // Never filtered as "standard" via Firestore == (see listForUser callers):
521
+ // orders written before this field existed have no value to match.
522
+ orderType: { canFilter: true, canSort: false },
546
523
  };
547
524
  const orderRepository = new OrderRepository();
548
525
  export { OrderRepository, orderRepository };
@@ -1,6 +1,8 @@
1
1
  import { type PaymentGateway, PaymentGatewayValues } from "../../payments/schemas";
2
2
  export { type PaymentGateway, PaymentGatewayValues };
3
3
  import type { ListingType } from "../../products/types/index";
4
+ import type { OrderType } from "../utils/order-splitter";
5
+ import type { AppliedOrderDiscount } from "../schemas/firestore";
4
6
  export type PaymentStatus = "pending" | "processing" | "paid" | "failed" | "refunded" | "partial_refund";
5
7
  export interface UserAddress {
6
8
  id: string;
@@ -57,6 +59,13 @@ export interface Order {
57
59
  address: UserAddress;
58
60
  orderStatus: OrderStatus;
59
61
  paymentStatus: PaymentStatus;
62
+ /**
63
+ * Which checkout lane produced this order. Absent on orders written before
64
+ * the field existed — treat missing as `"standard"`, never filter it out.
65
+ */
66
+ orderType?: OrderType;
67
+ /** Set when this order settled an accepted Make-an-Offer. */
68
+ offerId?: string;
60
69
  paymentGateway?: PaymentGateway;
61
70
  subtotal: number;
62
71
  shippingCost?: number;
@@ -64,7 +73,14 @@ export interface Order {
64
73
  tax?: number;
65
74
  total: number;
66
75
  currency: string;
76
+ /**
77
+ * First applied coupon's code only. Kept for back-compat; prefer
78
+ * `appliedDiscounts` when rendering, since a cart may stack one coupon per
79
+ * store plus one platform-wide coupon and this scalar shows only one of them.
80
+ */
67
81
  couponCode?: string;
82
+ /** Every discount applied to this order — the full stack, not just the first. */
83
+ appliedDiscounts?: AppliedOrderDiscount[];
68
84
  trackingNumber?: string;
69
85
  shippingCarrier?: string;
70
86
  /** Carrier tracking page URL — rendered as an external link that opens in a new tab. No live-tracking API integration; this is just the passthrough URL a seller/admin entered. */
@@ -1,4 +1,5 @@
1
- import { getListingRule, offerRule, } from "../../../_internal/shared/checkout/rules";
1
+ import { getListingRule, offerRule, auctionRule, } from "../../../_internal/shared/checkout/rules";
2
+ import { CART_LANE, laneOf } from "../../../_internal/shared/checkout/lanes";
2
3
  /**
3
4
  * Split a sequence of cart-items into order groups by `OrderType`.
4
5
  *
@@ -18,7 +19,18 @@ export function splitCartIntoOrderGroups(checks) {
18
19
  for (const check of checks) {
19
20
  const { item } = check;
20
21
  const lt = (item.listingType ?? "standard");
21
- const rule = item.isOffer ? offerRule : (getListingRule(lt) ?? getListingRule("standard"));
22
+ // Lane first, listing type second. A locked line's obligation not the
23
+ // product's listing type — decides how it settles: an accepted offer is
24
+ // always its own order, and a won auction is always its own order even
25
+ // though `auctionRule` is otherwise unreachable (auctions are
26
+ // `cartEligible: false` for user-initiated adds; the only way one reaches
27
+ // the cart is auction settlement writing the winner's locked line).
28
+ const lane = laneOf(item);
29
+ const rule = lane === CART_LANE.OFFER
30
+ ? offerRule
31
+ : lane === CART_LANE.AUCTION
32
+ ? auctionRule
33
+ : (getListingRule(lt) ?? getListingRule("standard"));
22
34
  // Prize-draw (and future types with maxLinesPerOrder < Infinity) may split
23
35
  // one cart item into N virtual batch items.
24
36
  const virtualItems = rule.splitMultipleOrders(item);
@@ -2,4 +2,4 @@ export interface PreOrderBottomActionsProps {
2
2
  price: number | null;
3
3
  currency: string;
4
4
  }
5
- export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): null;
5
+ export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): import("react").JSX.Element;
@@ -1,22 +1,6 @@
1
1
  "use client";
2
- import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
- import { formatCurrency } from "../../../utils/number.formatter";
4
- import { ACTION_ID } from "../../products/constants/action-defs";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ListingBottomActions } from "../../products/components/ListingBottomActions";
5
4
  export function PreOrderBottomActions({ price, currency, }) {
6
- useBottomActions({
7
- actions: [
8
- {
9
- id: ACTION_ID.RESERVE_NOW,
10
- label: "Reserve Now",
11
- variant: "primary",
12
- onClick: () => {
13
- document
14
- .getElementById("pre-order-buy-bar")
15
- ?.scrollIntoView({ behavior: "smooth" });
16
- },
17
- },
18
- ],
19
- infoLabel: price !== null ? formatCurrency(price, currency) : undefined,
20
- });
21
- return null;
5
+ return (_jsx(ListingBottomActions, { listingType: "preorder", anchorId: "pre-order-buy-bar", price: price, currency: currency }));
22
6
  }
@@ -1,48 +1,20 @@
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 } from "../../../ui";
5
3
  import { AdSlot } from "../../homepage/components/AdSlot";
6
- import { parseListingSearchParams } from "../../../utils/listing-params";
7
4
  import { PreOrdersIndexListing } from "./PreOrdersIndexListing";
8
- const DEFAULT_PAGE = 1;
5
+ import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
6
+ const PRE_ORDER_LISTING_TYPES = ["pre-order"];
9
7
  const DEFAULT_PAGE_SIZE = 24;
10
8
  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 buildPreOrderFilters(params) {
16
- const parts = ["status==published", "listingType==pre-order"];
17
- const minPrice = sp(params, "minPrice");
18
- const maxPrice = sp(params, "maxPrice");
19
- if (minPrice)
20
- parts.push(sieveFilter("price", SIEVE_OP.GTE, minPrice));
21
- if (maxPrice)
22
- parts.push(sieveFilter("price", SIEVE_OP.LTE, maxPrice));
23
- const store = sp(params, "storeId");
24
- if (store)
25
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, store));
26
- const preOrderProductionStatus = sp(params, "preOrderProductionStatus");
27
- if (preOrderProductionStatus)
28
- parts.push(sieveFilter("preOrderProductionStatus", SIEVE_OP.EQ, preOrderProductionStatus));
29
- // Mirror PreOrdersIndexListing's client-side default (same Root Cause pattern as
30
- // auctions' dateFrom default — SSR initialData is seeded into React Query with
31
- // staleTime:Infinity, so if this filter doesn't already exclude closed/out-of-stock
32
- // pre-orders, the client never refetches and they leak into the default view).
33
- const showClosed = sp(params, "showClosed") === "true";
34
- if (!showClosed)
35
- parts.push(sieveFilter("stockQuantity", SIEVE_OP.GT, 0));
36
- return parts.join(",");
37
- }
38
9
  export async function PreOrdersListView({ searchParams = {} }) {
39
- const std = parseListingSearchParams(searchParams);
40
- const sort = std.sorts ?? DEFAULT_SORT;
41
- const page = std.page ?? DEFAULT_PAGE;
42
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
43
- const filters = buildPreOrderFilters(searchParams);
44
- const result = await productRepository
45
- .list({ filters, sorts: sort, page, pageSize })
46
- .catch(() => null);
10
+ // Shared with /api/products. `hideSoldByDefault` covers this page's
11
+ // "Show closed" toggle the stockQuantity predicate runs in memory over a
12
+ // bounded fetch because the default sort is createdAt, not stockQuantity.
13
+ const result = await listPublicProducts(parsePublicProductParams(searchParams, {
14
+ listingTypes: PRE_ORDER_LISTING_TYPES,
15
+ pageSize: DEFAULT_PAGE_SIZE,
16
+ sorts: DEFAULT_SORT,
17
+ hideSoldByDefault: true,
18
+ }));
47
19
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Pre-Orders" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(PreOrdersIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
48
20
  }
@@ -1,62 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { productRepository } from "../../../repositories";
3
2
  import { Container, Heading, Main, Section, Text } from "../../../ui";
4
3
  import { AdSlot } from "../../homepage/components/AdSlot";
5
- import { parseListingSearchParams } from "../../../utils/listing-params";
6
4
  import { ProductsIndexListing } from "./ProductsIndexListing";
7
5
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
8
- import { sieveFilter, sieveMultiEq, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
9
6
  import { sortBy } from "../../../constants/sort";
10
7
  import { ART_STICKERS_TYPE_FILTER_TABS } from "../constants/listing-tabs";
8
+ import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
11
9
  const ART_STICKERS_LISTING_TYPES = ["art", "stickers"];
12
- const DEFAULT_PAGE = 1;
13
10
  const DEFAULT_PAGE_SIZE = 24;
14
11
  const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
15
- function sp(params, key) {
16
- const v = params[key];
17
- return Array.isArray(v) ? v[0] ?? "" : v ?? "";
18
- }
19
- function buildArtStickersFilters(params) {
20
- const listingTypeParam = sp(params, "listingType");
21
- const listingType = listingTypeParam && ART_STICKERS_LISTING_TYPES.includes(listingTypeParam)
22
- ? listingTypeParam
23
- : ART_STICKERS_LISTING_TYPES.join("|");
24
- const parts = [
25
- sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED),
26
- sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, listingType),
27
- ];
28
- const condition = sp(params, "condition");
29
- if (condition) {
30
- const values = condition.split("|").filter(Boolean);
31
- if (values.length === 1)
32
- parts.push(sieveFilter(PRODUCT_FIELDS.CONDITION, SIEVE_OP.EQ, values[0]));
33
- else if (values.length > 1)
34
- parts.push(sieveMultiEq(PRODUCT_FIELDS.CONDITION, values));
35
- }
36
- const minPrice = sp(params, "minPrice");
37
- const maxPrice = sp(params, "maxPrice");
38
- if (minPrice)
39
- parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.GTE, minPrice));
40
- if (maxPrice)
41
- parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.LTE, maxPrice));
42
- const storeId = sp(params, "seller");
43
- if (storeId)
44
- parts.push(sieveFilter(PRODUCT_FIELDS.STORE_ID, SIEVE_OP.EQ, storeId));
45
- // Mirror ProductsIndexListing's client-side "Show sold" default — see
46
- // ProductsIndexPageView.tsx's buildProductFilters for the full explanation.
47
- const showSold = sp(params, "showSold") === "true";
48
- if (!showSold)
49
- parts.push(sieveFilter(PRODUCT_FIELDS.STOCK_QUANTITY, SIEVE_OP.GT, 0));
50
- return sieveAnd(...parts);
51
- }
52
12
  export async function ArtStickersListView({ searchParams = {} }) {
53
- const std = parseListingSearchParams(searchParams);
54
- const sort = std.sorts ?? DEFAULT_SORT;
55
- const page = std.page ?? DEFAULT_PAGE;
56
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
57
- const filters = buildArtStickersFilters(searchParams);
58
- const result = await productRepository
59
- .list({ filters, sorts: sort, page, pageSize })
60
- .catch(() => null);
13
+ // Filter semantics live in `listPublicProducts`, shared verbatim with
14
+ // /api/products so the SSR first paint and the client's refetch can never
15
+ // disagree about what the default view means (Root Cause #30).
16
+ const result = await listPublicProducts(parsePublicProductParams(searchParams, {
17
+ listingTypes: ART_STICKERS_LISTING_TYPES,
18
+ pageSize: DEFAULT_PAGE_SIZE,
19
+ sorts: DEFAULT_SORT,
20
+ hideSoldByDefault: true,
21
+ }));
61
22
  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: "Art & Stickers" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Original art prints and sticker packs from independent sellers" })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: result ?? null, listingTypes: ART_STICKERS_LISTING_TYPES, typeTabs: ART_STICKERS_TYPE_FILTER_TABS, searchPlaceholder: "Search art & stickers..." }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
62
23
  }
@@ -24,7 +24,6 @@ const __O = {
24
24
  hidden: "overflow-hidden",
25
25
  };
26
26
  const DEFAULT_SORT = AUCTION_PUBLIC_SORT_OPTIONS[0].value;
27
- const AUCTION_SORT_OPTIONS = AUCTION_PUBLIC_SORT_OPTIONS;
28
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];
29
28
  export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
30
29
  const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
@@ -132,7 +131,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
132
131
  },
133
132
  },
134
133
  ] } } : {});
135
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, 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, toggles: [
134
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_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: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
136
135
  { label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") },
137
136
  ] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
138
137
  {
@@ -0,0 +1,28 @@
1
+ import { MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
2
+ /**
3
+ * The sticky-CTA registrar shared by every listing type whose bottom bar is just
4
+ * "primary CTA that scrolls to the buy/contact panel, plus a price label".
5
+ *
6
+ * Pre-order, prize-draw, classified, digital-code and live all fit that shape, which put
7
+ * it well past the Rule of Three for a per-type copy. Auction and standard product keep
8
+ * their own registrars — the auction ticks a live countdown into `secondaryLabel`, and
9
+ * the product bar carries three buttons whose labels track cart/wishlist state.
10
+ *
11
+ * Renders nothing; it exists purely to register into `BottomActionsContext`.
12
+ */
13
+ export interface ListingBottomActionsProps {
14
+ /** Selects the CTA set from the ACTIONS registry (Rule #7 — never inline actions). */
15
+ listingType: keyof typeof MOBILE_PRIMARY_ACTIONS;
16
+ /** `id` of the element the CTA scrolls to. */
17
+ anchorId: string;
18
+ price: number | null;
19
+ currency: string;
20
+ /**
21
+ * Register nothing at all — for ended/closed/sold-out listings, matching how the
22
+ * auction and prize-draw registrars bail out.
23
+ */
24
+ unavailable?: boolean;
25
+ /** Appended after the formatted price, e.g. "per entry". */
26
+ infoSuffix?: string;
27
+ }
28
+ export declare function ListingBottomActions({ listingType, anchorId, price, currency, unavailable, infoSuffix, }: ListingBottomActionsProps): null;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
+ import { formatCurrency } from "../../../utils/number.formatter";
4
+ import { ACTION_META, MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
5
+ export function ListingBottomActions({ listingType, anchorId, price, currency, unavailable = false, infoSuffix, }) {
6
+ const actionIds = MOBILE_PRIMARY_ACTIONS[listingType] ?? [];
7
+ const infoLabel = price !== null
8
+ ? `${formatCurrency(price, currency)}${infoSuffix ? ` ${infoSuffix}` : ""}`
9
+ : undefined;
10
+ useBottomActions(unavailable
11
+ ? {}
12
+ : {
13
+ actions: actionIds.map((id) => ({
14
+ ...ACTION_META[id],
15
+ onClick: () => {
16
+ document
17
+ .getElementById(anchorId)
18
+ ?.scrollIntoView({ behavior: "smooth" });
19
+ },
20
+ })),
21
+ infoLabel,
22
+ desktop: "after-scroll",
23
+ });
24
+ return null;
25
+ }
@@ -3,4 +3,4 @@ export interface PrizeDrawBottomActionsProps {
3
3
  currency: string;
4
4
  closed: boolean;
5
5
  }
6
- export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): null;
6
+ export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): import("react").JSX.Element;
@@ -1,24 +1,6 @@
1
1
  "use client";
2
- import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
- import { formatCurrency } from "../../../utils/number.formatter";
4
- import { ACTION_ID } from "../constants/action-defs";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ListingBottomActions } from "./ListingBottomActions";
5
4
  export function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }) {
6
- useBottomActions(closed
7
- ? {}
8
- : {
9
- actions: [
10
- {
11
- id: ACTION_ID.BUY_NOW,
12
- label: "Buy Now",
13
- variant: "primary",
14
- onClick: () => {
15
- document
16
- .getElementById("prize-draw-buy-bar")
17
- ?.scrollIntoView({ behavior: "smooth" });
18
- },
19
- },
20
- ],
21
- infoLabel: `${formatCurrency(pricePerEntry, currency)} per entry`,
22
- });
23
- return null;
5
+ return (_jsx(ListingBottomActions, { listingType: "prize-draw", anchorId: "prize-draw-buy-bar", price: pricePerEntry, currency: currency, unavailable: closed, infoSuffix: "per entry" }));
24
6
  }
@@ -122,6 +122,10 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
122
122
  infoLabel: price != null && currency
123
123
  ? formatCurrency(price, currency)
124
124
  : undefined,
125
+ // The bar itself owns responsive visibility now, so this one registrar serves
126
+ // both breakpoints. The desktop branch below must NOT also register —
127
+ // useBottomActions is last-mounted-wins and the two would clobber each other.
128
+ desktop: "after-scroll",
125
129
  }
126
130
  : {});
127
131
  const authModal = (_jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
- import { productRepository, reviewRepository } from "../../../repositories";
3
+ import { productRepository } from "../../../repositories";
4
4
  const __P = {
5
5
  p3: "p-[var(--appkit-space-3)]",
6
6
  p5: "p-[var(--appkit-space-5)]",
@@ -25,7 +25,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
25
25
  import { Button, Container, Div, Heading, Main, Nav, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, Dl, Dt, Dd, } from "../../../ui";
26
26
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
27
27
  import { safeDisplayName } from "../../../security";
28
- import { ReviewsList } from "../../reviews/components/ReviewsList";
28
+ import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
29
29
  import { ProductDetailView } from "./ProductDetailView";
30
30
  import { ProductGalleryClient } from "./ProductGalleryClient";
31
31
  import { ProductTabsShell } from "./ProductTabsShell";
@@ -37,7 +37,7 @@ import { SublistingCarouselSection } from "./SublistingCarouselSection";
37
37
  import { ShowGroupSection } from "./ShowGroupSection";
38
38
  import { HistoryTracker } from "../../history/components/HistoryTracker";
39
39
  import { RelatedItemsSection } from "./RelatedItemsSection";
40
- import { computeRelatedItems, toReview } from "../../../_internal/server/features/products/data";
40
+ import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
41
41
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
42
42
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
43
43
  // ---------------------------------------------------------------------------
@@ -165,12 +165,12 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
165
165
  const groupTitle = typeof p.groupTitle === "string" ? p.groupTitle : undefined;
166
166
  const storeId = typeof p.storeId === "string" ? p.storeId : null;
167
167
  // -- Fetch reviews + the shared 4-signal related-items computation + grouped listings in parallel --
168
- const [reviewDocs, related, groups] = await Promise.all([
169
- reviewRepository.findApprovedByProduct(product.id).catch(() => []),
168
+ // Only page 1 of reviews is fetched here; <ReviewsListingPanel> pages the rest client-side.
169
+ const [initialReviews, related, groups] = await Promise.all([
170
+ getReviewPageForProduct(product.id),
170
171
  computeRelatedItems(product),
171
172
  getGroupsWithItemsForProduct(product.id),
172
173
  ]);
173
- const reviews = reviewDocs.map(toReview);
174
174
  const { relatedItems, relatedByBrand, relatedByTags, relatedByStore } = related;
175
175
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: product.id, productType: "product", snapshot: {
176
176
  title: product.title,
@@ -231,7 +231,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
231
231
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
232
232
  : undefined, renderGroupSection: groupId
233
233
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
234
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: allSpecs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", 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) => ({
234
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: allSpecs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
235
235
  id: s.id,
236
236
  label: s.title,
237
237
  content: _jsx(CustomSectionTabContent, { section: s }),
@@ -1,75 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { productRepository } from "../../../repositories";
3
2
  import { ROUTES } from "../../../constants";
4
3
  import { Container, Div, Heading, Main, Section, Text, TextLink } from "../../../ui";
5
4
  import { AdSlot } from "../../homepage/components/AdSlot";
6
- import { parseListingSearchParams } from "../../../utils/listing-params";
7
5
  import { ProductsIndexListing } from "./ProductsIndexListing";
8
6
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
9
- import { sieveFilter, sieveMultiEq, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
10
7
  import { sortBy } from "../../../constants/sort";
11
8
  import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs";
12
- const DEFAULT_PAGE = 1;
9
+ import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
13
10
  const DEFAULT_PAGE_SIZE = 24;
14
11
  const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
15
- function sp(params, key) {
16
- const v = params[key];
17
- return Array.isArray(v) ? v[0] ?? "" : v ?? "";
18
- }
19
- function buildProductFilters(params) {
20
- const listingTypeParam = sp(params, "listingType");
21
- const listingType = listingTypeParam && GENERIC_PRODUCT_LISTING_TYPES.includes(listingTypeParam)
22
- ? listingTypeParam
23
- : GENERIC_PRODUCT_LISTING_TYPES.join("|");
24
- const parts = [
25
- sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED),
26
- sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, listingType),
27
- ];
28
- const condition = sp(params, "condition");
29
- if (condition) {
30
- const values = condition.split("|").filter(Boolean);
31
- if (values.length === 1)
32
- parts.push(sieveFilter(PRODUCT_FIELDS.CONDITION, SIEVE_OP.EQ, values[0]));
33
- // BUG FIX: pipe is invalid for ==; expand to multiple AND clauses
34
- else if (values.length > 1)
35
- parts.push(sieveMultiEq(PRODUCT_FIELDS.CONDITION, values));
36
- }
37
- const minPrice = sp(params, "minPrice");
38
- const maxPrice = sp(params, "maxPrice");
39
- if (minPrice)
40
- parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.GTE, minPrice));
41
- if (maxPrice)
42
- parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.LTE, maxPrice));
43
- const storeId = sp(params, "seller");
44
- if (storeId)
45
- parts.push(sieveFilter(PRODUCT_FIELDS.STORE_ID, SIEVE_OP.EQ, storeId));
46
- const freeShipping = sp(params, "freeShipping");
47
- if (freeShipping === "true") {
48
- parts.push(sieveFilter(PRODUCT_FIELDS.SHIPPING_PAID_BY, SIEVE_OP.EQ, PRODUCT_FIELDS.SHIPPING_PAID_BY_VALUES.SELLER));
49
- }
50
- // Mirror ProductsIndexListing's client-side "Show sold" default (same Root Cause
51
- // pattern as auctions' dateFrom default — SSR initialData is seeded into React
52
- // Query with staleTime:Infinity, so if this filter doesn't already exclude sold /
53
- // out-of-stock products, the client never refetches and they leak into the default view).
54
- const showSold = sp(params, "showSold") === "true";
55
- if (!showSold)
56
- parts.push(sieveFilter(PRODUCT_FIELDS.STOCK_QUANTITY, SIEVE_OP.GT, 0));
57
- return sieveAnd(...parts);
58
- }
59
12
  export async function ProductsIndexPageView({ searchParams = {} }) {
60
- const std = parseListingSearchParams(searchParams);
61
- const sort = std.sorts ?? DEFAULT_SORT;
62
- const page = std.page ?? DEFAULT_PAGE;
63
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
64
- const filters = buildProductFilters(searchParams);
65
- const result = await productRepository
66
- .list({
67
- filters,
68
- sorts: sort,
69
- page,
70
- pageSize,
71
- })
72
- .catch(() => null);
73
- const products = result ?? null;
13
+ // See ArtStickersListView — one shared filter implementation with
14
+ // /api/products, so SSR and the client refetch agree by construction.
15
+ const products = await listPublicProducts(parsePublicProductParams(searchParams, {
16
+ listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
17
+ pageSize: DEFAULT_PAGE_SIZE,
18
+ sorts: DEFAULT_SORT,
19
+ hideSoldByDefault: true,
20
+ }));
74
21
  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" })] })] }));
75
22
  }