@mohasinac/appkit 4.11.2 → 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 (165) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  2. package/dist/_internal/server/features/checkout/actions.js +111 -4
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/orders/adapters.js +7 -0
  5. package/dist/_internal/server/features/products/data.d.ts +12 -3
  6. package/dist/_internal/server/features/products/data.js +55 -4
  7. package/dist/_internal/server/features/products/index.d.ts +1 -1
  8. package/dist/_internal/server/features/products/index.js +1 -1
  9. package/dist/_internal/server/features/products/list-public.d.ts +19 -0
  10. package/dist/_internal/server/features/products/list-public.js +39 -6
  11. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  12. package/dist/_internal/server/features/reviews/data.js +3 -2
  13. package/dist/_internal/server/features/tester/visibility.js +3 -3
  14. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  16. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  17. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  18. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  19. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  20. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  21. package/dist/_internal/shared/features/reviews/config.js +8 -1
  22. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  23. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  24. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  25. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  26. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  27. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  28. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  29. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  30. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  31. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  32. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  33. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  34. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  35. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  36. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  37. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  38. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  39. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  40. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  41. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  42. package/dist/client.d.ts +5 -1
  43. package/dist/client.js +4 -1
  44. package/dist/constants/field-names.d.ts +12 -0
  45. package/dist/constants/field-names.js +12 -0
  46. package/dist/constants/table-keys.d.ts +2 -0
  47. package/dist/constants/table-keys.js +2 -0
  48. package/dist/features/account/components/UserOrdersView.js +27 -5
  49. package/dist/features/admin/components/AdminAddressesView.js +3 -1
  50. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  51. package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
  52. package/dist/features/admin/components/AdminProductsView.js +34 -18
  53. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  54. package/dist/features/admin/constants/filter-tabs.js +22 -9
  55. package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
  56. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  58. package/dist/features/cart/schemas/firestore.d.ts +0 -2
  59. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  60. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  61. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  62. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  63. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  64. package/dist/features/contact/email.js +5 -1
  65. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  66. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  67. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  68. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  69. package/dist/features/layout/AppLayoutShell.js +9 -2
  70. package/dist/features/layout/BottomActions.js +26 -3
  71. package/dist/features/layout/BottomActionsContext.d.ts +14 -0
  72. package/dist/features/layout/BottomActionsContext.js +13 -1
  73. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
  74. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  75. package/dist/features/layout/index.d.ts +1 -1
  76. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  77. package/dist/features/orders/repository/orders.repository.d.ts +4 -0
  78. package/dist/features/orders/repository/orders.repository.js +4 -0
  79. package/dist/features/orders/types/index.d.ts +16 -0
  80. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  81. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  82. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  83. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  84. package/dist/features/products/components/ListingBottomActions.js +25 -0
  85. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  86. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  87. package/dist/features/products/components/ProductDetailActions.js +4 -0
  88. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  89. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  90. package/dist/features/products/constants/listing-tabs.js +71 -58
  91. package/dist/features/products/constants/sieve.d.ts +104 -1
  92. package/dist/features/products/constants/sieve.js +27 -7
  93. package/dist/features/products/repository/products.repository.d.ts +45 -1
  94. package/dist/features/products/repository/products.repository.js +26 -3
  95. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  96. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  97. package/dist/features/products/utils/listing-type.d.ts +24 -0
  98. package/dist/features/products/utils/listing-type.js +48 -0
  99. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  100. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  101. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  102. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  103. package/dist/features/promotions/actions/index.d.ts +1 -0
  104. package/dist/features/promotions/actions/index.js +1 -0
  105. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  106. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  107. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  108. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  109. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  110. package/dist/features/promotions/components/index.d.ts +2 -0
  111. package/dist/features/promotions/components/index.js +1 -0
  112. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  113. package/dist/features/promotions/constants/coupon-help.js +42 -0
  114. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  115. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  116. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  117. package/dist/features/promotions/schemas/firestore.js +1 -1
  118. package/dist/features/promotions/schemas/index.d.ts +0 -20
  119. package/dist/features/promotions/schemas/index.js +0 -1
  120. package/dist/features/reviews/actions/review-actions.js +5 -1
  121. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  122. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  123. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  124. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  125. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  126. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  127. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  128. package/dist/features/reviews/components/index.d.ts +2 -0
  129. package/dist/features/reviews/components/index.js +1 -0
  130. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  131. package/dist/features/reviews/hooks/useReviews.js +14 -6
  132. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  133. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  134. package/dist/features/reviews/types/index.d.ts +8 -1
  135. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  136. package/dist/features/seller/components/SellerProductsView.js +21 -18
  137. package/dist/features/stores/actions/store-query-actions.js +1 -1
  138. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  139. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  140. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  141. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  142. package/dist/features/stores/schemas/index.d.ts +2 -2
  143. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  144. package/dist/features/tester/seed-data/index.d.ts +1 -0
  145. package/dist/features/tester/seed-data/index.js +1 -0
  146. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  147. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  148. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
  149. package/dist/index.d.ts +9 -3
  150. package/dist/index.js +11 -2
  151. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  152. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  153. package/dist/schemas/registry.d.ts +0 -12
  154. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  155. package/dist/seed/coupons-seed-data.js +29 -10
  156. package/dist/seed/faq-seed-data.js +2 -2
  157. package/dist/seed/manifest.js +2 -2
  158. package/dist/styles.css +44 -39
  159. package/dist/tailwind-utilities.css +1 -1
  160. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  161. package/dist/ui/components/FilterChipGroup.js +33 -4
  162. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  163. package/dist/ui/components/HorizontalScroller.js +9 -5
  164. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  165. package/package.json +1 -1
