@mohasinac/appkit 2.3.1 → 2.3.2

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 (253) hide show
  1. package/dist/client.d.ts +12 -8
  2. package/dist/client.js +7 -4
  3. package/dist/constants/api-endpoints.d.ts +4 -0
  4. package/dist/constants/api-endpoints.js +2 -0
  5. package/dist/core/contact-submissions.repository.d.ts +32 -0
  6. package/dist/core/contact-submissions.repository.js +49 -0
  7. package/dist/core/index.d.ts +2 -0
  8. package/dist/core/index.js +1 -0
  9. package/dist/features/about/components/HowPayoutsWorkView.js +1 -1
  10. package/dist/features/account/components/AddressFilters.d.ts +5 -0
  11. package/dist/features/account/components/AddressFilters.js +20 -0
  12. package/dist/features/account/components/AddressesIndexListing.d.ts +6 -0
  13. package/dist/features/account/components/AddressesIndexListing.js +51 -0
  14. package/dist/features/account/components/UserSidebar.d.ts +7 -3
  15. package/dist/features/account/components/UserSidebar.js +55 -7
  16. package/dist/features/account/hooks/useAddresses.d.ts +7 -0
  17. package/dist/features/account/hooks/useAddresses.js +12 -1
  18. package/dist/features/admin/actions/admin-read-actions.d.ts +12 -0
  19. package/dist/features/admin/actions/admin-read-actions.js +18 -0
  20. package/dist/features/admin/components/AdminBlogView.js +26 -2
  21. package/dist/features/admin/components/AdminCarouselView.js +18 -2
  22. package/dist/features/admin/components/AdminCategoriesView.js +27 -2
  23. package/dist/features/admin/components/AdminContactView.d.ts +4 -0
  24. package/dist/features/admin/components/AdminContactView.js +50 -0
  25. package/dist/features/admin/components/AdminFaqsView.js +19 -2
  26. package/dist/features/admin/components/AdminListingScaffold.d.ts +11 -2
  27. package/dist/features/admin/components/AdminListingScaffold.js +14 -3
  28. package/dist/features/admin/components/AdminNewsletterView.d.ts +4 -0
  29. package/dist/features/admin/components/AdminNewsletterView.js +50 -0
  30. package/dist/features/admin/components/AdminProductsView.js +30 -2
  31. package/dist/features/admin/components/AdminReviewsView.js +26 -2
  32. package/dist/features/admin/components/AdminStoresView.js +17 -2
  33. package/dist/features/admin/components/AdminUsersView.js +27 -2
  34. package/dist/features/admin/components/DataTable.d.ts +2 -1
  35. package/dist/features/admin/components/DataTable.js +18 -4
  36. package/dist/features/admin/components/index.d.ts +6 -0
  37. package/dist/features/admin/components/index.js +3 -0
  38. package/dist/features/admin/hooks/useAdminListingData.d.ts +3 -1
  39. package/dist/features/admin/hooks/useAdminListingData.js +12 -7
  40. package/dist/features/admin/server.d.ts +3 -0
  41. package/dist/features/admin/server.js +2 -0
  42. package/dist/features/auctions/components/AuctionDetailPageView.js +93 -47
  43. package/dist/features/auctions/components/AuctionFilters.d.ts +8 -0
  44. package/dist/features/auctions/components/AuctionFilters.js +12 -0
  45. package/dist/features/auctions/components/AuctionsListView.d.ts +6 -1
  46. package/dist/features/auctions/components/AuctionsListView.js +37 -5
  47. package/dist/features/auctions/schemas/index.d.ts +4 -4
  48. package/dist/features/blog/components/BlogFeaturedCard.d.ts +1 -7
  49. package/dist/features/blog/components/BlogFeaturedCard.js +4 -5
  50. package/dist/features/blog/components/BlogFilters.js +2 -1
  51. package/dist/features/blog/components/BlogIndexListing.js +14 -9
  52. package/dist/features/blog/components/BlogIndexPageView.d.ts +6 -1
  53. package/dist/features/blog/components/BlogIndexPageView.js +10 -2
  54. package/dist/features/blog/components/BlogListView.d.ts +2 -1
  55. package/dist/features/blog/components/BlogListView.js +10 -3
  56. package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -1
  57. package/dist/features/categories/components/CategoriesIndexListing.js +41 -38
  58. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +6 -1
  59. package/dist/features/categories/components/CategoriesIndexPageView.js +41 -2
  60. package/dist/features/categories/components/CategoryDetailPageView.js +13 -6
  61. package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -0
  62. package/dist/features/categories/components/CategoryDetailTabs.js +17 -0
  63. package/dist/features/categories/components/CategoryFilters.js +2 -1
  64. package/dist/features/categories/components/CategoryGrid.d.ts +2 -1
  65. package/dist/features/categories/components/CategoryGrid.js +8 -6
  66. package/dist/features/categories/components/CategoryProductsListing.js +22 -11
  67. package/dist/features/categories/components/index.d.ts +2 -0
  68. package/dist/features/categories/components/index.js +1 -0
  69. package/dist/features/categories/hooks/useCategories.d.ts +20 -0
  70. package/dist/features/categories/hooks/useCategories.js +50 -1
  71. package/dist/features/categories/hooks/useCategoryTree.d.ts +17 -0
  72. package/dist/features/categories/hooks/useCategoryTree.js +65 -0
  73. package/dist/features/events/components/AdminEventEditorView.d.ts +6 -0
  74. package/dist/features/events/components/AdminEventEditorView.js +203 -0
  75. package/dist/features/events/components/AdminEventsView.js +28 -2
  76. package/dist/features/events/components/EventCard.js +4 -2
  77. package/dist/features/events/components/EventFilters.js +2 -1
  78. package/dist/features/events/components/EventsIndexListing.js +40 -10
  79. package/dist/features/events/components/EventsListPageView.d.ts +6 -1
  80. package/dist/features/events/components/EventsListPageView.js +40 -7
  81. package/dist/features/events/components/index.d.ts +2 -0
  82. package/dist/features/events/components/index.js +1 -0
  83. package/dist/features/events/hooks/useEvents.js +2 -0
  84. package/dist/features/events/types/index.d.ts +1 -0
  85. package/dist/features/homepage/components/BlogArticlesSection.js +1 -1
  86. package/dist/features/homepage/components/EventsSection.js +1 -1
  87. package/dist/features/homepage/components/FeaturedAuctionsSection.js +1 -1
  88. package/dist/features/homepage/components/FeaturedPreOrdersSection.js +1 -1
  89. package/dist/features/homepage/components/FeaturedProductsSection.js +1 -1
  90. package/dist/features/homepage/components/FeaturedStoresSection.js +1 -1
  91. package/dist/features/homepage/components/HeroCarousel.js +1 -1
  92. package/dist/features/homepage/components/MarketplaceHomepageView.js +27 -17
  93. package/dist/features/homepage/components/SectionCarousel.js +4 -4
  94. package/dist/features/homepage/components/ShopByCategorySection.js +2 -2
  95. package/dist/features/homepage/schemas/firestore.d.ts +1 -1
  96. package/dist/features/homepage/schemas/firestore.js +2 -1
  97. package/dist/features/layout/AppLayoutShell.d.ts +6 -2
  98. package/dist/features/layout/AppLayoutShell.js +7 -3
  99. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +3 -1
  100. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +6 -2
  101. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +80 -12
  102. package/dist/features/pre-orders/components/PreOrderFilters.d.ts +8 -0
  103. package/dist/features/pre-orders/components/PreOrderFilters.js +21 -0
  104. package/dist/features/pre-orders/components/PreOrdersIndexListing.d.ts +2 -1
  105. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +69 -10
  106. package/dist/features/pre-orders/components/PreOrdersListView.d.ts +6 -1
  107. package/dist/features/pre-orders/components/PreOrdersListView.js +26 -7
  108. package/dist/features/pre-orders/components/index.d.ts +2 -0
  109. package/dist/features/pre-orders/components/index.js +1 -0
  110. package/dist/features/products/components/AuctionsIndexListing.d.ts +2 -1
  111. package/dist/features/products/components/AuctionsIndexListing.js +61 -9
  112. package/dist/features/products/components/InteractiveProductCard.d.ts +2 -4
  113. package/dist/features/products/components/InteractiveProductCard.js +2 -2
  114. package/dist/features/products/components/ProductDetailPageView.d.ts +1 -1
  115. package/dist/features/products/components/ProductDetailPageView.js +116 -25
  116. package/dist/features/products/components/ProductFilters.d.ts +6 -11
  117. package/dist/features/products/components/ProductFilters.js +5 -3
  118. package/dist/features/products/components/ProductGrid.d.ts +8 -2
  119. package/dist/features/products/components/ProductGrid.js +20 -5
  120. package/dist/features/products/components/ProductTabsShell.d.ts +3 -11
  121. package/dist/features/products/components/ProductTabsShell.js +14 -14
  122. package/dist/features/products/components/ProductsIndexListing.js +73 -9
  123. package/dist/features/products/components/ProductsIndexPageView.d.ts +6 -1
  124. package/dist/features/products/components/ProductsIndexPageView.js +39 -6
  125. package/dist/features/products/components/RelatedProductsCarousel.d.ts +7 -0
  126. package/dist/features/products/components/RelatedProductsCarousel.js +11 -0
  127. package/dist/features/products/components/index.d.ts +1 -0
  128. package/dist/features/products/components/index.js +1 -0
  129. package/dist/features/products/hooks/useProducts.js +16 -0
  130. package/dist/features/products/repository/products.repository.d.ts +8 -0
  131. package/dist/features/products/repository/products.repository.js +2 -0
  132. package/dist/features/products/schemas/index.d.ts +8 -8
  133. package/dist/features/products/types/index.d.ts +12 -0
  134. package/dist/features/promotions/components/CouponsIndexListing.d.ts +9 -0
  135. package/dist/features/promotions/components/CouponsIndexListing.js +86 -0
  136. package/dist/features/promotions/components/PromotionsView.d.ts +11 -5
  137. package/dist/features/promotions/components/PromotionsView.js +6 -1
  138. package/dist/features/promotions/components/index.d.ts +4 -2
  139. package/dist/features/promotions/components/index.js +2 -1
  140. package/dist/features/reviews/components/ReviewDetailPageView.d.ts +4 -0
  141. package/dist/features/reviews/components/ReviewDetailPageView.js +20 -0
  142. package/dist/features/reviews/components/ReviewDetailShell.d.ts +7 -0
  143. package/dist/features/reviews/components/ReviewDetailShell.js +80 -0
  144. package/dist/features/reviews/components/ReviewFilters.d.ts +3 -3
  145. package/dist/features/reviews/components/ReviewFilters.js +5 -4
  146. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +4 -3
  147. package/dist/features/reviews/components/ReviewsIndexListing.js +35 -51
  148. package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +6 -1
  149. package/dist/features/reviews/components/ReviewsIndexPageView.js +49 -3
  150. package/dist/features/reviews/components/ReviewsList.js +9 -1
  151. package/dist/features/reviews/components/index.d.ts +1 -0
  152. package/dist/features/reviews/hooks/useReviews.js +15 -1
  153. package/dist/features/reviews/types/index.d.ts +6 -1
  154. package/dist/features/seller/components/SellerSidebar.d.ts +8 -4
  155. package/dist/features/seller/components/SellerSidebar.js +6 -4
  156. package/dist/features/seller/components/index.d.ts +30 -0
  157. package/dist/features/seller/components/index.js +17 -0
  158. package/dist/features/seller/hooks/useSellerStore.d.ts +2 -0
  159. package/dist/features/seller/hooks/useSellerStore.js +2 -0
  160. package/dist/features/seller/permission-map.d.ts +4 -2
  161. package/dist/features/seller/permission-map.js +16 -14
  162. package/dist/features/seller/schemas/index.d.ts +2 -2
  163. package/dist/features/stores/api/[storeSlug]/reviews/route.d.ts +1 -1
  164. package/dist/features/stores/api/[storeSlug]/reviews/route.js +24 -19
  165. package/dist/features/stores/components/InteractiveStoreCard.d.ts +0 -5
  166. package/dist/features/stores/components/InteractiveStoreCard.js +9 -9
  167. package/dist/features/stores/components/StoreAuctionsListing.js +27 -9
  168. package/dist/features/stores/components/StoreDetailLayoutView.js +2 -0
  169. package/dist/features/stores/components/StoreFilters.d.ts +5 -0
  170. package/dist/features/stores/components/StoreFilters.js +20 -0
  171. package/dist/features/stores/components/StoreHeader.js +2 -2
  172. package/dist/features/stores/components/StorePreOrdersListing.d.ts +5 -0
  173. package/dist/features/stores/components/StorePreOrdersListing.js +40 -0
  174. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -0
  175. package/dist/features/stores/components/StorePreOrdersPageView.js +21 -0
  176. package/dist/features/stores/components/StoreProductsListing.js +21 -11
  177. package/dist/features/stores/components/StoreReviewsListing.js +2 -7
  178. package/dist/features/stores/components/StoresIndexListing.js +42 -8
  179. package/dist/features/stores/components/StoresIndexPageView.d.ts +6 -1
  180. package/dist/features/stores/components/StoresIndexPageView.js +9 -2
  181. package/dist/features/stores/components/index.d.ts +3 -0
  182. package/dist/features/stores/components/index.js +1 -0
  183. package/dist/features/stores/hooks/useStores.d.ts +7 -1
  184. package/dist/features/stores/hooks/useStores.js +16 -3
  185. package/dist/features/stores/schemas/index.d.ts +2 -2
  186. package/dist/features/stores/types/index.d.ts +3 -0
  187. package/dist/features/wishlist/hooks/useGuestWishlist.d.ts +20 -0
  188. package/dist/features/wishlist/hooks/useGuestWishlist.js +49 -0
  189. package/dist/features/wishlist/hooks/useWishlistCount.d.ts +7 -0
  190. package/dist/features/wishlist/hooks/useWishlistCount.js +31 -0
  191. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +56 -0
  192. package/dist/features/wishlist/hooks/useWishlistWithGuest.js +57 -0
  193. package/dist/features/wishlist/index.d.ts +3 -0
  194. package/dist/features/wishlist/index.js +3 -0
  195. package/dist/features/wishlist/utils/guest-wishlist.d.ts +22 -0
  196. package/dist/features/wishlist/utils/guest-wishlist.js +70 -0
  197. package/dist/index.d.ts +50 -1
  198. package/dist/index.js +63 -1
  199. package/dist/next/routing/route-map.d.ts +70 -36
  200. package/dist/next/routing/route-map.js +30 -22
  201. package/dist/seed/addresses-seed-data.js +62 -261
  202. package/dist/seed/beyblade-seed-data.d.ts +7 -0
  203. package/dist/seed/beyblade-seed-data.js +947 -0
  204. package/dist/seed/bids-seed-data.d.ts +10 -2
  205. package/dist/seed/bids-seed-data.js +220 -1071
  206. package/dist/seed/blog-posts-seed-data.d.ts +2 -2
  207. package/dist/seed/blog-posts-seed-data.js +455 -117
  208. package/dist/seed/cart-seed-data.d.ts +9 -9
  209. package/dist/seed/cart-seed-data.js +73 -74
  210. package/dist/seed/coupons-seed-data.d.ts +3 -4
  211. package/dist/seed/coupons-seed-data.js +3 -509
  212. package/dist/seed/events-seed-data.d.ts +2 -2
  213. package/dist/seed/events-seed-data.js +315 -476
  214. package/dist/seed/faq-seed-data.d.ts +18 -41
  215. package/dist/seed/faq-seed-data.js +1059 -1172
  216. package/dist/seed/hot-wheels-seed-data.d.ts +7 -0
  217. package/dist/seed/hot-wheels-seed-data.js +1365 -0
  218. package/dist/seed/index.d.ts +6 -1
  219. package/dist/seed/index.js +6 -1
  220. package/dist/seed/pokemon-carousel-slides-seed-data.d.ts +4 -2
  221. package/dist/seed/pokemon-carousel-slides-seed-data.js +152 -268
  222. package/dist/seed/pokemon-categories-seed-data.d.ts +18 -21
  223. package/dist/seed/pokemon-categories-seed-data.js +424 -1004
  224. package/dist/seed/pokemon-coupons-seed-data.d.ts +6 -0
  225. package/dist/seed/pokemon-coupons-seed-data.js +465 -0
  226. package/dist/seed/pokemon-homepage-sections-seed-data.d.ts +3 -2
  227. package/dist/seed/pokemon-homepage-sections-seed-data.js +67 -289
  228. package/dist/seed/pokemon-products-seed-data.js +662 -0
  229. package/dist/seed/pokemon-seed-bundle.d.ts +32 -11
  230. package/dist/seed/pokemon-seed-bundle.js +41 -11
  231. package/dist/seed/pokemon-stores-seed-data.d.ts +2 -3
  232. package/dist/seed/pokemon-stores-seed-data.js +56 -31
  233. package/dist/seed/pokemon-users-seed-data.d.ts +2 -2
  234. package/dist/seed/pokemon-users-seed-data.js +245 -261
  235. package/dist/seed/reviews-seed-data.d.ts +17 -2
  236. package/dist/seed/reviews-seed-data.js +519 -483
  237. package/dist/seed/site-settings-seed-data.js +14 -14
  238. package/dist/seed/store-addresses-seed-data.js +68 -50
  239. package/dist/seed/transformers-seed-data.d.ts +7 -0
  240. package/dist/seed/transformers-seed-data.js +510 -0
  241. package/dist/seed/wishlists-seed-data.d.ts +5 -1
  242. package/dist/seed/wishlists-seed-data.js +82 -4
  243. package/dist/server.d.ts +1 -0
  244. package/dist/server.js +2 -0
  245. package/dist/tokens/index.d.ts +6 -0
  246. package/dist/tokens/index.js +2 -0
  247. package/dist/ui/components/BaseListingCard.js +24 -26
  248. package/dist/ui/components/BaseListingCard.style.css +5 -5
  249. package/dist/ui/components/HorizontalScroller.d.ts +1 -1
  250. package/dist/ui/components/HorizontalScroller.js +19 -5
  251. package/dist/ui/components/SideDrawer.style.css +3 -11
  252. package/dist/ui/rich-text/RichText.js +19 -1
  253. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useCallback } from "react";
