@mohasinac/appkit 2.7.11 → 2.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/_internal/server/jobs/core/assignSpinPrize.js +8 -7
  2. package/dist/_internal/server/jobs/core/bundleStockSync.js +11 -6
  3. package/dist/_internal/server/jobs/core/countersReconcile.js +3 -2
  4. package/dist/_internal/server/jobs/core/onProductStockChange.js +12 -7
  5. package/dist/_internal/server/jobs/core/onProductWrite.js +3 -2
  6. package/dist/_internal/server/jobs/core/prizeRevealClose.js +7 -5
  7. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +11 -6
  8. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +13 -7
  9. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +9 -4
  10. package/dist/_internal/server/jobs/core/triggerEventRaffle.js +3 -2
  11. package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
  12. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +32 -32
  13. package/dist/_internal/shared/features/events/schema.d.ts +4 -4
  14. package/dist/_internal/shared/features/products/schema.d.ts +20 -20
  15. package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
  16. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  17. package/dist/constants/field-names.d.ts +813 -0
  18. package/dist/constants/field-names.js +918 -0
  19. package/dist/constants/sort.d.ts +7 -0
  20. package/dist/constants/sort.js +5 -0
  21. package/dist/constants/table-keys.d.ts +48 -0
  22. package/dist/constants/table-keys.js +46 -0
  23. package/dist/features/account/components/AddressesIndexListing.js +9 -6
  24. package/dist/features/admin/components/AdminAllEventEntriesView.js +4 -1
  25. package/dist/features/admin/components/AdminBidsView.js +4 -1
  26. package/dist/features/admin/components/AdminBlogView.js +9 -2
  27. package/dist/features/admin/components/AdminBrandsView.js +9 -2
  28. package/dist/features/admin/components/AdminCarouselView.js +4 -1
  29. package/dist/features/admin/components/AdminCartsView.js +4 -1
  30. package/dist/features/admin/components/AdminCategoriesView.js +9 -2
  31. package/dist/features/admin/components/AdminContactView.js +4 -1
  32. package/dist/features/admin/components/AdminCouponsView.js +4 -1
  33. package/dist/features/admin/components/AdminFaqsView.js +5 -1
  34. package/dist/features/admin/components/AdminFeaturesView.js +5 -2
  35. package/dist/features/admin/components/AdminHistoryView.js +5 -1
  36. package/dist/features/admin/components/AdminNewsletterView.js +4 -1
  37. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  38. package/dist/features/admin/components/AdminOrdersView.js +14 -12
  39. package/dist/features/admin/components/AdminPayoutsView.js +4 -1
  40. package/dist/features/admin/components/AdminPrizeDrawsView.js +4 -1
  41. package/dist/features/admin/components/AdminProductsView.js +14 -3
  42. package/dist/features/admin/components/AdminReturnRequestsView.js +4 -1
  43. package/dist/features/admin/components/AdminReviewsView.js +4 -1
  44. package/dist/features/admin/components/AdminScammersView.js +4 -1
  45. package/dist/features/admin/components/AdminSessionsView.js +4 -1
  46. package/dist/features/admin/components/AdminStoreAddressesView.js +5 -1
  47. package/dist/features/admin/components/AdminStoresView.js +10 -3
  48. package/dist/features/admin/components/AdminSublistingCategoriesView.js +4 -1
  49. package/dist/features/admin/components/AdminSupportTicketsView.js +4 -1
  50. package/dist/features/admin/components/AdminTeamView.js +4 -1
  51. package/dist/features/admin/components/AdminUsersView.js +9 -12
  52. package/dist/features/admin/components/AdminViewCards.d.ts +12 -0
  53. package/dist/features/admin/components/AdminViewCards.js +53 -0
  54. package/dist/features/admin/components/AdminWishlistsView.js +4 -1
  55. package/dist/features/auctions/repository/bid.repository.js +1 -6
  56. package/dist/features/auctions/schemas/index.d.ts +4 -4
  57. package/dist/features/blog/actions/blog-actions.d.ts +16 -16
  58. package/dist/features/blog/components/BlogFilters.d.ts +28 -28
  59. package/dist/features/blog/components/BlogFilters.js +18 -15
  60. package/dist/features/blog/components/BlogIndexListing.js +20 -16
  61. package/dist/features/blog/schemas/index.d.ts +10 -10
  62. package/dist/features/categories/components/CategoriesIndexListing.js +25 -21
  63. package/dist/features/categories/schemas/index.d.ts +2 -2
  64. package/dist/features/events/components/AdminEventsView.js +5 -1
  65. package/dist/features/events/components/EventFilters.d.ts +24 -24
  66. package/dist/features/events/components/EventFilters.js +16 -13
  67. package/dist/features/events/components/EventStatusBadge.js +1 -0
  68. package/dist/features/events/components/EventsIndexListing.js +31 -26
  69. package/dist/features/events/components/EventsListPageView.js +13 -10
  70. package/dist/features/events/schemas/firestore.d.ts +1 -0
  71. package/dist/features/events/schemas/firestore.js +1 -0
  72. package/dist/features/events/schemas/index.d.ts +1 -1
  73. package/dist/features/events/types/index.d.ts +1 -1
  74. package/dist/features/faq/actions/faq-actions.d.ts +10 -10
  75. package/dist/features/faq/schemas/index.d.ts +4 -4
  76. package/dist/features/orders/components/OrderFilters.d.ts +20 -20
  77. package/dist/features/orders/components/OrderFilters.js +33 -30
  78. package/dist/features/orders/repository/orders.repository.js +1 -15
  79. package/dist/features/orders/schemas/index.d.ts +2 -2
  80. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +35 -31
  81. package/dist/features/pre-orders/schemas/index.d.ts +3 -3
  82. package/dist/features/products/components/AuctionsIndexListing.js +34 -30
  83. package/dist/features/products/components/BidHistory.d.ts +0 -1
  84. package/dist/features/products/components/BidHistory.js +1 -1
  85. package/dist/features/products/components/PrizeDrawsIndexListing.js +44 -40
  86. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  87. package/dist/features/products/components/ProductFilters.d.ts +20 -20
  88. package/dist/features/products/components/ProductFilters.js +62 -59
  89. package/dist/features/products/components/ProductsIndexListing.js +28 -24
  90. package/dist/features/products/components/ProductsIndexPageView.js +18 -10
  91. package/dist/features/products/hooks/useProducts.js +2 -0
  92. package/dist/features/products/repository/products.repository.js +2 -22
  93. package/dist/features/products/schemas/index.d.ts +28 -28
  94. package/dist/features/products/schemas/product-features.validators.d.ts +12 -12
  95. package/dist/features/products/types/index.d.ts +2 -0
  96. package/dist/features/promotions/components/CouponsIndexListing.js +21 -17
  97. package/dist/features/promotions/schemas/index.d.ts +4 -4
  98. package/dist/features/reviews/components/ReviewDetailShell.js +6 -5
  99. package/dist/features/reviews/components/ReviewFilters.d.ts +16 -16
  100. package/dist/features/reviews/components/ReviewFilters.js +10 -7
  101. package/dist/features/reviews/components/ReviewsIndexListing.js +21 -17
  102. package/dist/features/reviews/components/ReviewsList.d.ts +5 -2
  103. package/dist/features/reviews/components/ReviewsList.js +11 -5
  104. package/dist/features/reviews/schemas/firestore.d.ts +1 -0
  105. package/dist/features/reviews/schemas/index.d.ts +8 -8
  106. package/dist/features/reviews/types/index.d.ts +1 -0
  107. package/dist/features/search/schemas/index.d.ts +2 -2
  108. package/dist/features/seller/components/SellerAuctionsView.js +5 -1
  109. package/dist/features/seller/components/SellerBidsView.js +4 -1
  110. package/dist/features/seller/components/SellerCouponsView.js +4 -1
  111. package/dist/features/seller/components/SellerOffersView.js +5 -1
  112. package/dist/features/seller/components/SellerOrdersView.js +4 -1
  113. package/dist/features/seller/components/SellerPayoutsView.js +5 -1
  114. package/dist/features/seller/components/SellerPreOrdersView.js +4 -1
  115. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -1
  116. package/dist/features/seller/components/SellerProductsView.js +4 -1
  117. package/dist/features/seller/schemas/index.d.ts +10 -10
  118. package/dist/features/stores/api/route.js +10 -1
  119. package/dist/features/stores/components/StoreReviewsListing.js +1 -1
  120. package/dist/features/stores/components/StoresIndexListing.js +24 -20
  121. package/dist/features/stores/schemas/firestore.d.ts +1 -0
  122. package/dist/features/stores/schemas/index.d.ts +4 -4
  123. package/dist/index.d.ts +8 -1
  124. package/dist/index.js +12 -1
  125. package/dist/react/hooks/useUrlTable.js +2 -1
  126. package/dist/seed/bids-seed-data.js +45 -44
  127. package/dist/seed/categories-seed-data.js +41 -40
  128. package/dist/seed/coupons-seed-data.js +41 -40
  129. package/dist/seed/products-auctions-seed-data.js +54 -53
  130. package/dist/seed/products-preorders-seed-data.js +25 -24
  131. package/dist/seed/products-prize-draws-seed-data.js +24 -23
  132. package/dist/seed/products-standard-seed-data.js +294 -293
  133. package/dist/seed/reviews-seed-data.js +61 -60
  134. package/dist/seed/scammers-seed-data.js +3 -2
  135. package/dist/seed/stores-seed-data.js +9 -8
  136. package/dist/seed/support-tickets-seed-data.js +13 -12
  137. package/dist/seed/users-seed-data.js +28 -27
  138. package/dist/tailwind-utilities.css +1 -1
  139. package/dist/utils/sieve-builder.d.ts +41 -0
  140. package/dist/utils/sieve-builder.js +66 -0
  141. package/package.json +1 -1