@@ -376,7 +376,7 @@ export class CouponsRepository extends BaseRepository {
376
376
  * `eligibleSubtotal` (the order slice the discount is based on)
377
377
  * and `eligibleProductIds` (the `productId` values that qualify).
378
378
  */
379
- async validateCouponForCart(code, userId, cartItems) {
379
+ async validateCouponForCart(code, userId, cartItems, opts) {
380
380
  const check = await this._fetchAndValidateCoupon(code, userId);
381
381
  if (!check.ok)
382
382
  return check.result;
@@ -400,25 +400,39 @@ export class CouponsRepository extends BaseRepository {
400
400
  eligible = eligible.filter((item) => item.listingType !== "auction");
401
401
  }
402
402
  // Apply product/category restrictions from the coupon itself
403
- const { applicableProducts, applicableCategories, excludeProducts } = coupon.restrictions;
403
+ const { applicableProducts, applicableCategories, excludeProducts, excludeCategories, } = coupon.restrictions;
404
404
  if (applicableProducts && applicableProducts.length > 0) {
405
405
  eligible = eligible.filter((item) => applicableProducts.includes(item.productId));
406
406
  }
407
407
  if (excludeProducts && excludeProducts.length > 0) {
408
408
  eligible = eligible.filter((item) => !excludeProducts.includes(item.productId));
409
409
  }
410
- // Note: category filtering requires product metadata — skipped here but
411
- // the restriction is stored and can be enforced by the order service.
412
- void applicableCategories;
410
+ // Category restrictions need product metadata the cart doesn't snapshot,
411
+ // so they are resolved through the injected callback. A product carries
412
+ // `categorySlugs[]` (1-to-many), so the test is set intersection.
413
+ const needsCategories = (applicableCategories?.length ?? 0) > 0 ||
414
+ (excludeCategories?.length ?? 0) > 0;
415
+ if (needsCategories && eligible.length > 0 && opts?.resolveCategorySlugs) {
416
+ const slugsByProduct = await opts.resolveCategorySlugs(eligible.map((item) => item.productId));
417
+ const slugsFor = (productId) => slugsByProduct.get(productId) ?? [];
418
+ if (applicableCategories && applicableCategories.length > 0) {
419
+ eligible = eligible.filter((item) => slugsFor(item.productId).some((slug) => applicableCategories.includes(slug)));
420
+ }
421
+ if (excludeCategories && excludeCategories.length > 0) {
422
+ eligible = eligible.filter((item) => !slugsFor(item.productId).some((slug) => excludeCategories.includes(slug)));
423
+ }
424
+ }
413
425
  if (eligible.length === 0) {
414
426
  return {
415
427
  valid: false,
416
428
  coupon,
417
429
  message: coupon.applicableToAuctions === true
418
430
  ? "This coupon only applies to auction items"
419
- : scope === "seller"
420
- ? "This coupon is not valid for the items in your cart from this seller"
421
- : "No eligible items found for this coupon",
431
+ : needsCategories
432
+ ? "This coupon does not apply to any of the categories in your cart"
433
+ : scope === "seller"
434
+ ? "This coupon is not valid for the items in your cart from this seller"
435
+ : "No eligible items found for this coupon",
422
436
  };
423
437
  }
424
438
  const eligibleSubtotal = eligible.reduce((sum, item) => sum + item.price * item.quantity, 0);
@@ -37,7 +37,6 @@ export interface RestrictionsConfig {
37
37
  excludeProducts?: string[];
38
38
  excludeCategories?: string[];
39
39
  firstTimeUserOnly: boolean;
40
- combineWithSellerCoupons: boolean;
41
40
  }
42
41
  export interface CouponStats {
43
42
  totalUses: number;
@@ -62,7 +62,7 @@ export const COUPON_TYPE_LABELS = {
62
62
  export const DEFAULT_COUPON_DATA = {
63
63
  usage: { currentUsage: 0 },
64
64
  validity: { isActive: true, startDate: new Date(), endDate: undefined },
65
- restrictions: { firstTimeUserOnly: false, combineWithSellerCoupons: false },
65
+ restrictions: { firstTimeUserOnly: false },
66
66
  stats: { totalUses: 0, totalRevenue: 0, totalDiscount: 0 },
67
67
  };
68
68
  export const COUPONS_PUBLIC_FIELDS = [
@@ -93,10 +93,8 @@ export declare const restrictionsConfigSchema: z.ZodObject<{
93
93
  excludeProducts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
94
94
  excludeCategories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
95
95
  firstTimeUserOnly: z.ZodBoolean;
96
- combineWithSellerCoupons: z.ZodBoolean;
97
96
  }, "strip", z.ZodTypeAny, {
98
97
  firstTimeUserOnly: boolean;
99
- combineWithSellerCoupons: boolean;
100
98
  applicableProducts?: string[] | undefined;
101
99
  applicableCategories?: string[] | undefined;
102
100
  applicableSellers?: string[] | undefined;
@@ -104,7 +102,6 @@ export declare const restrictionsConfigSchema: z.ZodObject<{
104
102
  excludeCategories?: string[] | undefined;
105
103
  }, {
106
104
  firstTimeUserOnly: boolean;
107
- combineWithSellerCoupons: boolean;
108
105
  applicableProducts?: string[] | undefined;
109
106
  applicableCategories?: string[] | undefined;
110
107
  applicableSellers?: string[] | undefined;
@@ -239,10 +236,8 @@ export declare const couponFirestoreSchema: z.ZodObject<{
239
236
  excludeProducts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
240
237
  excludeCategories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
241
238
  firstTimeUserOnly: z.ZodBoolean;
242
- combineWithSellerCoupons: z.ZodBoolean;
243
239
  }, "strip", z.ZodTypeAny, {
244
240
  firstTimeUserOnly: boolean;
245
- combineWithSellerCoupons: boolean;
246
241
  applicableProducts?: string[] | undefined;
247
242
  applicableCategories?: string[] | undefined;
248
243
  applicableSellers?: string[] | undefined;
@@ -250,7 +245,6 @@ export declare const couponFirestoreSchema: z.ZodObject<{
250
245
  excludeCategories?: string[] | undefined;
251
246
  }, {
252
247
  firstTimeUserOnly: boolean;
253
- combineWithSellerCoupons: boolean;
254
248
  applicableProducts?: string[] | undefined;
255
249
  applicableCategories?: string[] | undefined;
256
250
  applicableSellers?: string[] | undefined;
@@ -311,7 +305,6 @@ export declare const couponFirestoreSchema: z.ZodObject<{
311
305
  };
312
306
  restrictions: {
313
307
  firstTimeUserOnly: boolean;
314
- combineWithSellerCoupons: boolean;
315
308
  applicableProducts?: string[] | undefined;
316
309
  applicableCategories?: string[] | undefined;
317
310
  applicableSellers?: string[] | undefined;
@@ -370,7 +363,6 @@ export declare const couponFirestoreSchema: z.ZodObject<{
370
363
  };
371
364
  restrictions: {
372
365
  firstTimeUserOnly: boolean;
373
- combineWithSellerCoupons: boolean;
374
366
  applicableProducts?: string[] | undefined;
375
367
  applicableCategories?: string[] | undefined;
376
368
  applicableSellers?: string[] | undefined;
@@ -450,10 +442,8 @@ export declare const claimedCouponSnapshotSchema: z.ZodObject<{
450
442
  excludeProducts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
451
443
  excludeCategories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
452
444
  firstTimeUserOnly: z.ZodBoolean;
453
- combineWithSellerCoupons: z.ZodBoolean;
454
445
  }, "strip", z.ZodTypeAny, {
455
446
  firstTimeUserOnly: boolean;
456
- combineWithSellerCoupons: boolean;
457
447
  applicableProducts?: string[] | undefined;
458
448
  applicableCategories?: string[] | undefined;
459
449
  applicableSellers?: string[] | undefined;
@@ -461,7 +451,6 @@ export declare const claimedCouponSnapshotSchema: z.ZodObject<{
461
451
  excludeCategories?: string[] | undefined;
462
452
  }, {
463
453
  firstTimeUserOnly: boolean;
464
- combineWithSellerCoupons: boolean;
465
454
  applicableProducts?: string[] | undefined;
466
455
  applicableCategories?: string[] | undefined;
467
456
  applicableSellers?: string[] | undefined;
@@ -479,7 +468,6 @@ export declare const claimedCouponSnapshotSchema: z.ZodObject<{
479
468
  };
480
469
  restrictions: {
481
470
  firstTimeUserOnly: boolean;
482
- combineWithSellerCoupons: boolean;
483
471
  applicableProducts?: string[] | undefined;
484
472
  applicableCategories?: string[] | undefined;
485
473
  applicableSellers?: string[] | undefined;
@@ -499,7 +487,6 @@ export declare const claimedCouponSnapshotSchema: z.ZodObject<{
499
487
  };
500
488
  restrictions: {
501
489
  firstTimeUserOnly: boolean;
502
- combineWithSellerCoupons: boolean;
503
490
  applicableProducts?: string[] | undefined;
504
491
  applicableCategories?: string[] | undefined;
505
492
  applicableSellers?: string[] | undefined;
@@ -541,10 +528,8 @@ export declare const claimedCouponFirestoreSchema: z.ZodObject<{
541
528
  excludeProducts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
542
529
  excludeCategories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
543
530
  firstTimeUserOnly: z.ZodBoolean;
544
- combineWithSellerCoupons: z.ZodBoolean;
545
531
  }, "strip", z.ZodTypeAny, {
546
532
  firstTimeUserOnly: boolean;
547
- combineWithSellerCoupons: boolean;
548
533
  applicableProducts?: string[] | undefined;
549
534
  applicableCategories?: string[] | undefined;
550
535
  applicableSellers?: string[] | undefined;
@@ -552,7 +537,6 @@ export declare const claimedCouponFirestoreSchema: z.ZodObject<{
552
537
  excludeCategories?: string[] | undefined;
553
538
  }, {
554
539
  firstTimeUserOnly: boolean;
555
- combineWithSellerCoupons: boolean;
556
540
  applicableProducts?: string[] | undefined;
557
541
  applicableCategories?: string[] | undefined;
558
542
  applicableSellers?: string[] | undefined;
@@ -570,7 +554,6 @@ export declare const claimedCouponFirestoreSchema: z.ZodObject<{
570
554
  };
571
555
  restrictions: {
572
556
  firstTimeUserOnly: boolean;
573
- combineWithSellerCoupons: boolean;
574
557
  applicableProducts?: string[] | undefined;
575
558
  applicableCategories?: string[] | undefined;
576
559
  applicableSellers?: string[] | undefined;
@@ -590,7 +573,6 @@ export declare const claimedCouponFirestoreSchema: z.ZodObject<{
590
573
  };
591
574
  restrictions: {
592
575
  firstTimeUserOnly: boolean;
593
- combineWithSellerCoupons: boolean;
594
576
  applicableProducts?: string[] | undefined;
595
577
  applicableCategories?: string[] | undefined;
596
578
  applicableSellers?: string[] | undefined;
@@ -651,7 +633,6 @@ export declare const claimedCouponFirestoreSchema: z.ZodObject<{
651
633
  };
652
634
  restrictions: {
653
635
  firstTimeUserOnly: boolean;
654
- combineWithSellerCoupons: boolean;
655
636
  applicableProducts?: string[] | undefined;
656
637
  applicableCategories?: string[] | undefined;
657
638
  applicableSellers?: string[] | undefined;
@@ -692,7 +673,6 @@ export declare const claimedCouponFirestoreSchema: z.ZodObject<{
692
673
  };
693
674
  restrictions: {
694
675
  firstTimeUserOnly: boolean;
695
- combineWithSellerCoupons: boolean;
696
676
  applicableProducts?: string[] | undefined;
697
677
  applicableCategories?: string[] | undefined;
698
678
  applicableSellers?: string[] | undefined;
@@ -45,7 +45,6 @@ export const restrictionsConfigSchema = z.object({
45
45
  excludeProducts: z.array(z.string()).optional(),
46
46
  excludeCategories: z.array(z.string()).optional(),
47
47
  firstTimeUserOnly: z.boolean(),
48
- combineWithSellerCoupons: z.boolean(),
49
48
  });
50
49
  export const couponStatsSchema = z.object({
51
50
  totalUses: z.number().int().nonnegative(),
@@ -174,9 +174,13 @@ export async function listReviewsBySeller(sellerId) {
174
174
  }
175
175
  const reviewBatches = await Promise.all(productIds
176
176
  .slice(0, 20)
177
- .map((id) => reviewRepository.findApprovedByProduct(id).catch(() => [])));
177
+ .map((id) => reviewRepository.findApprovedByProduct(id, 50).catch(() => [])));
178
+ // Each per-product batch is newest-first, but concatenating them yields product order,
179
+ // not chronological order — the flattened list has to be re-sorted or callers get an
180
+ // arbitrarily interleaved "latest reviews" list.
178
181
  return reviewBatches
179
182
  .flat()
183
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
180
184
  .map((item) => maskPublicReview(item));
181
185
  }
182
186
  export async function getHomepageReviews() {
@@ -14,7 +14,8 @@ export async function ReviewDetailPageView({ id }) {
14
14
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-6xl", children: _jsx(Container, { size: "sm", children: _jsxs(Div, { className: "text-left", children: [_jsx(Text, { className: "mb-4", size: "5xl", "aria-hidden": "true", children: "\uD83D\uDD0D" }), _jsx(Heading, { color: "inverse", level: 1, className: "text-[var(--appkit-color-text)] dark: mb-2", size: "2xl", weight: "bold", children: "Review not found" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mb-6", size: "sm", children: "This review may have been removed or is no longer available." }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "inline-flex items-center gap-[var(--appkit-space-2)] rounded-lg bg-primary px-[var(--appkit-space-5)] py-[var(--appkit-space-2-5)] text-[length:var(--appkit-text-sm)] font-medium text-white hover:bg-primary-600 transition-colors", children: "\u2190 Back to Reviews" })] }) }) }) }));
15
15
  }
16
16
  const [sameProductDocs, sameStoreDocs] = await Promise.all([
17
- reviewRepository.findApprovedByProduct(review.productId).catch(() => []),
17
+ // 12 = the 6 rendered below, with headroom for the current review + any filtered out.
18
+ reviewRepository.findApprovedByProduct(review.productId, 12).catch(() => []),
18
19
  storeSlug ? reviewRepository.findApprovedByStore(storeSlug).catch(() => []) : Promise.resolve([]),
19
20
  ]);
20
21
  const sameProductReviews = sameProductDocs.filter((r) => r.id !== review.id).slice(0, 6);
@@ -70,5 +70,11 @@ export interface ReviewFiltersProps {
70
70
  variant?: ReviewFilterVariant;
71
71
  /** Optional brand options for filter by brand */
72
72
  brandOptions?: FacetOption[];
73
+ /**
74
+ * Restrict which filter controls render. Callers that only *apply* a subset must pass
75
+ * it here — a rendered control whose value the caller never reads is a silent no-op,
76
+ * which reads to the user as a broken filter. Omit to keep the variant's full set.
77
+ */
78
+ keys?: readonly string[];
73
79
  }
74
- export declare function ReviewFilters({ table, variant, brandOptions, }: ReviewFiltersProps): import("react").JSX.Element;
80
+ export declare function ReviewFilters({ table, variant, brandOptions, keys, }: ReviewFiltersProps): import("react").JSX.Element;
@@ -38,7 +38,8 @@ export function getReviewSortOptions(variant) {
38
38
  return REVIEW_PUBLIC_SORT_OPTIONS;
39
39
  }
40
40
  }
41
- export function ReviewFilters({ table, variant = "admin", brandOptions, }) {
41
+ export function ReviewFilters({ table, variant = "admin", brandOptions, keys, }) {
42
+ const shows = (key) => (keys ? keys.includes(key) : true);
42
43
  const t = useTranslations("filters");
43
44
  const statusOptions = [
44
45
  { value: "pending", label: t("reviewStatusPending") },
@@ -59,6 +60,6 @@ export function ReviewFilters({ table, variant = "admin", brandOptions, }) {
59
60
  ? table.get("rating").split("|").filter(Boolean)
60
61
  : [];
61
62
  const isAdmin = variant === "admin";
62
- const showStatus = variant !== "public";
63
- return (_jsxs(Div, { children: [showStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions, selected: selectedStatus, onChange: (vals) => table.set("status", vals.join("|")), searchable: false, defaultCollapsed: false })), _jsx(FilterFacetSection, { title: t("rating"), options: ratingOptions, selected: selectedRating, onChange: (vals) => table.set("rating", vals.join("|")), searchable: false, defaultCollapsed: variant === "admin" }), brandOptions && brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: table.get("brand") ? [table.get("brand")] : [], onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 6, selectionMode: "single", defaultCollapsed: true })), _jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: true }), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : "") }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : "") })] })), _jsx(RangeFilter, { title: t("votesRange"), minValue: table.get("minVotes"), maxValue: table.get("maxVotes"), onMinChange: (v) => table.set("minVotes", v), onMaxChange: (v) => table.set("maxVotes", v), minBound: 0, maxBound: 10000, step: 1, minPlaceholder: t("minVotes"), maxPlaceholder: t("maxVotes"), defaultCollapsed: true }), _jsx(SwitchFilter, { title: "Media", label: "Show reviews with photos only", checked: table.get("hasImages") === "true", onChange: (v) => table.set("hasImages", v ? "true" : "") })] }));
63
+ const showStatus = variant !== "public" && shows(TABLE_KEYS.STATUS);
64
+ return (_jsxs(Div, { children: [showStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions, selected: selectedStatus, onChange: (vals) => table.set("status", vals.join("|")), searchable: false, defaultCollapsed: false })), shows(TABLE_KEYS.RATING) && (_jsx(FilterFacetSection, { title: t("rating"), options: ratingOptions, selected: selectedRating, onChange: (vals) => table.set("rating", vals.join("|")), searchable: false, defaultCollapsed: variant === "admin" })), shows(TABLE_KEYS.BRAND) && brandOptions && brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: table.get("brand") ? [table.get("brand")] : [], onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 6, selectionMode: "single", defaultCollapsed: true })), shows(TABLE_KEYS.DATE_FROM) && (_jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: true })), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : "") }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : "") })] })), shows("minVotes") && (_jsx(RangeFilter, { title: t("votesRange"), minValue: table.get("minVotes"), maxValue: table.get("maxVotes"), onMinChange: (v) => table.set("minVotes", v), onMaxChange: (v) => table.set("maxVotes", v), minBound: 0, maxBound: 10000, step: 1, minPlaceholder: t("minVotes"), maxPlaceholder: t("maxVotes"), defaultCollapsed: true })), shows("hasImages") && (_jsx(SwitchFilter, { title: "Media", label: "Show reviews with photos only", checked: table.get("hasImages") === "true", onChange: (v) => table.set("hasImages", v ? "true" : "") }))] }));
64
65
  }