4
+ import { Search, SlidersHorizontal, X } from "lucide-react";
5
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
+ import { Pagination, SortDropdown, Div, Text, Heading } from "../../../ui";
7
+ import { usePromotions } from "../hooks/usePromotions";
8
+ import { CouponCard } from "./CouponCard";
9
+ const COUPON_SORT_OPTIONS = [
10
+ { value: "name", label: "Name A–Z" },
11
+ { value: "-name", label: "Name Z–A" },
12
+ { value: "-validity.endDate", label: "Expiring Soon" },
13
+ { value: "-createdAt", label: "Newest First" },
14
+ { value: "createdAt", label: "Oldest First" },
15
+ ];
16
+ const COUPON_TYPES = [
17
+ { value: "percentage", label: "% Off" },
18
+ { value: "fixed", label: "Fixed Amount" },
19
+ { value: "free_shipping", label: "Free Shipping" },
20
+ { value: "buy_x_get_y", label: "Buy X Get Y" },
21
+ ];
22
+ export function CouponsIndexListing({ initialCoupons, storeSlug, sellerId, }) {
23
+ const table = useUrlTable({ defaults: { pageSize: "12", sort: "-createdAt" } });
24
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
25
+ const [filterOpen, setFilterOpen] = useState(false);
26
+ const [copiedCode, setCopiedCode] = useState(null);
27
+ // Build Sieve filter string
28
+ const buildFilters = () => {
29
+ const parts = ["validity.isActive==true"];
30
+ const typeFilter = table.get("type");
31
+ if (typeFilter)
32
+ parts.push(`type==${typeFilter}`);
33
+ const dateFrom = table.get("dateFrom");
34
+ if (dateFrom)
35
+ parts.push(`validity.startDate>=${dateFrom}`);
36
+ const dateTo = table.get("dateTo");
37
+ if (dateTo)
38
+ parts.push(`validity.endDate<=${dateTo}`);
39
+ if (sellerId)
40
+ parts.push(`sellerId==${sellerId}`);
41
+ return parts.join(",");
42
+ };
43
+ const { promotions: coupons, total, totalPages, isLoading } = usePromotions({
44
+ page: table.getNumber("page", 1),
45
+ pageSize: table.getNumber("pageSize", 12),
46
+ sort: table.get("sort") || "-createdAt",
47
+ filters: buildFilters(),
48
+ });
49
+ // Use initial data on first load if available and no search/filter active
50
+ const displayCoupons = !isLoading && coupons.length > 0
51
+ ? coupons
52
+ : !isLoading && initialCoupons && !table.get("q") && !table.get("type")
53
+ ? initialCoupons
54
+ : coupons;
55
+ const commitSearch = useCallback(() => {
56
+ table.set("q", searchInput.trim());
57
+ table.setPage(1);
58
+ }, [searchInput, table]);
59
+ const handleKeyDown = (e) => {
60
+ if (e.key === "Enter")
61
+ commitSearch();
62
+ };
63
+ const handleCopy = useCallback((code) => {
64
+ navigator.clipboard.writeText(code).catch(() => { });
65
+ setCopiedCode(code);
66
+ setTimeout(() => setCopiedCode(null), 2000);
67
+ }, []);
68
+ const activeType = table.get("type");
69
+ const hasActiveFilters = !!activeType || !!table.get("dateFrom") || !!table.get("dateTo");
70
+ const clearFilters = () => {
71
+ table.set("type", "");
72
+ table.set("dateFrom", "");
73
+ table.set("dateTo", "");
74
+ table.setPage(1);
75
+ };
76
+ return (_jsxs("div", { className: "min-h-[40vh]", children: [_jsxs("div", { className: "sticky top-0 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
77
+ ? "border-primary bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
78
+ : "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", ""); table.setPage(1); }, 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); table.setPage(1); }, 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", ""); table.setPage(1); }, "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", ""); table.setPage(1); }, "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", ""); table.setPage(1); }, "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: {
79
+ copy: copiedCode === coupon.code ? "Copied!" : "Copy",
80
+ copied: "Copied!",
81
+ expires: "Expires",
82
+ minOrder: "Min. order",
83
+ off: "OFF",
84
+ freeShipping: "Free Shipping",
85
+ }, onCopy: handleCopy }, 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); table.setPage(1); }, className: "accent-primary" }), t.label] }, t.value))), table.get("type") && (_jsx("button", { type: "button", onClick: () => { table.set("type", ""); table.setPage(1); }, 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); table.setPage(1); }, 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); table.setPage(1); }, 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" })] })] })] }))] }));
86
+ }
@@ -9,12 +9,20 @@ export interface PromotionProductItem {
9
9
  export interface PromotionsViewProductSectionProps {
10
10
  title: string;
11
11
  subtitle?: string;
12
- /** Render the product grid — use your app's ProductGrid/ProductCard here */
13
12
  renderProducts: () => React.ReactNode;
14
- /** If false/undefined, the section is hidden */
15
13
  hasProducts?: boolean;
16
14
  }
17
15
  export declare function PromotionsViewProductSection({ title, subtitle, renderProducts, hasProducts, }: PromotionsViewProductSectionProps): import("react/jsx-runtime").JSX.Element | null;
16
+ export interface PromotionsHeroProps {
17
+ labels: {
18
+ exclusiveOffersBadge: string;
19
+ title: string;
20
+ subtitle: string;
21
+ };
22
+ heroBannerClass?: string;
23
+ }
24
+ /** Slim hero banner for the promotions page — tabs + content live in the page. */
25
+ export declare function PromotionsHero({ labels, heroBannerClass, }: PromotionsHeroProps): import("react/jsx-runtime").JSX.Element;
18
26
  export interface PromotionsViewProps {
19
27
  labels: {
20
28
  exclusiveOffersBadge: string;
@@ -32,12 +40,10 @@ export interface PromotionsViewProps {
32
40
  };
33
41
  hasContent: boolean;
34
42
  heroBannerClass?: string;
35
- /** Render the coupon grid */
36
43
  renderCoupons?: () => React.ReactNode;
37
44
  couponsCount?: number;
38
- /** Render the promoted products section */
39
45
  renderDealsSection?: () => React.ReactNode;
40
- /** Render the featured products section */
41
46
  renderFeaturedSection?: () => React.ReactNode;
42
47
  }
48
+ /** @deprecated Use PromotionsHero + per-tab content in your page instead. */
43
49
  export declare function PromotionsView({ labels, hasContent, heroBannerClass, renderCoupons, couponsCount, renderDealsSection, renderFeaturedSection, }: PromotionsViewProps): import("react/jsx-runtime").JSX.Element;
@@ -5,6 +5,11 @@ export function PromotionsViewProductSection({ title, subtitle, renderProducts,
5
5
  return null;
6
6
  return (_jsxs(Section, { children: [_jsxs(Div, { className: "mb-6", children: [_jsx(Heading, { level: 2, children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-1", children: subtitle }))] }), renderProducts()] }));
7
7
  }
8
+ /** Slim hero banner for the promotions page — tabs + content live in the page. */
9
+ export function PromotionsHero({ labels, heroBannerClass = "bg-gradient-to-br from-rose-500 to-orange-500", }) {
10
+ return (_jsx(Div, { className: `${heroBannerClass} text-white py-14`, children: _jsxs(Div, { className: "max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center", children: [_jsxs(Text, { className: "text-white font-medium mb-2 uppercase tracking-widest text-sm", children: ["\uD83C\uDF89 ", labels.exclusiveOffersBadge] }), _jsx(Heading, { level: 1, variant: "none", className: "text-white mb-4", children: labels.title }), _jsx(Text, { variant: "none", className: "text-lg text-white/90 max-w-2xl mx-auto", children: labels.subtitle })] }) }));
11
+ }
12
+ /** @deprecated Use PromotionsHero + per-tab content in your page instead. */
8
13
  export function PromotionsView({ labels, hasContent, heroBannerClass = "bg-gradient-to-br from-rose-500 to-orange-500", renderCoupons, couponsCount = 0, renderDealsSection, renderFeaturedSection, }) {
9
- return (_jsxs(Div, { className: "min-h-screen bg-white dark:bg-slate-900", children: [_jsx(Div, { className: `${heroBannerClass} text-white py-16`, children: _jsxs(Div, { className: "max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center", children: [_jsxs(Text, { className: "text-white font-medium mb-2 uppercase tracking-widest text-sm", children: ["\uD83C\uDF89 ", labels.exclusiveOffersBadge] }), _jsx(Heading, { level: 1, variant: "none", className: "text-white mb-4", children: labels.title }), _jsx(Text, { variant: "none", className: "text-lg text-white/90 max-w-2xl mx-auto", children: labels.subtitle })] }) }), _jsxs(Div, { className: "max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-12", children: [!hasContent && (_jsxs(Div, { className: "text-center py-16", children: [_jsx(Heading, { level: 2, className: "mb-2", children: labels.emptyDeals }), _jsx(Text, { variant: "secondary", children: labels.checkBack })] })), hasContent && (_jsxs(Div, { className: "space-y-12", children: [_jsxs(Section, { children: [_jsxs(Div, { className: "mb-6", children: [_jsx(Heading, { level: 2, children: labels.couponsTitle }), couponsCount > 0 && (_jsx(Text, { variant: "secondary", className: "mt-1", children: labels.couponsSubtitle }))] }), couponsCount > 0 ? (renderCoupons?.()) : (_jsx(Text, { variant: "secondary", size: "sm", children: labels.emptyCoupons }))] }), renderDealsSection?.(), renderFeaturedSection?.()] }))] })] }));
14
+ return (_jsxs(Div, { className: "min-h-screen bg-white dark:bg-slate-900", children: [_jsx(PromotionsHero, { labels: labels, heroBannerClass: heroBannerClass }), _jsxs(Div, { className: "max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-12", children: [!hasContent && (_jsxs(Div, { className: "text-center py-16", children: [_jsx(Heading, { level: 2, className: "mb-2", children: labels.emptyDeals }), _jsx(Text, { variant: "secondary", children: labels.checkBack })] })), hasContent && (_jsxs(Div, { className: "space-y-12", children: [renderCoupons && (_jsxs(Section, { children: [_jsxs(Div, { className: "mb-6", children: [_jsx(Heading, { level: 2, children: labels.couponsTitle }), couponsCount > 0 && (_jsx(Text, { variant: "secondary", className: "mt-1", children: labels.couponsSubtitle }))] }), couponsCount > 0 ? (renderCoupons()) : (_jsx(Text, { variant: "secondary", size: "sm", children: labels.emptyCoupons }))] })), renderDealsSection?.(), renderFeaturedSection?.()] }))] })] }));
10
15
  }
@@ -1,3 +1,5 @@
1
1
  export { CouponCard } from "./CouponCard";
2
- export { PromotionsView, PromotionsViewProductSection } from "./PromotionsView";
3
- export type { PromotionsViewProps, PromotionsViewProductSectionProps, PromotionProductItem, } from "./PromotionsView";
2
+ export { CouponsIndexListing } from "./CouponsIndexListing";
3
+ export type { CouponsIndexListingProps } from "./CouponsIndexListing";
4
+ export { PromotionsView, PromotionsViewProductSection, PromotionsHero, } from "./PromotionsView";
5
+ export type { PromotionsViewProps, PromotionsViewProductSectionProps, PromotionProductItem, PromotionsHeroProps, } from "./PromotionsView";
@@ -1,2 +1,3 @@
1
1
  export { CouponCard } from "./CouponCard";
2
- export { PromotionsView, PromotionsViewProductSection } from "./PromotionsView";
2
+ export { CouponsIndexListing } from "./CouponsIndexListing";
3
+ export { PromotionsView, PromotionsViewProductSection, PromotionsHero, } from "./PromotionsView";
@@ -0,0 +1,4 @@
1
+ export interface ReviewDetailPageViewProps {
2
+ id: string;
3
+ }
4
+ export declare function ReviewDetailPageView({ id }: ReviewDetailPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from "next/link";
3
+ import { reviewRepository, storeRepository } from "../../../repositories";
4
+ import { Container, Main, Section } from "../../../ui";
5
+ import { ROUTES } from "../../../next";
6
+ import { ReviewDetailShell } from "./ReviewDetailShell";
7
+ export async function ReviewDetailPageView({ id }) {
8
+ const review = await reviewRepository.findById(id).catch(() => null);
9
+ // Resolve store slug from sellerId so the shell can link to the store page
10
+ const storeSlug = review?.sellerId
11
+ ? await storeRepository
12
+ .findAll({ filters: `ownerId==${review.sellerId}`, pageSize: 1 })
13
+ .then((r) => r?.items?.[0]?.storeSlug ?? null)
14
+ .catch(() => null)
15
+ : null;
16
+ if (!review || review.status !== "approved") {
17
+ return (_jsx(Main, { children: _jsx(Section, { className: "py-24", children: _jsx(Container, { size: "sm", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-5xl mb-4", "aria-hidden": "true", children: "\uD83D\uDD0D" }), _jsx("h1", { className: "text-2xl font-bold text-neutral-900 dark:text-white mb-2", children: "Review not found" }), _jsx("p", { className: "text-sm text-neutral-500 dark:text-zinc-400 mb-6", 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-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-600 transition-colors", children: "\u2190 Back to Reviews" })] }) }) }) }));
18
+ }
19
+ return (_jsxs(Main, { children: [_jsx("div", { className: "border-b border-neutral-100 dark:border-zinc-800 bg-neutral-50 dark:bg-zinc-950 py-2.5 px-4", children: _jsxs("nav", { className: "mx-auto max-w-3xl flex items-center gap-1.5 text-xs text-neutral-400 dark:text-zinc-500", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary transition-colors", children: "Home" }), _jsx("span", { children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "hover:text-primary transition-colors", children: "Reviews" }), _jsx("span", { children: "/" }), _jsx("span", { className: "text-neutral-700 dark:text-zinc-300 truncate max-w-[200px]", children: review.title ?? review.id })] }) }), _jsx(ReviewDetailShell, { review: review, storeHref: storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null })] }));
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { Review } from "../types";
2
+ interface ReviewDetailShellProps {
3
+ review: Review;
4
+ storeHref?: string | null;
5
+ }
6
+ export declare function ReviewDetailShell({ review, storeHref }: ReviewDetailShellProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import Link from "next/link";
5
+ import { StarRating } from "../../../ui";
6
+ import { RichText } from "../../../ui";
7
+ import { maskName } from "../../../security";
8
+ import { getDefaultLocale } from "../../../core/baseline-resolver";
9
+ import { normalizeRichTextHtml } from "../../../utils/string.formatter";
10
+ import { ROUTES } from "../../../next";
11
+ import { apiClient } from "../../../http";
12
+ import { REVIEW_ENDPOINTS } from "../../../constants/api-endpoints";
13
+ export function ReviewDetailShell({ review, storeHref }) {
14
+ const displayName = maskName(review.userName);
15
+ const initials = displayName.charAt(0).toUpperCase();
16
+ const date = review.createdAt
17
+ ? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
18
+ year: "numeric",
19
+ month: "long",
20
+ day: "numeric",
21
+ })
22
+ : "";
23
+ // ── Lightbox ────────────────────────────────────────────────────────────────
24
+ const images = review.images ?? [];
25
+ const [lightboxIdx, setLightboxIdx] = useState(null);
26
+ const closeLightbox = useCallback(() => setLightboxIdx(null), []);
27
+ const prevImage = useCallback(() => setLightboxIdx((i) => (i === null ? null : (i - 1 + images.length) % images.length)), [images.length]);
28
+ const nextImage = useCallback(() => setLightboxIdx((i) => (i === null ? null : (i + 1) % images.length)), [images.length]);
29
+ useEffect(() => {
30
+ if (lightboxIdx === null)
31
+ return;
32
+ const onKey = (e) => {
33
+ if (e.key === "Escape")
34
+ closeLightbox();
35
+ if (e.key === "ArrowLeft")
36
+ prevImage();
37
+ if (e.key === "ArrowRight")
38
+ nextImage();
39
+ };
40
+ window.addEventListener("keydown", onKey);
41
+ return () => window.removeEventListener("keydown", onKey);
42
+ }, [lightboxIdx, closeLightbox, prevImage, nextImage]);
43
+ // ── Helpful vote ────────────────────────────────────────────────────────────
44
+ const storageKey = `review_voted_${review.id}`;
45
+ const [helpfulCount, setHelpfulCount] = useState(review.helpfulCount ?? 0);
46
+ const [voted, setVoted] = useState(false);
47
+ const [voting, setVoting] = useState(false);
48
+ useEffect(() => {
49
+ setVoted(localStorage.getItem(storageKey) === "1");
50
+ }, [storageKey]);
51
+ const handleVote = async () => {
52
+ if (voted || voting)
53
+ return;
54
+ setVoting(true);
55
+ try {
56
+ await apiClient.post(`${REVIEW_ENDPOINTS.LIST}/${review.id}/vote`, {});
57
+ setHelpfulCount((c) => c + 1);
58
+ setVoted(true);
59
+ localStorage.setItem(storageKey, "1");
60
+ }
61
+ catch {
62
+ // silently fail
63
+ }
64
+ finally {
65
+ setVoting(false);
66
+ }
67
+ };
68
+ // ── Links ───────────────────────────────────────────────────────────────────
69
+ const productHref = review.productId
70
+ ? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
71
+ : null;
72
+ const sellerHref = storeHref ?? null;
73
+ const reviewerHref = String(ROUTES.PUBLIC.PROFILE(review.userId));
74
+ 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
+ ? "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-white scale-110"
79
+ : "border-transparent opacity-60 hover:opacity-100"}`, style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) }))] }))] }));
80
+ }
@@ -2,9 +2,9 @@ import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
3
  export type ReviewFilterVariant = "admin" | "seller" | "public";
4
4
  export declare const REVIEW_FILTER_KEYS: {
5
- readonly admin: readonly ["status", "rating", "brand", "verified", "featured"];
6
- readonly seller: readonly ["status", "rating", "brand"];
7
- readonly public: readonly ["rating", "brand"];
5
+ readonly admin: readonly ["status", "rating", "brand", "verified", "featured", "dateFrom", "dateTo"];
6
+ readonly seller: readonly ["status", "rating", "brand", "dateFrom", "dateTo"];
7
+ readonly public: readonly ["rating", "brand", "dateFrom", "dateTo"];
8
8
  };
9
9
  export declare const REVIEW_ADMIN_SORT_OPTIONS: readonly [{
10
10
  readonly value: "-createdAt";
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslations } from "next-intl";
3
3
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
4
+ import { RangeFilter } from "../../filters/RangeFilter";
4
5
  import { SwitchFilter } from "../../filters/SwitchFilter";
5
6
  import { Div } from "../../../ui";
6
7
  export const REVIEW_FILTER_KEYS = {
7
- admin: ["status", "rating", "brand", "verified", "featured"],
8
- seller: ["status", "rating", "brand"],
9
- public: ["rating", "brand"],
8
+ admin: ["status", "rating", "brand", "verified", "featured", "dateFrom", "dateTo"],
9
+ seller: ["status", "rating", "brand", "dateFrom", "dateTo"],
10
+ public: ["rating", "brand", "dateFrom", "dateTo"],
10
11
  };
11
12
  export const REVIEW_ADMIN_SORT_OPTIONS = [
12
13
  { value: "-createdAt", key: "sortNewest" },
@@ -55,5 +56,5 @@ export function ReviewFilters({ table, variant = "admin", brandOptions, }) {
55
56
  : [];
56
57
  const isAdmin = variant === "admin";
57
58
  const showStatus = variant !== "public";
58
- 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 })), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : ""), defaultCollapsed: true })] }))] }));
59
+ 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" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : ""), defaultCollapsed: 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 })] }));
59
60
  }
@@ -1,5 +1,6 @@
1
- import type { Review } from "../types";
1
+ import type { ReviewListResponse } from "../types";
2
2
  export interface ReviewsIndexListingProps {
3
- reviews: Review[];
3
+ initialData?: ReviewListResponse;
4
+ variant?: "admin" | "seller" | "public";
4
5
  }
5
- export declare function ReviewsIndexListing({ reviews }: ReviewsIndexListingProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function ReviewsIndexListing({ initialData, variant, }: ReviewsIndexListingProps): import("react/jsx-runtime").JSX.Element;
@@ -1,55 +1,39 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useMemo, useState } from "react";
4
- import { Div, Grid, Pagination, SlottedListingView, SortDropdown, Stack, Text, } from "../../../ui";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useCallback } from "react";
4
+ import { Search, SlidersHorizontal, X } from "lucide-react";
5
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
+ import { Pagination, SortDropdown } from "../../../ui";
5
7
  import { ReviewCard } from "./ReviewsList";
8
+ import { ReviewFilters, REVIEW_PUBLIC_SORT_OPTIONS } from "./ReviewFilters";
9
+ import { useReviews } from "../hooks/useReviews";
6
10
  const PAGE_SIZE = 12;
7
- const SORT_OPTIONS = [
8
- { value: "-createdAt", label: "Newest First" },
9
- { value: "createdAt", label: "Oldest First" },
10
- { value: "-rating", label: "Highest Rating" },
11
- { value: "rating", label: "Lowest Rating" },
12
- ];
13
- const STAR_OPTIONS = [
14
- { value: "", label: "All Ratings" },
15
- { value: "5", label: "5 Stars" },
16
- { value: "4", label: "4 Stars" },
17
- { value: "3", label: "3 Stars" },
18
- { value: "2", label: "2 Stars" },
19
- { value: "1", label: "1 Star" },
20
- ];
21
- export function ReviewsIndexListing({ reviews }) {
22
- const [sort, setSort] = useState("-createdAt");
23
- const [starFilter, setStarFilter] = useState("");
24
- const [page, setPage] = useState(1);
25
- const filtered = useMemo(() => {
26
- let result = [...reviews];
27
- if (starFilter) {
28
- const star = parseInt(starFilter, 10);
29
- result = result.filter((r) => r.rating === star);
30
- }
31
- result.sort((a, b) => {
32
- if (sort === "-rating")
33
- return b.rating - a.rating;
34
- if (sort === "rating")
35
- return a.rating - b.rating;
36
- const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
37
- const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
38
- return sort === "createdAt" ? aTime - bTime : bTime - aTime;
39
- });
40
- return result;
41
- }, [reviews, sort, starFilter]);
42
- const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
43
- const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
44
- const handleSort = (val) => {
45
- setSort(val);
46
- setPage(1);
47
- };
48
- const handleStarFilter = (val) => {
49
- setStarFilter(val);
50
- setPage(1);
51
- };
52
- return (_jsx(Div, { className: "min-h-screen", children: _jsx(SlottedListingView, { portal: "public", manageSearch: false, manageSort: false, inlineToolbar: true, renderSort: () => (_jsx(SortDropdown, { value: sort, onChange: handleSort, options: SORT_OPTIONS })), renderFilters: () => (_jsxs(Div, { className: "space-y-2 p-4", children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Filter by Rating" }), STAR_OPTIONS.map((opt) => (_jsx("button", { type: "button", onClick: () => handleStarFilter(opt.value), className: `block w-full rounded-lg px-3 py-2 text-left text-sm transition ${starFilter === opt.value
53
- ? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
54
- : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-slate-800"}`, children: opt.label }, opt.value)))] })), renderTable: () => paginated.length === 0 ? (_jsx(Stack, { align: "center", gap: "3", className: "justify-center py-24 text-center", children: _jsx(Text, { className: "text-xl font-medium text-zinc-900 dark:text-zinc-50", children: "No reviews found" }) })) : (_jsx(Grid, { cols: 3, gap: "md", children: paginated.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })), renderPagination: () => totalPages > 1 ? (_jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: setPage })) : null, total: filtered.length, isLoading: false }) }));
11
+ export function ReviewsIndexListing({ initialData, variant = "public", }) {
12
+ const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "-createdAt" } });
13
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
14
+ const [filterOpen, setFilterOpen] = useState(false);
15
+ const sort = table.get("sort") || "-createdAt";
16
+ const currentPage = table.getNumber("page", 1);
17
+ const commitSearch = useCallback(() => {
18
+ table.set("q", searchInput.trim());
19
+ table.setPage(1);
20
+ }, [searchInput, table]);
21
+ const closeFilters = () => setFilterOpen(false);
22
+ const ratingRaw = table.get("rating");
23
+ const { reviews, total, totalPages, isLoading } = useReviews({
24
+ q: table.get("q") || undefined,
25
+ rating: ratingRaw || undefined,
26
+ dateFrom: table.get("dateFrom") || undefined,
27
+ dateTo: table.get("dateTo") || undefined,
28
+ minVotes: table.get("minVotes") ? Number(table.get("minVotes")) : undefined,
29
+ maxVotes: table.get("maxVotes") ? Number(table.get("maxVotes")) : undefined,
30
+ sort,
31
+ page: currentPage,
32
+ perPage: PAGE_SIZE,
33
+ }, { initialData });
34
+ const sortOptions = REVIEW_PUBLIC_SORT_OPTIONS.map((opt) => ({
35
+ value: opt.value,
36
+ label: opt.key,
37
+ }));
38
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx("div", { className: "sticky top-0 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 border-zinc-300 dark:border-slate-600 px-3.5 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: "Filters" })] }), _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: (e) => e.key === "Enter" && commitSearch(), placeholder: "Search reviews by product name...", 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", ""); table.setPage(1); }, className: "px-2 text-zinc-400 hover:text-zinc-600 transition-colors", "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: sort, onChange: (v) => { table.set("sort", v); table.setPage(1); }, options: sortOptions })] })] }) }), _jsxs("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." })) : (_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))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: closeFilters }), _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: closeFilters, "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: table, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsx("button", { type: "button", onClick: closeFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply filters" }) })] })] }))] }));
55
39
  }
@@ -1 +1,6 @@
1
- export declare function ReviewsIndexPageView(): Promise<import("react/jsx-runtime").JSX.Element>;
1
+ type SearchParams = Record<string, string | string[]>;
2
+ export interface ReviewsIndexPageViewProps {
3
+ searchParams?: SearchParams;
4
+ }
5
+ export declare function ReviewsIndexPageView({ searchParams }: ReviewsIndexPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
6
+ export {};
@@ -3,7 +3,53 @@ import { reviewRepository } from "../../../repositories";
3
3
  import { Container, Heading, Main, Section } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { ReviewsIndexListing } from "./ReviewsIndexListing";
6
- export async function ReviewsIndexPageView() {
7
- const reviews = await reviewRepository.findByStatus("approved").catch(() => []);
8
- return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8 text-3xl font-semibold text-zinc-900 dark:text-zinc-50", children: "Reviews" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(ReviewsIndexListing, { reviews: reviews }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
6
+ function sp(params, key) {
7
+ const v = params[key];
8
+ return Array.isArray(v) ? v[0] ?? "" : v ?? "";
9
+ }
10
+ function buildReviewFilters(params) {
11
+ const parts = ["status==approved"];
12
+ const rating = sp(params, "rating");
13
+ if (rating) {
14
+ const values = rating.split("|").filter(Boolean);
15
+ if (values.length === 1)
16
+ parts.push(`rating==${values[0]}`);
17
+ else if (values.length > 1)
18
+ parts.push(`rating==${values.join("|")}`);
19
+ }
20
+ const minVotes = sp(params, "minVotes");
21
+ if (minVotes)
22
+ parts.push(`helpfulCount>=${minVotes}`);
23
+ const maxVotes = sp(params, "maxVotes");
24
+ if (maxVotes)
25
+ parts.push(`helpfulCount<=${maxVotes}`);
26
+ const dateFrom = sp(params, "dateFrom");
27
+ const dateTo = sp(params, "dateTo");
28
+ if (dateFrom)
29
+ parts.push(`createdAt>=${dateFrom}`);
30
+ if (dateTo)
31
+ parts.push(`createdAt<=${dateTo}`);
32
+ const q = sp(params, "q");
33
+ if (q)
34
+ parts.push(`productTitle@=*${q}`);
35
+ return parts.join(",");
36
+ }
37
+ export async function ReviewsIndexPageView({ searchParams = {} }) {
38
+ const sort = sp(searchParams, "sort") || "-createdAt";
39
+ const page = Number(sp(searchParams, "page")) || 1;
40
+ const filters = buildReviewFilters(searchParams);
41
+ const result = await reviewRepository
42
+ .listAll({ filters, sorts: sort, page, pageSize: 12 })
43
+ .catch(() => null);
44
+ const initialData = result
45
+ ? {
46
+ items: result.items,
47
+ total: result.total,
48
+ page: result.page,
49
+ pageSize: result.pageSize,
50
+ totalPages: result.totalPages,
51
+ hasMore: result.hasMore,
52
+ }
53
+ : undefined;
54
+ return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8 text-3xl font-semibold text-zinc-900 dark:text-zinc-50", children: "Reviews" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(ReviewsIndexListing, { initialData: initialData }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
9
55
  }
@@ -1,9 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from "next/link";
2
3
  import { Div, Heading, Pagination, RichText, Row, Skeleton, Span, Stack, Text, } from "../../../ui";
3
4
  import { StarRating } from "../../../ui";
5
+ import { THEME_CONSTANTS } from "../../../tokens";
4
6
  import { maskName } from "../../../security";
5
7
  import { getDefaultLocale } from "../../../core/baseline-resolver";
6
8
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
9
+ import { ROUTES } from "../../../next";
7
10
  export function ReviewCard({ review, className = "" }) {
8
11
  const date = review.createdAt
9
12
  ? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
@@ -14,7 +17,12 @@ export function ReviewCard({ review, className = "" }) {
14
17
  : "";
15
18
  const displayName = maskName(review.userName);
16
19
  const initials = displayName.charAt(0).toUpperCase();
17
- return (_jsxs(Div, { className: `rounded-xl border border-neutral-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900 ${className}`, 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"] }))] }));
20
+ const reviewHref = String(ROUTES.PUBLIC.REVIEW_DETAIL(review.id));
21
+ const productHref = review.productId
22
+ ? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
23
+ : null;
24
+ const card = (_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 cursor-pointer ${className}`, 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"] })), productHref && (_jsxs(Div, { className: "mt-auto pt-3 flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 border-t border-neutral-100 dark:border-zinc-800", 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" })] }))] }));
25
+ return (_jsx(Link, { href: reviewHref, className: "block h-full", children: card }));
18
26
  }
19
27
  export function ReviewsList({ reviews, isLoading, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No reviews yet", }) {
20
28
  if (isLoading) {
@@ -10,3 +10,4 @@ export { ReviewsListView } from "./ReviewsListView";
10
10
  export type { ReviewsListViewProps } from "./ReviewsListView";
11
11
  export { ReviewsIndexListing } from "./ReviewsIndexListing";
12
12
  export type { ReviewsIndexListingProps } from "./ReviewsIndexListing";
13
+ export type { ReviewDetailPageViewProps } from "./ReviewDetailPageView";