@@ -170,6 +170,8 @@ export interface ProductListParams {
170
170
  dateTo?: string;
171
171
  /** Pre-order production status */
172
172
  preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship";
173
+ /** Prize-draw reveal status */
174
+ prizeRevealStatus?: "pending" | "open" | "closed";
173
175
  /** Filter by brand name */
174
176
  brand?: string;
175
177
  /** Shipping / free shipping */
@@ -6,12 +6,16 @@ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { Pagination, SortDropdown, Div, Text, Heading } from "../../../ui";
7
7
  import { usePromotions } from "../hooks/usePromotions";
8
8
  import { CouponCard } from "./CouponCard";
9
+ import { TABLE_KEYS } from "../../../constants/table-keys";
10
+ import { COUPON_FIELDS } from "../../../constants/field-names";
11
+ import { sortBy } from "../../../constants/sort";
12
+ const DEFAULT_SORT = sortBy(COUPON_FIELDS.CREATED_AT);
9
13
  const COUPON_SORT_OPTIONS = [
10
- { value: "name", label: "Name A–Z" },
11
- { value: "-name", label: "Name Z–A" },
14
+ { value: sortBy(COUPON_FIELDS.NAME, "ASC"), label: "Name A–Z" },
15
+ { value: sortBy(COUPON_FIELDS.NAME), label: "Name Z–A" },
12
16
  { value: "-validity.endDate", label: "Expiring Soon" },
13
- { value: "-createdAt", label: "Newest First" },
14
- { value: "createdAt", label: "Oldest First" },
17
+ { value: sortBy(COUPON_FIELDS.CREATED_AT), label: "Newest First" },
18
+ { value: sortBy(COUPON_FIELDS.CREATED_AT, "ASC"), label: "Oldest First" },
15
19
  ];
16
20
  const COUPON_TYPES = [
17
21
  { value: "percentage", label: "% Off" },
@@ -20,8 +24,8 @@ const COUPON_TYPES = [
20
24
  { value: "buy_x_get_y", label: "Buy X Get Y" },
21
25
  ];
22
26
  export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
23
- const table = useUrlTable({ defaults: { pageSize: "12", sort: "-createdAt" } });
24
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
27
+ const table = useUrlTable({ defaults: { pageSize: "12", sort: DEFAULT_SORT } });
28
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
25
29
  const [filterOpen, setFilterOpen] = useState(false);
26
30
  // Build Sieve filter string
27
31
  const buildFilters = () => {
@@ -29,10 +33,10 @@ export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
29
33
  const typeFilter = table.get("type");
30
34
  if (typeFilter)
31
35
  parts.push(`type==${typeFilter}`);
32
- const dateFrom = table.get("dateFrom");
36
+ const dateFrom = table.get(TABLE_KEYS.DATE_FROM);
33
37
  if (dateFrom)
34
38
  parts.push(`validity.startDate>=${dateFrom}`);
35
- const dateTo = table.get("dateTo");
39
+ const dateTo = table.get(TABLE_KEYS.DATE_TO);
36
40
  if (dateTo)
37
41
  parts.push(`validity.endDate<=${dateTo}`);
38
42
  if (storeId)
@@ -40,37 +44,37 @@ export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
40
44
  return parts.join(",");
41
45
  };
42
46
  const { promotions: coupons, total, totalPages, isLoading } = usePromotions({
43
- page: table.getNumber("page", 1),
44
- pageSize: table.getNumber("pageSize", 12),
45
- sort: table.get("sort") || "-createdAt",
47
+ page: table.getNumber(TABLE_KEYS.PAGE, 1),
48
+ pageSize: table.getNumber(TABLE_KEYS.PAGE_SIZE, 12),
49
+ sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
46
50
  filters: buildFilters(),
47
51
  });
48
52
  // Use initial data on first load if available and no search/filter active
49
53
  const displayCoupons = !isLoading && coupons.length > 0
50
54
  ? coupons
51
- : !isLoading && initialCoupons && !table.get("q") && !table.get("type")
55
+ : !isLoading && initialCoupons && !table.get(TABLE_KEYS.QUERY) && !table.get("type")
52
56
  ? initialCoupons
53
57
  : coupons;
54
58
  const commitSearch = useCallback(() => {
55
- table.set("q", searchInput.trim());
59
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
56
60
  }, [searchInput, table]);
57
61
  const handleKeyDown = (e) => {
58
62
  if (e.key === "Enter")
59
63
  commitSearch();
60
64
  };
61
65
  const activeType = table.get("type");
62
- const hasActiveFilters = !!activeType || !!table.get("dateFrom") || !!table.get("dateTo");
66
+ const hasActiveFilters = !!activeType || !!table.get(TABLE_KEYS.DATE_FROM) || !!table.get(TABLE_KEYS.DATE_TO);
63
67
  const clearFilters = () => {
64
- table.setMany({ type: "", dateFrom: "", dateTo: "" });
68
+ table.setMany({ type: "", [TABLE_KEYS.DATE_FROM]: "", [TABLE_KEYS.DATE_TO]: "" });
65
69
  };
66
70
  return (_jsxs("div", { className: "min-h-[40vh]", children: [_jsxs("div", { className: "sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2.5 px-4", children: [_jsxs("div", { className: "flex items-center gap-2.5 max-w-full", children: [_jsxs("button", { type: "button", onClick: () => setFilterOpen(true), className: `flex shrink-0 items-center gap-2 rounded-lg border px-3.5 py-2 text-sm font-medium transition-colors ${hasActiveFilters
67
71
  ? "border-primary bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
68
- : "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsxs("span", { className: "hidden sm:inline", children: ["Filters", hasActiveFilters ? " •" : ""] })] }), _jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900", children: [_jsx("input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search by name or description\u2026", className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), searchInput && (_jsx("button", { type: "button", onClick: () => { setSearchInput(""); table.set("q", ""); }, className: "p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300", "aria-label": "Clear search", children: _jsx(X, { className: "h-3.5 w-3.5" }) })), _jsx("button", { type: "button", onClick: commitSearch, className: "flex shrink-0 items-center justify-center px-3 py-2 text-zinc-400 hover:text-primary dark:hover:text-primary-400 transition-colors", "aria-label": "Search", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get("sort") || "-createdAt", onChange: (v) => { table.set("sort", v); }, options: COUPON_SORT_OPTIONS })] })] }), hasActiveFilters && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 mt-2", children: [activeType && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: [COUPON_TYPES.find((t) => t.value === activeType)?.label ?? activeType, _jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, "aria-label": "Remove type filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get("dateFrom") && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: ["From: ", table.get("dateFrom"), _jsx("button", { type: "button", onClick: () => { table.set("dateFrom", ""); }, "aria-label": "Remove from-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get("dateTo") && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: ["To: ", table.get("dateTo"), _jsx("button", { type: "button", onClick: () => { table.set("dateTo", ""); }, "aria-label": "Remove to-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 underline", children: "Clear all" })] }))] }), _jsxs("div", { className: "py-6 px-4", children: [isLoading ? (_jsx("div", { className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border-2 border-zinc-100 dark:border-slate-700 p-4 animate-pulse space-y-3", children: [_jsx("div", { className: "h-6 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" })] }, i))) })) : displayCoupons.length === 0 ? (_jsx("div", { className: "py-16 text-center", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500", children: "No coupons match your search." }) })) : (_jsx("div", { className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3", children: displayCoupons.map((coupon) => (_jsx(CouponCard, { coupon: coupon, labels: {
72
+ : "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsxs("span", { className: "hidden sm:inline", children: ["Filters", hasActiveFilters ? " •" : ""] })] }), _jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900", children: [_jsx("input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search by name or description\u2026", className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), searchInput && (_jsx("button", { type: "button", onClick: () => { setSearchInput(""); table.set(TABLE_KEYS.QUERY, ""); }, className: "p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300", "aria-label": "Clear search", children: _jsx(X, { className: "h-3.5 w-3.5" }) })), _jsx("button", { type: "button", onClick: commitSearch, className: "flex shrink-0 items-center justify-center px-3 py-2 text-zinc-400 hover:text-primary dark:hover:text-primary-400 transition-colors", "aria-label": "Search", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, onChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, options: COUPON_SORT_OPTIONS })] })] }), hasActiveFilters && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 mt-2", children: [activeType && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: [COUPON_TYPES.find((t) => t.value === activeType)?.label ?? activeType, _jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, "aria-label": "Remove type filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_FROM) && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: ["From: ", table.get(TABLE_KEYS.DATE_FROM), _jsx("button", { type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_FROM, ""); }, "aria-label": "Remove from-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_TO) && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: ["To: ", table.get(TABLE_KEYS.DATE_TO), _jsx("button", { type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_TO, ""); }, "aria-label": "Remove to-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 underline", children: "Clear all" })] }))] }), _jsxs("div", { className: "py-6 px-4", children: [isLoading ? (_jsx("div", { className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border-2 border-zinc-100 dark:border-slate-700 p-4 animate-pulse space-y-3", children: [_jsx("div", { className: "h-6 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" })] }, i))) })) : displayCoupons.length === 0 ? (_jsx("div", { className: "py-16 text-center", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500", children: "No coupons match your search." }) })) : (_jsx("div", { className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3", children: displayCoupons.map((coupon) => (_jsx(CouponCard, { coupon: coupon, labels: {
69
73
  copy: "Copy",
70
74
  copied: "Copied!",
71
75
  expires: "Expires",
72
76
  minOrder: "Min. order",
73
77
  off: "OFF",
74
78
  freeShipping: "Free Shipping",
75
- } }, coupon.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), !isLoading && total > 0 && (_jsx(Div, { className: "mt-4 text-center", children: _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [total, " coupon", total !== 1 ? "s" : "", " available"] }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-6", children: [_jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Discount Type" }), _jsxs("div", { className: "space-y-2", children: [COUPON_TYPES.map((t) => (_jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "coupon-type", value: t.value, checked: table.get("type") === t.value, onChange: () => { table.set("type", t.value); }, className: "accent-primary" }), t.label] }, t.value))), table.get("type") && (_jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", children: "Clear type" }))] })] }), _jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Valid Date Range" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "From date" }), _jsx("input", { type: "date", value: table.get("dateFrom") || "", onChange: (e) => { table.set("dateFrom", e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "To date" }), _jsx("input", { type: "date", value: table.get("dateTo") || "", onChange: (e) => { table.set("dateTo", e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] })] })] })] }), _jsxs("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5 flex gap-2", children: [_jsx("button", { type: "button", onClick: clearFilters, className: "flex-1 rounded-lg border border-zinc-300 dark:border-slate-600 py-2.5 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: "Clear all" }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), className: "flex-1 rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply" })] })] })] }))] }));
79
+ } }, coupon.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), !isLoading && total > 0 && (_jsx(Div, { className: "mt-4 text-center", children: _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [total, " coupon", total !== 1 ? "s" : "", " available"] }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-6", children: [_jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Discount Type" }), _jsxs("div", { className: "space-y-2", children: [COUPON_TYPES.map((t) => (_jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "coupon-type", value: t.value, checked: table.get("type") === t.value, onChange: () => { table.set("type", t.value); }, className: "accent-primary" }), t.label] }, t.value))), table.get("type") && (_jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", children: "Clear type" }))] })] }), _jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Valid Date Range" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "From date" }), _jsx("input", { type: "date", value: table.get(TABLE_KEYS.DATE_FROM) || "", onChange: (e) => { table.set(TABLE_KEYS.DATE_FROM, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "To date" }), _jsx("input", { type: "date", value: table.get(TABLE_KEYS.DATE_TO) || "", onChange: (e) => { table.set(TABLE_KEYS.DATE_TO, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] })] })] })] }), _jsxs("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5 flex gap-2", children: [_jsx("button", { type: "button", onClick: clearFilters, className: "flex-1 rounded-lg border border-zinc-300 dark:border-slate-600 py-2.5 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: "Clear all" }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), className: "flex-1 rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply" })] })] })] }))] }));
76
80
  }
@@ -49,10 +49,11 @@ export declare const couponItemSchema: z.ZodObject<{
49
49
  isActive: boolean;
50
50
  isPublic: boolean;
51
51
  description: string;
52
- discountValue: number;
53
52
  usageCount: number;
53
+ discountValue: number;
54
54
  storeId?: string | undefined;
55
55
  expiresAt?: string | undefined;
56
+ startsAt?: string | undefined;
56
57
  minOrderAmount?: number | undefined;
57
58
  maxDiscountAmount?: number | undefined;
58
59
  maxUsageCount?: number | undefined;
@@ -61,7 +62,6 @@ export declare const couponItemSchema: z.ZodObject<{
61
62
  getQuantity?: number | undefined;
62
63
  applicableProductIds?: string[] | undefined;
63
64
  applicableCategoryIds?: string[] | undefined;
64
- startsAt?: string | undefined;
65
65
  }, {
66
66
  id: string;
67
67
  createdAt: string;
@@ -73,10 +73,11 @@ export declare const couponItemSchema: z.ZodObject<{
73
73
  isActive: boolean;
74
74
  isPublic: boolean;
75
75
  description: string;
76
- discountValue: number;
77
76
  usageCount: number;
77
+ discountValue: number;
78
78
  storeId?: string | undefined;
79
79
  expiresAt?: string | undefined;
80
+ startsAt?: string | undefined;
80
81
  minOrderAmount?: number | undefined;
81
82
  maxDiscountAmount?: number | undefined;
82
83
  maxUsageCount?: number | undefined;
@@ -85,7 +86,6 @@ export declare const couponItemSchema: z.ZodObject<{
85
86
  getQuantity?: number | undefined;
86
87
  applicableProductIds?: string[] | undefined;
87
88
  applicableCategoryIds?: string[] | undefined;
88
- startsAt?: string | undefined;
89
89
  }>;
90
90
  export declare const promotionsListParamsSchema: z.ZodObject<{
91
91
  scope: z.ZodOptional<z.ZodEnum<["admin", "seller"]>>;
@@ -2,8 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useCallback } from "react";
4
4
  import Link from "next/link";
5
- import { StarRating } from "../../../ui";
6
- import { RichText } from "../../../ui";
5
+ import { RichText, Span, StarRating } from "../../../ui";
7
6
  import { maskName } from "../../../security";
8
7
  import { getDefaultLocale } from "../../../core/baseline-resolver";
9
8
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
@@ -70,11 +69,13 @@ export function ReviewDetailShell({ review, storeHref }) {
70
69
  ? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
71
70
  : null;
72
71
  const sellerHref = storeHref ?? null;
73
- const reviewerHref = String(ROUTES.PUBLIC.PROFILE(review.userId));
72
+ const reviewerHref = !review.isAnonymous && review.userId
73
+ ? String(ROUTES.PUBLIC.PROFILE(review.userId))
74
+ : null;
74
75
  const currentImage = lightboxIdx !== null ? images[lightboxIdx] : null;
75
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-b border-neutral-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 pb-8 pt-10", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx(StarRating, { value: review.rating, size: "lg", readOnly: true }), _jsxs("span", { className: "text-2xl font-bold text-neutral-900 dark:text-white", children: [review.rating, ".0"] }), review.verified && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "\u2713 Verified Purchase" })), review.featured && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", children: "\u2605 Featured" }))] }), review.title && (_jsx("h1", { className: "text-2xl font-bold text-neutral-900 dark:text-white mb-4 leading-snug", children: review.title })), _jsxs("div", { className: "flex items-center gap-3", children: [review.userAvatar ? (_jsx("div", { role: "img", "aria-label": displayName, className: "h-11 w-11 flex-shrink-0 rounded-full bg-center bg-cover ring-2 ring-white dark:ring-zinc-800", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-primary/10 text-base font-bold text-primary ring-2 ring-white dark:ring-zinc-800", children: initials })), _jsxs("div", { className: "min-w-0", children: [_jsx(Link, { href: reviewerHref, className: "text-sm font-semibold text-neutral-900 dark:text-white hover:text-primary transition-colors", children: displayName }), date && (_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mt-0.5", children: date }))] })] })] }) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-8 space-y-8", children: [review.comment && (_jsx("section", { children: _jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-neutral dark:prose-invert max-w-none prose-p:leading-relaxed prose-headings:font-semibold prose-img:rounded-lg prose-a:text-primary", className: "text-neutral-700 dark:text-zinc-300" }) })), images.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: ["Photos (", images.length, ")"] }), _jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 gap-2", children: images.map((img, i) => (_jsxs("button", { type: "button", onClick: () => setLightboxIdx(i), "aria-label": `View photo ${i + 1}`, className: "group relative aspect-square overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-neutral-100 dark:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: [_jsx("div", { className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30", children: _jsx("span", { className: "text-white text-xl", children: "\uD83D\uDD0D" }) })] }, i))) })] })), review.video && (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: "Video" }), _jsx("div", { className: "overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-black aspect-video", children: _jsx("video", { src: review.video.url, poster: review.video.thumbnailUrl, controls: true, className: "h-full w-full", preload: "metadata" }) })] })), _jsxs("section", { className: "flex items-center gap-4 py-4 border-t border-neutral-100 dark:border-zinc-800", children: [_jsx("div", { className: "text-sm text-neutral-500 dark:text-zinc-400", children: helpfulCount > 0 && (_jsxs("span", { children: [_jsx("strong", { className: "text-neutral-900 dark:text-white", children: helpfulCount }), " ", helpfulCount === 1 ? "person" : "people", " found this helpful"] })) }), _jsxs("button", { type: "button", onClick: handleVote, disabled: voted || voting, className: `ml-auto flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors ${voted
76
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-b border-neutral-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 pb-8 pt-10", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx(StarRating, { value: review.rating, size: "lg", readOnly: true }), _jsxs("span", { className: "text-2xl font-bold text-neutral-900 dark:text-white", children: [review.rating, ".0"] }), review.verified && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "\u2713 Verified Purchase" })), review.featured && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", children: "\u2605 Featured" }))] }), review.title && (_jsx("h1", { className: "text-2xl font-bold text-neutral-900 dark:text-white mb-4 leading-snug", children: review.title })), _jsxs("div", { className: "flex items-center gap-3", children: [review.userAvatar ? (_jsx("div", { role: "img", "aria-label": displayName, className: "h-11 w-11 flex-shrink-0 rounded-full bg-center bg-cover ring-2 ring-white dark:ring-zinc-800", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-primary/10 text-base font-bold text-primary ring-2 ring-white dark:ring-zinc-800", children: initials })), _jsxs("div", { className: "min-w-0", children: [reviewerHref ? (_jsx(Link, { href: reviewerHref, className: "text-sm font-semibold text-neutral-900 dark:text-white hover:text-primary transition-colors", children: displayName })) : (_jsx("span", { className: "text-sm font-semibold text-neutral-900 dark:text-white", children: review.isAnonymous ? "Anonymous" : displayName })), date && (_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mt-0.5", children: date }))] })] })] }) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-8 space-y-8", children: [review.comment && (_jsx("section", { children: _jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-neutral dark:prose-invert max-w-none prose-p:leading-relaxed prose-headings:font-semibold prose-img:rounded-lg prose-a:text-primary", className: "text-neutral-700 dark:text-zinc-300" }) })), images.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: ["Photos (", images.length, ")"] }), _jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 gap-2", children: images.map((img, i) => (_jsxs("button", { type: "button", onClick: () => setLightboxIdx(i), "aria-label": `View photo ${i + 1}`, className: "group relative aspect-square overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-neutral-100 dark:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: [_jsx("div", { className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30", children: _jsx("span", { className: "text-white text-xl", children: "\uD83D\uDD0D" }) })] }, i))) })] })), review.video && (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: "Video" }), _jsx("div", { className: "overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-black aspect-video", children: _jsx("video", { src: review.video.url, poster: review.video.thumbnailUrl, controls: true, className: "h-full w-full", preload: "metadata" }) })] })), _jsxs("section", { className: "flex items-center gap-4 py-4 border-t border-neutral-100 dark:border-zinc-800", children: [_jsx("div", { className: "text-sm text-neutral-500 dark:text-zinc-400", children: helpfulCount > 0 && (_jsxs("span", { children: [_jsx("strong", { className: "text-neutral-900 dark:text-white", children: helpfulCount }), " ", helpfulCount === 1 ? "person" : "people", " found this helpful"] })) }), _jsxs("button", { type: "button", onClick: handleVote, disabled: voted || voting, className: `ml-auto flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors ${voted
76
77
  ? "border-emerald-200 bg-emerald-50 text-emerald-600 dark:border-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-400 cursor-default"
77
- : "border-neutral-300 dark:border-zinc-600 text-neutral-700 dark:text-zinc-200 hover:border-primary hover:text-primary dark:hover:border-primary dark:hover:text-primary disabled:opacity-50"}`, children: [_jsx("span", { "aria-hidden": "true", children: voted ? "✓" : "👍" }), voted ? "Marked helpful" : voting ? "Saving…" : "Helpful?"] })] }), _jsxs("section", { className: "grid gap-3 sm:grid-cols-3", children: [productHref && (_jsxs(Link, { href: productHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30 text-xl", children: "\uD83D\uDCE6" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Product" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: review.productTitle ?? "View Product" })] })] })), sellerHref && (_jsxs(Link, { href: sellerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/30 text-xl", children: "\uD83C\uDFEA" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Seller" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: "View Seller" })] })] })), _jsxs(Link, { href: reviewerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30 text-xl", children: "\uD83D\uDC64" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Reviewer" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: displayName })] })] })] })] }), lightboxIdx !== null && currentImage && (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/95", onClick: closeLightbox, role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", children: [_jsx("button", { type: "button", onClick: closeLightbox, "aria-label": "Close lightbox", className: "absolute top-4 right-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 transition-colors text-xl", children: "\u00D7" }), _jsxs("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 text-white/70 text-sm", children: [lightboxIdx + 1, " / ", images.length] }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); prevImage(); }, "aria-label": "Previous image", className: "absolute left-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u2039" })), _jsx("div", { className: "max-h-[85vh] max-w-[85vw] flex items-center justify-center", onClick: (e) => e.stopPropagation(), children: _jsx("img", { src: currentImage.url, alt: `Review photo ${lightboxIdx + 1}`, className: "max-h-[85vh] max-w-[85vw] rounded-lg object-contain shadow-2xl" }) }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); nextImage(); }, "aria-label": "Next image", className: "absolute right-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u203A" })), images.length > 1 && (_jsx("div", { className: "absolute bottom-4 left-0 right-0 flex justify-center gap-2 px-4", children: images.map((img, i) => (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); setLightboxIdx(i); }, "aria-label": `Go to image ${i + 1}`, className: `h-12 w-12 flex-shrink-0 rounded-md bg-center bg-cover border-2 transition-all ${i === lightboxIdx
78
+ : "border-neutral-300 dark:border-zinc-600 text-neutral-700 dark:text-zinc-200 hover:border-primary hover:text-primary dark:hover:border-primary dark:hover:text-primary disabled:opacity-50"}`, children: [_jsx("span", { "aria-hidden": "true", children: voted ? "✓" : "👍" }), voted ? "Marked helpful" : voting ? "Saving…" : "Helpful?"] })] }), _jsxs("section", { className: "grid gap-3 sm:grid-cols-3", children: [productHref && (_jsxs(Link, { href: productHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30 text-xl", children: "\uD83D\uDCE6" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Product" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: review.productTitle ?? "View Product" })] })] })), sellerHref && (_jsxs(Link, { href: sellerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/30 text-xl", children: "\uD83C\uDFEA" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Seller" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: "View Seller" })] })] })), reviewerHref ? (_jsxs(Link, { href: reviewerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30 text-xl", children: "\uD83D\uDC64" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Reviewer" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: displayName })] })] })) : (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30 text-xl", children: "\uD83D\uDC64" }), _jsxs("div", { className: "min-w-0", children: [_jsx(Span, { className: "block text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Reviewer" }), _jsx(Span, { className: "block text-sm font-medium text-neutral-900 dark:text-white truncate", children: "Anonymous" })] })] }))] })] }), lightboxIdx !== null && currentImage && (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/95", onClick: closeLightbox, role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", children: [_jsx("button", { type: "button", onClick: closeLightbox, "aria-label": "Close lightbox", className: "absolute top-4 right-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 transition-colors text-xl", children: "\u00D7" }), _jsxs("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 text-white/70 text-sm", children: [lightboxIdx + 1, " / ", images.length] }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); prevImage(); }, "aria-label": "Previous image", className: "absolute left-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u2039" })), _jsx("div", { className: "max-h-[85vh] max-w-[85vw] flex items-center justify-center", onClick: (e) => e.stopPropagation(), children: _jsx("img", { src: currentImage.url, alt: `Review photo ${lightboxIdx + 1}`, className: "max-h-[85vh] max-w-[85vw] rounded-lg object-contain shadow-2xl" }) }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); nextImage(); }, "aria-label": "Next image", className: "absolute right-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u203A" })), images.length > 1 && (_jsx("div", { className: "absolute bottom-4 left-0 right-0 flex justify-center gap-2 px-4", children: images.map((img, i) => (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); setLightboxIdx(i); }, "aria-label": `Go to image ${i + 1}`, className: `h-12 w-12 flex-shrink-0 rounded-md bg-center bg-cover border-2 transition-all ${i === lightboxIdx
78
79
  ? "border-white scale-110"
79
80
  : "border-transparent opacity-60 hover:opacity-100"}`, style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) }))] }))] }));
80
81
  }
@@ -7,55 +7,55 @@ export declare const REVIEW_FILTER_KEYS: {
7
7
  readonly public: readonly ["rating", "brand", "dateFrom", "dateTo"];
8
8
  };
9
9
  export declare const REVIEW_ADMIN_SORT_OPTIONS: readonly [{
10
- readonly value: "-createdAt";
10
+ readonly value: string;
11
11
  readonly key: "sortNewest";
12
12
  }, {
13
- readonly value: "createdAt";
13
+ readonly value: string;
14
14
  readonly key: "sortOldest";
15
15
  }, {
16
- readonly value: "-rating";
16
+ readonly value: string;
17
17
  readonly key: "sortHighestRated";
18
18
  }, {
19
- readonly value: "rating";
19
+ readonly value: string;
20
20
  readonly key: "sortLowestRated";
21
21
  }];
22
22
  export declare const REVIEW_SELLER_SORT_OPTIONS: readonly [{
23
- readonly value: "-createdAt";
23
+ readonly value: string;
24
24
  readonly key: "sortNewest";
25
25
  }, {
26
- readonly value: "createdAt";
26
+ readonly value: string;
27
27
  readonly key: "sortOldest";
28
28
  }, {
29
- readonly value: "-rating";
29
+ readonly value: string;
30
30
  readonly key: "sortHighestRated";
31
31
  }, {
32
- readonly value: "rating";
32
+ readonly value: string;
33
33
  readonly key: "sortLowestRated";
34
34
  }];
35
35
  export declare const REVIEW_PUBLIC_SORT_OPTIONS: readonly [{
36
- readonly value: "-createdAt";
36
+ readonly value: string;
37
37
  readonly key: "sortNewest";
38
38
  }, {
39
- readonly value: "createdAt";
39
+ readonly value: string;
40
40
  readonly key: "sortOldest";
41
41
  }, {
42
- readonly value: "-rating";
42
+ readonly value: string;
43
43
  readonly key: "sortHighestRated";
44
44
  }, {
45
- readonly value: "rating";
45
+ readonly value: string;
46
46
  readonly key: "sortLowestRated";
47
47
  }];
48
48
  export declare const REVIEW_SORT_OPTIONS: readonly [{
49
- readonly value: "-createdAt";
49
+ readonly value: string;
50
50
  readonly key: "sortNewest";
51
51
  }, {
52
- readonly value: "createdAt";
52
+ readonly value: string;
53
53
  readonly key: "sortOldest";
54
54
  }, {
55
- readonly value: "-rating";
55
+ readonly value: string;
56
56
  readonly key: "sortHighestRated";
57
57
  }, {
58
- readonly value: "rating";
58
+ readonly value: string;
59
59
  readonly key: "sortLowestRated";
60
60
  }];
61
61
  export declare function getReviewFilterKeys(variant: ReviewFilterVariant): readonly string[];
@@ -5,16 +5,19 @@ import { FilterFacetSection } from "../../filters/FilterFacetSection";
5
5
  import { RangeFilter } from "../../filters/RangeFilter";
6
6
  import { SwitchFilter } from "../../filters/SwitchFilter";
7
7
  import { Div } from "../../../ui";
8
+ import { TABLE_KEYS } from "../../../constants/table-keys";
9
+ import { REVIEW_FIELDS } from "../../../constants/field-names";
10
+ import { sortBy } from "../../../constants/sort";
8
11
  export const REVIEW_FILTER_KEYS = {
9
- admin: ["status", "rating", "brand", "verified", "featured", "dateFrom", "dateTo"],
10
- seller: ["status", "rating", "brand", "dateFrom", "dateTo"],
11
- public: ["rating", "brand", "dateFrom", "dateTo"],
12
+ admin: [TABLE_KEYS.STATUS, TABLE_KEYS.RATING, TABLE_KEYS.BRAND, "verified", TABLE_KEYS.FEATURED, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
13
+ seller: [TABLE_KEYS.STATUS, TABLE_KEYS.RATING, TABLE_KEYS.BRAND, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
14
+ public: [TABLE_KEYS.RATING, TABLE_KEYS.BRAND, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
12
15
  };
13
16
  export const REVIEW_ADMIN_SORT_OPTIONS = [
14
- { value: "-createdAt", key: "sortNewest" },
15
- { value: "createdAt", key: "sortOldest" },
16
- { value: "-rating", key: "sortHighestRated" },
17
- { value: "rating", key: "sortLowestRated" },
17
+ { value: sortBy(REVIEW_FIELDS.CREATED_AT), key: "sortNewest" },
18
+ { value: sortBy(REVIEW_FIELDS.CREATED_AT, "ASC"), key: "sortOldest" },
19
+ { value: sortBy(REVIEW_FIELDS.RATING), key: "sortHighestRated" },
20
+ { value: sortBy(REVIEW_FIELDS.RATING, "ASC"), key: "sortLowestRated" },
18
21
  ];
19
22
  export const REVIEW_SELLER_SORT_OPTIONS = REVIEW_ADMIN_SORT_OPTIONS;
20
23
  export const REVIEW_PUBLIC_SORT_OPTIONS = REVIEW_ADMIN_SORT_OPTIONS;
@@ -7,21 +7,25 @@ import { Pagination, ListingToolbar } from "../../../ui";
7
7
  import { ReviewCard } from "./ReviewsList";
8
8
  import { ReviewFilters, REVIEW_PUBLIC_SORT_OPTIONS } from "./ReviewFilters";
9
9
  import { useReviews } from "../hooks/useReviews";
10
+ import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
11
+ import { sortBy } from "../../../constants/sort";
12
+ import { REVIEW_FIELDS } from "../../../constants/field-names";
10
13
  const PAGE_SIZE = 12;
11
- const FILTER_KEYS = ["rating", "dateFrom", "dateTo", "minVotes", "maxVotes"];
14
+ const DEFAULT_SORT = sortBy(REVIEW_FIELDS.CREATED_AT);
15
+ const FILTER_KEYS = ["rating", TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO, "minVotes", "maxVotes"];
12
16
  export function ReviewsIndexListing({ initialData, variant = "public", }) {
13
- const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "-createdAt" } });
14
- const [searchInput, setSearchInput] = useState(table.get("q") || "");
17
+ const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
18
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
15
19
  const [filterOpen, setFilterOpen] = useState(false);
16
- const [view, setView] = useState(table.get("view") || "grid");
20
+ const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
17
21
  const handleViewToggle = (next) => {
18
- if (next === "table")
22
+ if (next === VIEW_MODE.TABLE)
19
23
  return;
20
24
  setView(next);
21
- table.set("view", next);
25
+ table.set(TABLE_KEYS.VIEW, next);
22
26
  };
23
- const sort = table.get("sort") || "-createdAt";
24
- const currentPage = table.getNumber("page", 1);
27
+ const sort = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
28
+ const currentPage = table.getNumber(TABLE_KEYS.PAGE, 1);
25
29
  // Pending filter state — buffered until "Apply Filters" clicked
26
30
  const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
27
31
  const pendingTable = useMemo(() => ({
@@ -64,24 +68,24 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
64
68
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
65
69
  }, []);
66
70
  const resetAll = useCallback(() => {
67
- const updates = { q: "", sort: "" };
71
+ const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" };
68
72
  for (const k of FILTER_KEYS)
69
73
  updates[k] = "";
70
74
  table.setMany(updates);
71
75
  setSearchInput("");
72
76
  }, [table]);
73
77
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
74
- const hasActiveState = !!table.get("q") ||
75
- table.get("sort") !== "-createdAt" ||
78
+ const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
79
+ table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
76
80
  activeFilterCount > 0;
77
81
  const commitSearch = useCallback(() => {
78
- table.set("q", searchInput.trim());
82
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
79
83
  }, [searchInput, table]);
80
84
  const { reviews, total, totalPages, isLoading } = useReviews({
81
- q: table.get("q") || undefined,
82
- rating: table.get("rating") || undefined,
83
- dateFrom: table.get("dateFrom") || undefined,
84
- dateTo: table.get("dateTo") || undefined,
85
+ q: table.get(TABLE_KEYS.QUERY) || undefined,
86
+ rating: table.get(TABLE_KEYS.RATING) || undefined,
87
+ dateFrom: table.get(TABLE_KEYS.DATE_FROM) || undefined,
88
+ dateTo: table.get(TABLE_KEYS.DATE_TO) || undefined,
85
89
  minVotes: table.get("minVotes") ? Number(table.get("minVotes")) : undefined,
86
90
  maxVotes: table.get("maxVotes") ? Number(table.get("maxVotes")) : undefined,
87
91
  sort,
@@ -92,5 +96,5 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
92
96
  value: opt.value,
93
97
  label: opt.key,
94
98
  }));
95
- 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("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
99
+ 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("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
96
100
  }
@@ -1,15 +1,18 @@
1
1
  import type { Review } from "../types";
2
+ export type ReviewCardContext = "listing" | "store" | "general";
2
3
  export interface ReviewCardProps {
3
4
  review: Review;
5
+ context?: ReviewCardContext;
4
6
  className?: string;
5
7
  }
6
- export declare function ReviewCard({ review, className }: ReviewCardProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function ReviewCard({ review, context, className }: ReviewCardProps): import("react/jsx-runtime").JSX.Element;
7
9
  export interface ReviewsListProps {
8
10
  reviews: Review[];
11
+ context?: ReviewCardContext;
9
12
  isLoading?: boolean;
10
13
  totalPages?: number;
11
14
  currentPage?: number;
12
15
  onPageChange?: (page: number) => void;
13
16
  emptyLabel?: string;
14
17
  }
15
- export declare function ReviewsList({ reviews, isLoading, totalPages, currentPage, onPageChange, emptyLabel, }: ReviewsListProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function ReviewsList({ reviews, context, isLoading, totalPages, currentPage, onPageChange, emptyLabel, }: ReviewsListProps): import("react/jsx-runtime").JSX.Element;
@@ -7,7 +7,7 @@ import { maskName } from "../../../security";
7
7
  import { getDefaultLocale } from "../../../core/baseline-resolver";
8
8
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
9
9
  import { ROUTES } from "../../../next";
10
- export function ReviewCard({ review, className = "" }) {
10
+ export function ReviewCard({ review, context = "general", className = "" }) {
11
11
  const date = review.createdAt
12
12
  ? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
13
13
  year: "numeric",
@@ -21,15 +21,21 @@ export function ReviewCard({ review, className = "" }) {
21
21
  const productHref = review.productId
22
22
  ? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
23
23
  : null;
24
- const hasFooter = !!(review.storeSlug && review.storeName) || !!productHref;
25
- return (_jsxs(Div, { className: `group flex flex-col h-full rounded-xl border border-neutral-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900 transition-shadow hover:shadow-md ${className}`, children: [_jsxs(Link, { href: reviewHref, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { className: "flex items-start gap-3", children: [review.userAvatar ? (_jsx(Div, { role: "img", "aria-label": displayName, className: "h-9 w-9 flex-shrink-0 rounded-full bg-center bg-cover", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx(Div, { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-neutral-200 text-sm font-medium text-neutral-600 dark:bg-zinc-700 dark:text-zinc-300", children: initials })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { wrap: true, gap: "sm", children: [_jsx(Span, { className: "font-medium text-neutral-900 dark:text-white", children: displayName }), review.verified && (_jsx(Span, { className: "rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "Verified" })), date && (_jsx(Span, { className: "text-xs text-neutral-400 dark:text-zinc-500", children: date }))] }), _jsx(Div, { className: "mt-1", children: _jsx(StarRating, { value: review.rating, size: "sm", readOnly: true }) })] })] }), review.title && (_jsx(Heading, { level: 4, className: "mt-3 font-semibold text-neutral-900 dark:text-white", children: review.title })), review.comment && (_jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-2 text-sm leading-relaxed text-neutral-600 dark:text-zinc-400" })), review.images && review.images.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mt-3", children: review.images.map((img, i) => (_jsx(Div, { role: "img", "aria-label": `Review image ${i + 1}`, className: "h-16 w-16 rounded-lg bg-center bg-cover border border-neutral-100 dark:border-zinc-700", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) })), (review.helpfulCount ?? 0) > 0 && (_jsxs(Text, { className: "mt-3 text-xs text-neutral-400 dark:text-zinc-500", children: [review.helpfulCount, " found this helpful"] }))] }), hasFooter && (_jsxs(Div, { className: "mt-3 pt-3 border-t border-neutral-100 dark:border-zinc-800 flex flex-col gap-1.5", children: [review.storeSlug && review.storeName && (_jsxs(Link, { href: String(ROUTES.PUBLIC.STORE_DETAIL(review.storeSlug)), className: "flex items-center gap-1.5 text-xs font-medium text-primary hover:underline", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83C\uDFEA" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: review.storeName })] })), productHref && (_jsxs(Link, { href: productHref, className: "flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 hover:text-neutral-700 dark:hover:text-zinc-200", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: review.productTitle ?? "View Product" }), _jsx("span", { "aria-hidden": "true", className: "ml-auto text-primary group-hover:translate-x-0.5 transition-transform", children: "\u2192" })] }))] }))] }));
24
+ const profileHref = !review.isAnonymous && review.userId
25
+ ? String(ROUTES.PUBLIC.PROFILE(review.userId))
26
+ : null;
27
+ const showStoreLink = context !== "store" && !!(review.storeSlug && review.storeName);
28
+ const showProductLink = context !== "listing" && !!productHref;
29
+ const showProfileLink = !!profileHref;
30
+ const hasFooter = showStoreLink || showProductLink || showProfileLink;
31
+ return (_jsxs(Div, { className: `group flex flex-col h-full rounded-xl border border-neutral-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900 transition-shadow hover:shadow-md ${className}`, children: [_jsxs(Link, { href: reviewHref, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { className: "flex items-start gap-3", children: [review.userAvatar ? (_jsx(Div, { role: "img", "aria-label": displayName, className: "h-9 w-9 flex-shrink-0 rounded-full bg-center bg-cover", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx(Div, { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-neutral-200 text-sm font-medium text-neutral-600 dark:bg-zinc-700 dark:text-zinc-300", children: initials })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { wrap: true, gap: "sm", children: [_jsx(Span, { className: "font-medium text-neutral-900 dark:text-white", children: displayName }), review.verified && (_jsx(Span, { className: "rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "Verified" })), date && (_jsx(Span, { className: "text-xs text-neutral-400 dark:text-zinc-500", children: date }))] }), _jsx(Div, { className: "mt-1", children: _jsx(StarRating, { value: review.rating, size: "sm", readOnly: true }) })] })] }), review.title && (_jsx(Heading, { level: 4, className: "mt-3 font-semibold text-neutral-900 dark:text-white", children: review.title })), review.comment && (_jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-2 text-sm leading-relaxed text-neutral-600 dark:text-zinc-400" })), review.images && review.images.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mt-3", children: review.images.map((img, i) => (_jsx(Div, { role: "img", "aria-label": `Review image ${i + 1}`, className: "h-16 w-16 rounded-lg bg-center bg-cover border border-neutral-100 dark:border-zinc-700", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) })), (review.helpfulCount ?? 0) > 0 && (_jsxs(Text, { className: "mt-3 text-xs text-neutral-400 dark:text-zinc-500", children: [review.helpfulCount, " found this helpful"] }))] }), hasFooter && (_jsxs(Div, { className: "mt-3 pt-3 border-t border-neutral-100 dark:border-zinc-800 flex flex-col gap-1.5", children: [showStoreLink && (_jsxs(Link, { href: String(ROUTES.PUBLIC.STORE_DETAIL(review.storeSlug)), className: "flex items-center gap-1.5 text-xs font-medium text-primary hover:underline", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83C\uDFEA" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: review.storeName })] })), showProductLink && (_jsxs(Link, { href: productHref, className: "flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 hover:text-neutral-700 dark:hover:text-zinc-200", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: review.productTitle ?? "View Product" }), _jsx("span", { "aria-hidden": "true", className: "ml-auto text-primary group-hover:translate-x-0.5 transition-transform", children: "\u2192" })] })), showProfileLink && (_jsxs(Link, { href: profileHref, className: "flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 hover:text-neutral-700 dark:hover:text-zinc-200", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDC64" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: displayName })] }))] }))] }));
26
32
  }
27
- export function ReviewsList({ reviews, isLoading, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No reviews yet", }) {
33
+ export function ReviewsList({ reviews, context = "general", isLoading, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No reviews yet", }) {
28
34
  if (isLoading) {
29
35
  return (_jsx(Stack, { gap: "md", children: Array.from({ length: 4 }).map((_, i) => (_jsxs(Div, { className: "rounded-xl border border-neutral-200 dark:border-zinc-700 p-5", children: [_jsxs(Div, { className: "flex gap-3", children: [_jsx(Skeleton, { className: "h-9 w-9 rounded-full" }), _jsxs(Stack, { gap: "sm", className: "flex-1", children: [_jsx(Skeleton, { className: "h-4 w-24 rounded" }), _jsx(Skeleton, { className: "h-3 w-16 rounded" })] })] }), _jsxs(Stack, { gap: "xs", className: "mt-3", children: [_jsx(Skeleton, { className: "h-3 w-full rounded" }), _jsx(Skeleton, { className: "h-3 w-3/4 rounded" })] })] }, i))) }));
30
36
  }
31
37
  if (reviews.length === 0) {
32
38
  return (_jsx(Text, { className: "py-12 text-center text-sm text-neutral-500 dark:text-zinc-500", children: emptyLabel }));
33
39
  }
34
- return (_jsxs(Stack, { gap: "lg", children: [_jsx(Stack, { gap: "md", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) }), totalPages > 1 && onPageChange && (_jsx(Div, { className: "flex justify-center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) }))] }));
40
+ return (_jsxs(Stack, { gap: "lg", children: [_jsx(Stack, { gap: "md", children: reviews.map((review) => (_jsx(ReviewCard, { review: review, context: context }, review.id))) }), totalPages > 1 && onPageChange && (_jsx(Div, { className: "flex justify-center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) }))] }));
35
41
  }
@@ -37,6 +37,7 @@ export interface ReviewDocument {
37
37
  reportCount: number;
38
38
  verified: boolean;
39
39
  featured?: boolean;
40
+ isAnonymous?: boolean;
40
41
  sellerReply?: string;
41
42
  sellerRepliedAt?: Date;
42
43
  createdAt: Date;
@@ -109,18 +109,18 @@ export declare const reviewSchema: z.ZodObject<{
109
109
  duration?: number | undefined;
110
110
  } | undefined;
111
111
  productTitle?: string | undefined;
112
+ featured?: boolean | undefined;
113
+ storeName?: string | undefined;
112
114
  images?: {
113
115
  url: string;
114
116
  thumbnailUrl?: string | undefined;
115
117
  }[] | undefined;
116
- storeName?: string | undefined;
117
- featured?: boolean | undefined;
118
118
  storeSlug?: string | undefined;
119
- verified?: boolean | undefined;
120
119
  userAvatar?: string | undefined;
121
120
  comment?: string | undefined;
122
121
  helpfulCount?: number | undefined;
123
122
  reportCount?: number | undefined;
123
+ verified?: boolean | undefined;
124
124
  }, {
125
125
  id: string;
126
126
  status: "pending" | "approved" | "rejected";
@@ -139,18 +139,18 @@ export declare const reviewSchema: z.ZodObject<{
139
139
  duration?: number | undefined;
140
140
  } | undefined;
141
141
  productTitle?: string | undefined;
142
+ featured?: boolean | undefined;
143
+ storeName?: string | undefined;
142
144
  images?: {
143
145
  url: string;
144
146
  thumbnailUrl?: string | undefined;
145
147
  }[] | undefined;
146
- storeName?: string | undefined;
147
- featured?: boolean | undefined;
148
148
  storeSlug?: string | undefined;
149
- verified?: boolean | undefined;
150
149
  userAvatar?: string | undefined;
151
150
  comment?: string | undefined;
152
151
  helpfulCount?: number | undefined;
153
152
  reportCount?: number | undefined;
153
+ verified?: boolean | undefined;
154
154
  }>;
155
155
  /** Base schema for list-query parameters. */
156
156
  export declare const reviewListParamsSchema: z.ZodObject<{
@@ -170,9 +170,9 @@ export declare const reviewListParamsSchema: z.ZodObject<{
170
170
  status?: "pending" | "approved" | "rejected" | undefined;
171
171
  page?: number | undefined;
172
172
  productId?: string | undefined;
173
+ featured?: boolean | undefined;
173
174
  rating?: number | undefined;
174
175
  userId?: string | undefined;
175
- featured?: boolean | undefined;
176
176
  }, {
177
177
  sort?: string | undefined;
178
178
  perPage?: number | undefined;
@@ -180,7 +180,7 @@ export declare const reviewListParamsSchema: z.ZodObject<{
180
180
  status?: "pending" | "approved" | "rejected" | undefined;
181
181
  page?: number | undefined;
182
182
  productId?: string | undefined;
183
+ featured?: boolean | undefined;
183
184
  rating?: number | undefined;
184
185
  userId?: string | undefined;
185
- featured?: boolean | undefined;
186
186
  }>;
@@ -29,6 +29,7 @@ export interface Review {
29
29
  reportCount?: number;
30
30
  verified?: boolean;
31
31
  featured?: boolean;
32
+ isAnonymous?: boolean;
32
33
  createdAt?: string;
33
34
  updatedAt?: string;
34
35
  }