@@ -4,4 +4,10 @@ export interface ReviewsIndexListingProps {
4
4
  initialData?: ReviewListResponse;
5
5
  variant?: "admin" | "seller" | "public";
6
6
  }
7
+ /**
8
+ * The public `/reviews` index. Everything below the props is `ReviewsListingPanel` —
9
+ * this file and `StoreReviewsListing` used to be near-identical copies of the same
10
+ * toolbar + sticky pagination + filter drawer + card grid, and the detail-page reviews
11
+ * tab would have been a third. See the Duplication Decision Framework (Rule of Three).
12
+ */
7
13
  export declare function ReviewsIndexListing({ initialData, variant, }: ReviewsIndexListingProps): React.JSX.Element;
@@ -1,112 +1,12 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useCallback, useMemo } from "react";
4
- import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
- import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
6
- import { ReviewCard } from "./ReviewsList";
7
- import { ReviewFilters, REVIEW_PUBLIC_SORT_OPTIONS } from "./ReviewFilters";
8
- import { useReviews } from "../hooks/useReviews";
9
- import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
10
- import { sortBy } from "../../../constants/sort";
11
- import { REVIEW_FIELDS } from "../../../constants/field-names";
12
- const __P = {
13
- p4: "p-[var(--appkit-space-4)]",
14
- };
15
- const __O = {
16
- hidden: "overflow-hidden",
17
- };
18
- const PAGE_SIZE = 12;
19
- const DEFAULT_SORT = sortBy(REVIEW_FIELDS.CREATED_AT);
20
- const FILTER_KEYS = ["rating", TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO, "minVotes", "maxVotes", "hasImages"];
21
- const SORT_OPTION_LABELS = {
22
- sortNewest: "Newest First",
23
- sortOldest: "Oldest First",
24
- sortHighestRated: "Highest Rated",
25
- sortLowestRated: "Lowest Rated",
26
- };
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ReviewsListingPanel } from "./ReviewsListingPanel";
4
+ /**
5
+ * The public `/reviews` index. Everything below the props is `ReviewsListingPanel`
6
+ * this file and `StoreReviewsListing` used to be near-identical copies of the same
7
+ * toolbar + sticky pagination + filter drawer + card grid, and the detail-page reviews
8
+ * tab would have been a third. See the Duplication Decision Framework (Rule of Three).
9
+ */
27
10
  export function ReviewsIndexListing({ initialData, variant = "public", }) {
28
- const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
29
- const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
30
- const [filterOpen, setFilterOpen] = useState(false);
31
- const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
32
- const handleViewToggle = (next) => {
33
- if (next === VIEW_MODE.TABLE)
34
- return;
35
- setView(next);
36
- table.set(TABLE_KEYS.VIEW, next);
37
- };
38
- const sort = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
39
- const currentPage = table.getNumber(TABLE_KEYS.PAGE, 1);
40
- // Pending filter state — buffered until "Apply Filters" clicked
41
- const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
42
- const pendingTable = useMemo(() => ({
43
- get: (key) => pendingFilters[key] ?? "",
44
- getNumber: (key, fallback = 0) => {
45
- const v = pendingFilters[key];
46
- if (!v)
47
- return fallback;
48
- const n = Number(v);
49
- return isNaN(n) ? fallback : n;
50
- },
51
- set: (key, value) => setPendingFilters((p) => ({ ...p, [key]: value })),
52
- setMany: (updates) => setPendingFilters((p) => ({ ...p, ...updates })),
53
- clear: (keys) => {
54
- const ks = keys ?? FILTER_KEYS;
55
- setPendingFilters((p) => ({
56
- ...p,
57
- ...Object.fromEntries(ks.map((k) => [k, ""])),
58
- }));
59
- },
60
- setPage: (_) => { },
61
- setPageSize: (_) => { },
62
- setSort: (_) => { },
63
- buildSieveParams: () => "",
64
- buildSearchParams: () => "",
65
- params: new URLSearchParams(),
66
- }), [pendingFilters]);
67
- const openFilters = useCallback(() => {
68
- setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
69
- setFilterOpen(true);
70
- }, [table]);
71
- const applyFilters = useCallback(() => {
72
- const updates = { page: "1" };
73
- for (const k of FILTER_KEYS)
74
- updates[k] = pendingFilters[k] ?? "";
75
- table.setMany(updates);
76
- setFilterOpen(false);
77
- }, [pendingFilters, table]);
78
- const clearFilters = useCallback(() => {
79
- setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
80
- }, []);
81
- const resetAll = useCallback(() => {
82
- const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" };
83
- for (const k of FILTER_KEYS)
84
- updates[k] = "";
85
- table.setMany(updates);
86
- setSearchInput("");
87
- }, [table]);
88
- const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
89
- const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
90
- table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
91
- activeFilterCount > 0;
92
- const commitSearch = useCallback(() => {
93
- table.set(TABLE_KEYS.QUERY, searchInput.trim());
94
- }, [searchInput, table]);
95
- const { reviews, totalPages, isLoading } = useReviews({
96
- q: table.get(TABLE_KEYS.QUERY) || undefined,
97
- rating: table.get(TABLE_KEYS.RATING) || undefined,
98
- dateFrom: table.get(TABLE_KEYS.DATE_FROM) || undefined,
99
- dateTo: table.get(TABLE_KEYS.DATE_TO) || undefined,
100
- minVotes: table.get("minVotes") ? Number(table.get("minVotes")) : undefined,
101
- maxVotes: table.get("maxVotes") ? Number(table.get("maxVotes")) : undefined,
102
- hasImages: table.get("hasImages") === "true" ? true : undefined,
103
- sort,
104
- page: currentPage,
105
- perPage: PAGE_SIZE,
106
- }, { initialData });
107
- const sortOptions = REVIEW_PUBLIC_SORT_OPTIONS.map((opt) => ({
108
- value: opt.value,
109
- label: SORT_OPTION_LABELS[opt.key] ?? opt.key,
110
- }));
111
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "3", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" })] }) }, i))) })) : reviews.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No reviews found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) })] }));
11
+ return (_jsx(ReviewsListingPanel, { source: { kind: "all" }, stateMode: "url", initialData: initialData, variant: variant, showSummary: false, className: "min-h-screen" }));
112
12
  }
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import type { ReviewListResponse } from "../types";
3
+ import type { ReviewCardContext } from "./ReviewsList";
4
+ /**
5
+ * Which reviews this panel lists. `product` and `all` go through `/api/reviews`;
6
+ * `store` goes through `/api/stores/[slug]/reviews`, which is the only endpoint that
7
+ * can fan a store's reviews out across its products.
8
+ */
9
+ export type ReviewsSource = {
10
+ kind: "product";
11
+ productId: string;
12
+ } | {
13
+ kind: "store";
14
+ storeSlug: string;
15
+ } | {
16
+ kind: "all";
17
+ };
18
+ export interface ReviewsListingPanelProps {
19
+ source: ReviewsSource;
20
+ pageSize?: number;
21
+ context?: ReviewCardContext;
22
+ /**
23
+ * `"url"` writes page/sort/filters to the query string — right for the dedicated
24
+ * review pages (shareable, SEO route segments read them back).
25
+ *
26
+ * `"local"` keeps that state in the component. Detail pages must use this: `useUrlTable`
27
+ * calls `router.replace()` on every change, which re-runs the whole product/auction RSC
28
+ * tree just to turn a page of reviews.
29
+ */
30
+ stateMode?: "url" | "local";
31
+ /** SSR-seeded page 1. Only applied while the panel is genuinely at its default query. */
32
+ initialData?: ReviewListResponse;
33
+ /** Rating + count line above the toolbar. */
34
+ showSummary?: boolean;
35
+ /** Free-text search over product titles — meaningless on a single-product list. */
36
+ showSearch?: boolean;
37
+ variant?: "admin" | "seller" | "public";
38
+ emptyLabel?: string;
39
+ className?: string;
40
+ }
41
+ export declare function ReviewsListingPanel({ source, pageSize, context, stateMode, initialData, showSummary, showSearch, variant, emptyLabel, className, }: ReviewsListingPanelProps): React.JSX.Element;