@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
@@ -1,44 +1,47 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useMemo } from "react";
4
- import { useCategoriesList } from "../hooks/useCategories";
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 { useCategoriesFiltered } from "../hooks/useCategories";
5
6
  import { ROUTES } from "../../../next";
6
- import { Container, Grid, Input, Section, SlottedListingView, SortDropdown, Stack, Text, } from "../../../ui";
7
- import { CategoryCard } from "./CategoryGrid";
8
7
  import { Pagination } from "../../../ui";
8
+ import { CategoryCard } from "./CategoryGrid";
9
+ import { CategoryFilters } from "./CategoryFilters";
10
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
9
11
  const PAGE_SIZE = 24;
10
- export function CategoriesIndexListing({ initialData }) {
11
- const { categories, isLoading } = useCategoriesList({ initialData });
12
- const [search, setSearch] = useState("");
13
- const [sort, setSort] = useState("name");
14
- const [page, setPage] = useState(1);
15
- const filtered = useMemo(() => {
16
- let result = [...categories];
17
- const q = search.trim().toLowerCase();
18
- if (q) {
19
- result = result.filter((c) => c.name.toLowerCase().includes(q) ||
20
- (c.description ?? "").toLowerCase().includes(q));
21
- }
22
- result.sort((a, b) => {
23
- if (sort === "-productCount") {
24
- return (b.productCount ?? 0) - (a.productCount ?? 0);
25
- }
26
- if (sort === "-name") {
27
- return b.name.localeCompare(a.name);
28
- }
29
- return a.name.localeCompare(b.name);
30
- });
31
- return result;
32
- }, [categories, search, sort]);
33
- const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
34
- const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
35
- const handleSearch = (val) => {
36
- setSearch(val);
37
- setPage(1);
12
+ const SORT_OPTIONS = [
13
+ { value: "name", label: "Name A–Z" },
14
+ { value: "-name", label: "Name Z–A" },
15
+ { value: "-productCount", label: "Most Products" },
16
+ ];
17
+ export function CategoriesIndexListing({ initialData: _ }) {
18
+ const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "name" } });
19
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
20
+ const [filterOpen, setFilterOpen] = useState(false);
21
+ const sort = table.get("sort") || "name";
22
+ const page = table.getNumber("page", 1);
23
+ const commitSearch = useCallback(() => {
24
+ table.set("q", searchInput.trim());
25
+ table.setPage(1);
26
+ }, [searchInput, table]);
27
+ const clearSearch = () => {
28
+ setSearchInput("");
29
+ table.set("q", "");
30
+ table.setPage(1);
38
31
  };
39
- return (_jsx(Section, { children: _jsx(Container, { size: "xl", children: _jsx(SlottedListingView, { portal: "public", manageSearch: false, manageSort: false, inlineToolbar: true, renderSearch: () => (_jsx(Input, { value: search, onChange: (e) => handleSearch(e.target.value), placeholder: "Search categories...", className: "max-w-sm" })), renderSort: () => (_jsx(SortDropdown, { value: sort, onChange: (v) => { setSort(v); setPage(1); }, options: [
40
- { value: "name", label: "Name A–Z" },
41
- { value: "-name", label: "Name Z–A" },
42
- { value: "-productCount", label: "Most Products" },
43
- ] })), renderTable: () => paginated.length === 0 && !isLoading ? (_jsxs(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 categories found" }), search && (_jsxs(Text, { className: "text-sm text-zinc-500", children: ["No results for \u201C", search, "\u201D"] }))] })) : (_jsx(Grid, { cols: 4, gap: "md", children: paginated.map((category) => (_jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id))) })), renderPagination: () => totalPages > 1 ? (_jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: setPage })) : null, total: filtered.length, isLoading: isLoading }) }) }));
32
+ const closeFilters = () => setFilterOpen(false);
33
+ const { categories, total, totalPages, isLoading } = useCategoriesFiltered({
34
+ q: table.get("q") || undefined,
35
+ isFeatured: table.get("isFeatured") === "true" || undefined,
36
+ isBrand: table.get("isBrand") === "true" || undefined,
37
+ rootOnly: table.get("rootOnly") === "true" || undefined,
38
+ tier: table.get("tier") ? Number(table.get("tier")) : undefined,
39
+ minProductCount: table.get("minItemCount") ? Number(table.get("minItemCount")) : undefined,
40
+ maxProductCount: table.get("maxItemCount") ? Number(table.get("maxItemCount")) : undefined,
41
+ sort,
42
+ page,
43
+ pageSize: PAGE_SIZE,
44
+ });
45
+ const activeSearch = table.get("q") || "";
46
+ 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 -mx-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 categories...", 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: clearSearch, 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" }), _jsx("select", { value: sort, onChange: (e) => { table.set("sort", e.target.value); table.setPage(1); }, className: "rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3 py-2 text-sm text-zinc-700 dark:text-zinc-200 outline-none focus:ring-2 focus:ring-primary/30", children: SORT_OPTIONS.map((o) => _jsx("option", { value: o.value, children: o.label }, o.value)) })] })] }) }), _jsxs("div", { className: "py-6", children: [isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-[4/3] bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3.5 space-y-2", children: [_jsx("div", { className: "h-3.5 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" })] })] }, i))) })) : categories.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: activeSearch ? `No categories matching "${activeSearch}"` : "No categories found" })) : (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: categories.map((category) => (_jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: page, 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(CategoryFilters, { table: table, variant: "public" }) }), _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" }) })] })] }))] }));
44
47
  }
@@ -1 +1,6 @@
1
- export declare function CategoriesIndexPageView(): Promise<import("react/jsx-runtime").JSX.Element>;
1
+ type SearchParams = Record<string, string | string[]>;
2
+ export interface CategoriesIndexPageViewProps {
3
+ searchParams?: SearchParams;
4
+ }
5
+ export declare function CategoriesIndexPageView({ searchParams }: CategoriesIndexPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
6
+ export {};
@@ -3,9 +3,48 @@ import { categoriesRepository } from "../../../repositories";
3
3
  import { Container, Heading, Main, Section } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { CategoriesIndexListing } from "./CategoriesIndexListing";
6
- export async function CategoriesIndexPageView() {
6
+ function sp(params, key) {
7
+ const v = params[key];
8
+ return Array.isArray(v) ? v[0] ?? "" : v ?? "";
9
+ }
10
+ function buildCategoryFilters(params) {
11
+ const parts = [];
12
+ const isFeatured = sp(params, "isFeatured");
13
+ if (isFeatured === "true")
14
+ parts.push("isFeatured==true");
15
+ const isBrand = sp(params, "isBrand");
16
+ if (isBrand === "true")
17
+ parts.push("isBrand==true");
18
+ const rootOnly = sp(params, "rootOnly");
19
+ if (rootOnly === "true")
20
+ parts.push("tier==0");
21
+ const minItemCount = sp(params, "minItemCount");
22
+ const maxItemCount = sp(params, "maxItemCount");
23
+ if (minItemCount)
24
+ parts.push(`metrics.totalItemCount>=${minItemCount}`);
25
+ if (maxItemCount)
26
+ parts.push(`metrics.totalItemCount<=${maxItemCount}`);
27
+ const tier = sp(params, "tier");
28
+ if (tier) {
29
+ const values = tier.split("|").filter(Boolean);
30
+ if (values.length === 1)
31
+ parts.push(`tier==${values[0]}`);
32
+ else if (values.length > 1)
33
+ parts.push(`tier==${values.join("|")}`);
34
+ }
35
+ return parts.join(",");
36
+ }
37
+ export async function CategoriesIndexPageView({ searchParams = {} }) {
38
+ const sort = sp(searchParams, "sort") || "name";
39
+ const page = Number(sp(searchParams, "page")) || 1;
40
+ const filters = buildCategoryFilters(searchParams);
7
41
  const result = await categoriesRepository
8
- .list({ page: 1, pageSize: 200, sorts: "name" })
42
+ .list({
43
+ page,
44
+ pageSize: 200,
45
+ sorts: sort,
46
+ ...(filters ? { filters } : {}),
47
+ })
9
48
  .catch(() => null);
10
49
  const initialData = (result?.items ?? []);
11
50
  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: "Categories" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoriesIndexListing, { initialData: initialData }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
@@ -1,10 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { categoriesRepository, productRepository } from "../../../repositories";
4
4
  import { ROUTES } from "../../../next";
5
- import { normalizeRichTextHtml } from "../../../utils/string.formatter";
6
- import { Container, Div, Heading, Main, Row, RichText, Section, Span, } from "../../../ui";
7
- import { CategoryProductsListing } from "./CategoryProductsListing";
5
+ import { Container, Main, Section } from "../../../ui";
6
+ import { CategoryDetailTabs } from "./CategoryDetailTabs";
8
7
  export async function CategoryDetailPageView({ slug }) {
9
8
  const [categoryResult, productsResult] = await Promise.all([
10
9
  categoriesRepository
@@ -13,7 +12,7 @@ export async function CategoryDetailPageView({ slug }) {
13
12
  .catch(() => undefined),
14
13
  productRepository
15
14
  .list({
16
- filters: `status==published,categorySlug==${slug}`,
15
+ filters: `status==published,categorySlug==${slug},isAuction==false`,
17
16
  sorts: "-createdAt",
18
17
  page: 1,
19
18
  pageSize: 24,
@@ -21,5 +20,13 @@ export async function CategoryDetailPageView({ slug }) {
21
20
  .catch(() => null),
22
21
  ]);
23
22
  const category = categoryResult;
24
- return (_jsxs(Main, { children: [_jsx(Section, { className: "bg-zinc-50 py-4 dark:bg-zinc-900", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { align: "center", gap: "xs", className: "text-sm text-zinc-500", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600", children: "Home" }), _jsx(Span, { children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORIES), className: "hover:text-primary-600", children: "Categories" }), _jsx(Span, { children: "/" }), _jsx(Span, { className: "text-zinc-900 dark:text-zinc-100", children: category?.name ?? slug })] }) }) }), _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-50", children: category?.name ?? slug }), category?.description && (_jsx(Div, { className: "mb-8 text-zinc-600 dark:text-zinc-400", children: _jsx(RichText, { html: normalizeRichTextHtml(category.description) }) })), _jsx(CategoryProductsListing, { categorySlug: slug, initialData: productsResult ?? undefined })] }) })] }));
23
+ const childCategories = category?.id
24
+ ? await categoriesRepository.getChildren(category.id).catch(() => [])
25
+ : [];
26
+ const productCount = category?.metrics?.productCount ?? 0;
27
+ const coverImage = category?.display?.coverImage;
28
+ const hasCover = Boolean(coverImage);
29
+ return (_jsxs(Main, { children: [_jsxs("section", { className: `relative overflow-hidden ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-zinc-50 dark:bg-zinc-900"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute inset-0 bg-center bg-cover", style: { backgroundImage: `url(${coverImage})` } }), _jsx("div", { className: "absolute inset-0 bg-black/55" })] })), _jsxs("div", { className: `relative z-10 max-w-7xl mx-auto px-4 ${hasCover ? "py-12" : "py-8"}`, children: [_jsxs("nav", { className: "flex items-center gap-1.5 text-sm mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-zinc-500 hover:text-primary-600 transition-colors", children: "Home" }), _jsx("span", { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORIES), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-zinc-500 hover:text-primary-600 transition-colors", children: "Categories" }), _jsx("span", { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx("span", { className: hasCover ? "text-white font-medium" : "text-zinc-900 dark:text-zinc-100 font-medium", children: category?.name ?? slug })] }), _jsx("h1", { className: `text-3xl md:text-4xl font-bold mb-2 ${hasCover ? "text-white" : "text-zinc-900 dark:text-zinc-50"}`, children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx("p", { className: `text-base max-w-2xl mb-4 ${hasCover ? "text-white/80" : "text-zinc-600 dark:text-zinc-400"}`, children: category.description })), productCount > 0 && (_jsxs("span", { className: `inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1 rounded-full ${hasCover
30
+ ? "bg-white/20 text-white backdrop-blur-sm"
31
+ : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, children: [productCount.toLocaleString(), " ", productCount === 1 ? "item" : "items"] }))] })] }), childCategories.length > 0 && (_jsx("section", { className: "border-b border-zinc-100 dark:border-zinc-800 bg-white dark:bg-zinc-900", children: _jsx("div", { className: "max-w-7xl mx-auto px-4 py-3", children: _jsx("div", { className: "flex gap-2.5 overflow-x-auto pb-1", style: { scrollbarWidth: "none" }, children: childCategories.map((child) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)), className: "flex-shrink-0 flex items-center gap-1.5 rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-4 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:border-primary hover:text-primary transition-colors whitespace-nowrap", children: [child.display?.icon && (_jsx("span", { className: "text-base leading-none", children: child.display.icon })), child.name, (child.metrics?.productCount ?? 0) > 0 && (_jsx("span", { className: "text-xs text-zinc-400 dark:text-zinc-500", children: (child.metrics?.productCount ?? 0).toLocaleString() }))] }, child.id))) }) }) })), _jsx(Section, { className: "py-6", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, initialProductsData: productsResult ?? undefined }) }) })] }));
25
32
  }
@@ -0,0 +1,5 @@
1
+ export interface CategoryDetailTabsProps {
2
+ categorySlug: string;
3
+ initialProductsData?: any;
4
+ }
5
+ export declare function CategoryDetailTabs({ categorySlug, initialProductsData }: CategoryDetailTabsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { CategoryProductsListing } from "./CategoryProductsListing";
5
+ import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing";
6
+ import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing";
7
+ const TABS = [
8
+ { id: "products", label: "Products" },
9
+ { id: "auctions", label: "Auctions" },
10
+ { id: "pre-orders", label: "Pre-Orders" },
11
+ ];
12
+ export function CategoryDetailTabs({ categorySlug, initialProductsData }) {
13
+ const [activeTab, setActiveTab] = useState("products");
14
+ return (_jsxs("div", { children: [_jsx("div", { className: "flex border-b border-zinc-200 dark:border-slate-700 mb-6", children: TABS.map((t) => (_jsx("button", { type: "button", onClick: () => setActiveTab(t.id), className: `px-5 py-2.5 text-sm font-medium transition-colors -mb-px border-b-2 ${activeTab === t.id
15
+ ? "border-primary text-primary"
16
+ : "border-transparent text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100"}`, children: t.label }, t.id))) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug }))] }));
17
+ }
@@ -1,6 +1,7 @@
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 CATEGORY_FILTER_KEYS = {
@@ -70,5 +71,5 @@ export function CategoryFilters({ table, variant = "admin", }) {
70
71
  const activeOnly = table.get("isActive") === "true";
71
72
  const searchableOnly = table.get("isSearchable") === "true";
72
73
  const leafOnly = table.get("isLeaf") === "true";
73
- return (_jsxs(Div, { children: [_jsx(FilterFacetSection, { title: tr("tier", "Tier"), options: tierOptions, selected: selectedTier, onChange: (vals) => table.set("tier", vals.join("|")), searchable: false, defaultCollapsed: variant !== "admin" }), _jsx(SwitchFilter, { title: tr("featured", "Featured"), label: tr("showFeaturedOnly", "Show featured only"), checked: featuredOnly, onChange: (v) => table.set("isFeatured", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: tr("brand", "Brand"), label: tr("showBrandsOnly", "Show brands only"), checked: brandOnly, onChange: (v) => table.set("isBrand", v ? "true" : ""), defaultCollapsed: true }), variant !== "public" && (_jsx(SwitchFilter, { title: tr("status", "Status"), label: tr("showActiveOnly", "Show active only"), checked: activeOnly, onChange: (v) => table.set("isActive", v ? "true" : ""), defaultCollapsed: true })), variant === "admin" && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: tr("searchable", "Searchable"), label: tr("showSearchableOnly", "Show searchable only"), checked: searchableOnly, onChange: (v) => table.set("isSearchable", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: tr("leaf", "Leaf"), label: tr("showLeafOnly", "Show leaf only"), checked: leafOnly, onChange: (v) => table.set("isLeaf", v ? "true" : ""), defaultCollapsed: true })] }))] }));
74
+ return (_jsxs(Div, { children: [_jsx(FilterFacetSection, { title: tr("tier", "Tier"), options: tierOptions, selected: selectedTier, onChange: (vals) => table.set("tier", vals.join("|")), searchable: false, defaultCollapsed: variant !== "admin" }), _jsx(SwitchFilter, { title: tr("featured", "Featured"), label: tr("showFeaturedOnly", "Show featured only"), checked: featuredOnly, onChange: (v) => table.set("isFeatured", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: tr("brand", "Brand"), label: tr("showBrandsOnly", "Show brands only"), checked: brandOnly, onChange: (v) => table.set("isBrand", v ? "true" : ""), defaultCollapsed: true }), variant !== "public" && (_jsx(SwitchFilter, { title: tr("status", "Status"), label: tr("showActiveOnly", "Show active only"), checked: activeOnly, onChange: (v) => table.set("isActive", v ? "true" : ""), defaultCollapsed: true })), variant === "admin" && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: tr("searchable", "Searchable"), label: tr("showSearchableOnly", "Show searchable only"), checked: searchableOnly, onChange: (v) => table.set("isSearchable", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: tr("leaf", "Leaf"), label: tr("showLeafOnly", "Show leaf only"), checked: leafOnly, onChange: (v) => table.set("isLeaf", v ? "true" : ""), defaultCollapsed: true })] })), _jsx(SwitchFilter, { title: tr("rootLevelOnly", "Root Level"), label: tr("showRootLevelOnly", "Top level only"), checked: table.get("rootOnly") === "true", onChange: (v) => table.set("rootOnly", v ? "true" : ""), defaultCollapsed: true }), _jsx(RangeFilter, { title: tr("itemCount", "Item Count"), minValue: table.get("minItemCount"), maxValue: table.get("maxItemCount"), onMinChange: (v) => table.set("minItemCount", v), onMaxChange: (v) => table.set("maxItemCount", v), minBound: 0, maxBound: 10000, step: 10, minPlaceholder: tr("minItemCount", "Min items"), maxPlaceholder: tr("maxItemCount", "Max items"), defaultCollapsed: true })] }));
74
75
  }
@@ -8,8 +8,9 @@ export interface CategoryCardProps {
8
8
  export declare function CategoryCard({ category, href, onClick, className, }: CategoryCardProps): import("react/jsx-runtime").JSX.Element;
9
9
  export interface CategoryGridProps {
10
10
  categories: CategoryItem[];
11
+ getHref?: (category: CategoryItem) => string;
11
12
  onCategoryClick?: (category: CategoryItem) => void;
12
13
  emptyLabel?: string;
13
14
  className?: string;
14
15
  }
15
- export declare function CategoryGrid({ categories, onCategoryClick, emptyLabel, className, }: CategoryGridProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel, className, }: CategoryGridProps): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
- import { Div, Grid, Text } from "../../../ui";
3
+ import { ArrowRight } from "lucide-react";
4
+ import { Div, Span, Text } from "../../../ui";
4
5
  import { THEME_CONSTANTS } from "../../../tokens";
5
6
  export function CategoryCard({ category, href, onClick, className = "", }) {
6
- const cardClass = `group relative overflow-hidden rounded-xl border border-neutral-200 bg-white dark:border-zinc-700 dark:bg-zinc-900 shadow-sm transition hover:shadow-md ${onClick || href ? "cursor-pointer" : ""} ${className}`;
7
- const inner = (_jsxs(_Fragment, { children: [category.display?.coverImage ? (_jsx(Div, { className: "aspect-video w-full overflow-hidden bg-neutral-100 dark:bg-zinc-800", children: _jsx(Div, { role: "img", "aria-label": category.name, className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${category.display.coverImage})` } }) })) : (_jsx(Div, { className: "aspect-video w-full bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-zinc-800 dark:to-zinc-700" })), _jsxs(Div, { className: "p-4", children: [_jsx(Text, { className: `font-semibold ${THEME_CONSTANTS.utilities.textClamp2} text-neutral-900 dark:text-white`, children: category.name }), category.description && (_jsx(Text, { className: `mt-1 text-sm ${THEME_CONSTANTS.utilities.textClamp2} text-neutral-500 dark:text-zinc-400`, children: category.description })), category.metrics && (_jsxs(Text, { className: "mt-2 text-xs text-neutral-400 dark:text-zinc-500", children: [category.metrics.productCount.toLocaleString(), " items"] }))] })] }));
7
+ const productCount = category.metrics?.productCount ?? category.productCount ?? 0;
8
+ const inner = (_jsxs(Div, { className: "flex h-full flex-col", children: [_jsxs(Div, { className: "relative aspect-[4/3] w-full overflow-hidden bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-zinc-800 dark:to-zinc-700 flex-shrink-0", children: [category.display?.coverImage ? (_jsx(Div, { role: "img", "aria-label": category.name, className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${category.display.coverImage})` } })) : category.display?.color ? (_jsx(Div, { className: "h-full w-full opacity-80", style: { backgroundColor: category.display.color } })) : null, category.display?.icon && (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center text-4xl", children: category.display.icon })), category.isFeatured && (_jsx(Span, { className: "absolute left-2 top-2 rounded-full bg-amber-400 p-1 text-xs leading-none", children: "\u2605" }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3.5", children: [_jsx(Text, { className: `font-semibold text-sm leading-snug ${THEME_CONSTANTS.utilities.textClamp2} text-neutral-900 dark:text-white`, children: category.name }), category.description && (_jsx(Text, { className: `mt-1 text-xs ${THEME_CONSTANTS.utilities.textClamp2} text-neutral-500 dark:text-zinc-400 flex-1`, children: category.description })), _jsxs(Div, { className: "mt-2 flex items-center justify-between gap-2", children: [_jsxs(Text, { className: "text-xs text-neutral-400 dark:text-zinc-500", children: [productCount.toLocaleString(), " ", productCount === 1 ? "item" : "items"] }), _jsxs(Span, { className: "inline-flex items-center gap-1 rounded-md border border-zinc-200 dark:border-zinc-700 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 group-hover:bg-primary group-hover:border-primary group-hover:text-white transition-colors", children: ["Browse ", _jsx(ArrowRight, { className: "h-3 w-3" })] })] })] })] }));
9
+ const cardClass = `group relative flex flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white dark:border-zinc-700 dark:bg-zinc-900 shadow-sm transition hover:shadow-md h-full ${className}`;
8
10
  if (href) {
9
11
  return (_jsx(Link, { href: href, className: cardClass, children: inner }));
10
12
  }
@@ -12,9 +14,9 @@ export function CategoryCard({ category, href, onClick, className = "", }) {
12
14
  ? (e) => (e.key === "Enter" || e.key === " ") && onClick(category)
13
15
  : undefined, onClick: onClick ? () => onClick(category) : undefined, className: cardClass, children: inner }));
14
16
  }
15
- export function CategoryGrid({ categories, onCategoryClick, emptyLabel = "No categories found", className = "", }) {
17
+ export function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel = "No categories found", className = "", }) {
16
18
  if (categories.length === 0) {
17
19
  return (_jsx(Text, { className: "py-12 text-center text-sm text-neutral-500 dark:text-zinc-500", children: emptyLabel }));
18
20
  }
19
- return (_jsx(Grid, { cols: "categoryCards", className: className, children: categories.map((cat) => (_jsx(CategoryCard, { category: cat, onClick: onCategoryClick }, cat.id))) }));
21
+ return (_jsx("div", { className: `grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 ${className}`, children: categories.map((cat) => (_jsx(CategoryCard, { category: cat, href: getHref ? getHref(cat) : undefined, onClick: !getHref ? onCategoryClick : undefined }, cat.id))) }));
20
22
  }
@@ -1,29 +1,40 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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, LayoutGrid, List, X } from "lucide-react";
3
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
4
6
  import { useProducts } from "../../products/hooks/useProducts";
5
- import { Div, Grid, Pagination, SlottedListingView, SortDropdown, Input, Stack, Text, } from "../../../ui";
7
+ import { Pagination, SortDropdown } from "../../../ui";
6
8
  import { ROUTES } from "../../../next";
7
- import { InteractiveProductCard } from "../../products/components/InteractiveProductCard";
9
+ import { ProductGrid } from "../../products/components/ProductGrid";
8
10
  import { ProductFilters } from "../../products/components/ProductFilters";
11
+ import { PRODUCT_PUBLIC_SORT_OPTIONS } from "../../products/components/ProductFilters";
9
12
  export function CategoryProductsListing({ categorySlug, initialData, }) {
10
13
  const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
14
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
15
+ const [filterOpen, setFilterOpen] = useState(false);
16
+ const [view, setView] = useState(table.get("view") || "card");
11
17
  const params = {
12
18
  q: table.get("q") || undefined,
13
19
  categorySlug,
14
20
  condition: table.get("condition") || undefined,
21
+ brand: table.get("brand") || undefined,
15
22
  minPrice: table.get("minPrice") ? Number(table.get("minPrice")) : undefined,
16
23
  maxPrice: table.get("maxPrice") ? Number(table.get("maxPrice")) : undefined,
17
- sort: table.get("sort") || undefined,
24
+ sort: table.get("sort") || "-createdAt",
18
25
  page: table.getNumber("page", 1),
19
26
  perPage: table.getNumber("pageSize", 24),
27
+ isAuction: false,
20
28
  };
21
29
  const { products, total, totalPages, page, isLoading } = useProducts(params, { initialData });
22
- return (_jsx(Div, { className: "min-h-[200px]", children: _jsx(SlottedListingView, { portal: "public", manageSearch: true, manageSort: true, inlineToolbar: true, renderSearch: (search, setSearch) => (_jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Search in category...", className: "max-w-sm" })), renderSort: (value, onChange) => (_jsx(SortDropdown, { value: value, onChange: onChange, options: [
23
- { value: "-createdAt", label: "Newest First" },
24
- { value: "-price", label: "Price: High to Low" },
25
- { value: "price", label: "Price: Low to High" },
26
- { value: "title", label: "Title A-Z" },
27
- { value: "-views", label: "Most Viewed" },
28
- ] })), renderFilters: () => _jsx(ProductFilters, { table: table }), renderTable: () => products.length === 0 && !isLoading ? (_jsxs(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 products in this category yet" }), _jsx(Text, { className: "text-sm text-zinc-500", children: "Check back soon or browse other categories." })] })) : (_jsx(Grid, { cols: "productCards", gap: "md", children: products.map((p) => (_jsx(InteractiveProductCard, { product: p, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug ?? p.id)) }, p.id))) })), renderPagination: () => (_jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) })), total: total, isLoading: isLoading }) }));
30
+ const commitSearch = useCallback(() => {
31
+ table.set("q", searchInput.trim());
32
+ table.setPage(1);
33
+ }, [searchInput, table]);
34
+ const handleViewToggle = (next) => {
35
+ setView(next);
36
+ table.set("view", next);
37
+ };
38
+ const closeFilters = () => setFilterOpen(false);
39
+ return (_jsxs("div", { className: "min-h-[200px]", 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 in ${categorySlug.replace(/-/g, " ")}...`, 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" }), _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: PRODUCT_PUBLIC_SORT_OPTIONS })] }), _jsxs("div", { className: "flex shrink-0 items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => handleViewToggle("card"), "aria-label": "Grid view", className: `p-2 transition-colors ${view === "card" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => handleViewToggle("list"), "aria-label": "List view", className: `p-2 transition-colors ${view === "list" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(List, { className: "h-4 w-4" }) })] })] }) }), _jsxs("div", { className: "py-6", children: [isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view, emptyLabel: `No products found in this category.` })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: page, 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(ProductFilters, { table: table, currencyPrefix: "\u20B9" }) }), _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" }) })] })] }))] }));
29
40
  }
@@ -24,3 +24,5 @@ export { getCategoryTableColumns } from "./CategoryTableColumns";
24
24
  export type { CategoryTableColumnsLabels } from "./CategoryTableColumns";
25
25
  export { CategoriesIndexListing } from "./CategoriesIndexListing";
26
26
  export type { CategoriesIndexListingProps } from "./CategoriesIndexListing";
27
+ export { CategoryDetailTabs } from "./CategoryDetailTabs";
28
+ export type { CategoryDetailTabsProps } from "./CategoryDetailTabs";
@@ -12,3 +12,4 @@ export { CategoryForm } from "./CategoryForm";
12
12
  export { CategorySelectorCreate } from "./CategorySelectorCreate";
13
13
  export { getCategoryTableColumns } from "./CategoryTableColumns";
14
14
  export { CategoriesIndexListing } from "./CategoriesIndexListing";
15
+ export { CategoryDetailTabs } from "./CategoryDetailTabs";
@@ -9,6 +9,26 @@ export declare function useCategoriesList(opts?: UseCategoriesListOptions): {
9
9
  isLoading: boolean;
10
10
  error: Error | null;
11
11
  };
12
+ export interface CategoriesFilteredParams {
13
+ q?: string;
14
+ isFeatured?: boolean;
15
+ isBrand?: boolean;
16
+ rootOnly?: boolean;
17
+ tier?: number;
18
+ minProductCount?: number;
19
+ maxProductCount?: number;
20
+ sort?: string;
21
+ page?: number;
22
+ pageSize?: number;
23
+ }
24
+ /** API-driven for structured filters; text search + pagination done in hook (Firestore substring limitation). */
25
+ export declare function useCategoriesFiltered(params?: CategoriesFilteredParams): {
26
+ categories: CategoryItem[];
27
+ total: number;
28
+ totalPages: number;
29
+ isLoading: boolean;
30
+ error: Error | null;
31
+ };
12
32
  interface UseCategoryDetailOptions {
13
33
  initialCategory?: CategoryItem;
14
34
  initialChildren?: CategoryItem[];
@@ -1,3 +1,4 @@
1
+ import { useMemo } from "react";
1
2
  import { useQuery } from "@tanstack/react-query";
2
3
  import { apiClient } from "../../../http";
3
4
  import { CATEGORY_ENDPOINTS } from "../../../constants/api-endpoints";
@@ -7,7 +8,7 @@ export function useCategoriesList(opts) {
7
8
  queryFn: () => apiClient.get(CATEGORY_ENDPOINTS.FLAT),
8
9
  initialData: opts?.initialData,
9
10
  enabled: opts?.enabled,
10
- staleTime: 5 * 60 * 1000, // 5 min — categories change infrequently
11
+ staleTime: 5 * 60 * 1000,
11
12
  });
12
13
  return {
13
14
  categories: query.data ?? [],
@@ -16,6 +17,54 @@ export function useCategoriesList(opts) {
16
17
  error: query.error,
17
18
  };
18
19
  }
20
+ /** API-driven for structured filters; text search + pagination done in hook (Firestore substring limitation). */
21
+ export function useCategoriesFiltered(params = {}) {
22
+ const sp = new URLSearchParams();
23
+ sp.set("flat", "true");
24
+ if (params.isFeatured)
25
+ sp.set("featured", "true");
26
+ if (params.isBrand)
27
+ sp.set("isBrand", "true");
28
+ if (params.rootOnly)
29
+ sp.set("tier", "0");
30
+ else if (params.tier !== undefined)
31
+ sp.set("tier", String(params.tier));
32
+ const qs = sp.toString();
33
+ const query = useQuery({
34
+ queryKey: ["categories", "filtered", qs],
35
+ queryFn: () => apiClient.get(`/api/categories?${qs}`),
36
+ staleTime: 2 * 60 * 1000,
37
+ });
38
+ const { page = 1, pageSize = 24, sort = "name", q = "", minProductCount, maxProductCount } = params;
39
+ const paginated = useMemo(() => {
40
+ let items = query.data ?? [];
41
+ const lq = q.toLowerCase();
42
+ if (lq)
43
+ items = items.filter((c) => c.name.toLowerCase().includes(lq) || (c.description ?? "").toLowerCase().includes(lq));
44
+ if (minProductCount !== undefined)
45
+ items = items.filter((c) => (c.productCount ?? 0) >= minProductCount);
46
+ if (maxProductCount !== undefined)
47
+ items = items.filter((c) => (c.productCount ?? 0) <= maxProductCount);
48
+ items = [...items].sort((a, b) => {
49
+ if (sort === "-productCount")
50
+ return (b.productCount ?? 0) - (a.productCount ?? 0);
51
+ if (sort === "-name")
52
+ return b.name.localeCompare(a.name);
53
+ return a.name.localeCompare(b.name);
54
+ });
55
+ const total = items.length;
56
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
57
+ const slice = items.slice((page - 1) * pageSize, page * pageSize);
58
+ return { items: slice, total, totalPages };
59
+ }, [query.data, q, sort, page, pageSize, minProductCount, maxProductCount]);
60
+ return {
61
+ categories: paginated.items,
62
+ total: paginated.total,
63
+ totalPages: paginated.totalPages,
64
+ isLoading: query.isLoading,
65
+ error: query.error,
66
+ };
67
+ }
19
68
  export function useCategoryDetail(slug, opts) {
20
69
  const categoryEndpoint = opts?.categoryEndpoint ?? CATEGORY_ENDPOINTS.BY_SLUG(encodeURIComponent(slug));
21
70
  const categoryQuery = useQuery({
@@ -0,0 +1,17 @@
1
+ import type { CategoryItem } from "../types";
2
+ /**
3
+ * Returns all categories flattened in DFS tree order:
4
+ * parent → its children → their children, etc.
5
+ * Suitable for use as filter facet options.
6
+ */
7
+ export declare function useCategoryTree(opts?: {
8
+ enabled?: boolean;
9
+ }): {
10
+ categories: CategoryItem[];
11
+ isLoading: boolean;
12
+ };
13
+ /** Convert category tree to FacetOption[] with indentation prefix for tier > 0. */
14
+ export declare function categoriesToFacetOptions(categories: CategoryItem[]): {
15
+ value: string;
16
+ label: string;
17
+ }[];
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { apiClient } from "../../../http";
4
+ import { CATEGORY_ENDPOINTS } from "../../../constants/api-endpoints";
5
+ /**
6
+ * Returns all categories flattened in DFS tree order:
7
+ * parent → its children → their children, etc.
8
+ * Suitable for use as filter facet options.
9
+ */
10
+ export function useCategoryTree(opts) {
11
+ const { data, isLoading } = useQuery({
12
+ queryKey: ["categories", "tree"],
13
+ queryFn: async () => {
14
+ const res = await apiClient.get(`${CATEGORY_ENDPOINTS.LIST}?pageSize=300&sort=tier,order,name`);
15
+ const items = Array.isArray(res)
16
+ ? res
17
+ : res.items ?? res.data ?? [];
18
+ return flattenTree(items);
19
+ },
20
+ enabled: opts?.enabled ?? true,
21
+ staleTime: 5 * 60 * 1000, // 5 min
22
+ });
23
+ return {
24
+ categories: data ?? [],
25
+ isLoading,
26
+ };
27
+ }
28
+ /** Build a flat DFS-ordered list from a flat array with parentIds. */
29
+ function flattenTree(items) {
30
+ // Build adjacency map
31
+ const byId = new Map();
32
+ const children = new Map();
33
+ for (const item of items) {
34
+ byId.set(item.id, item);
35
+ const parentId = item.parentIds?.[item.parentIds.length - 1] ?? null;
36
+ if (!children.has(parentId))
37
+ children.set(parentId, []);
38
+ children.get(parentId).push(item);
39
+ }
40
+ // Sort each level by order then name
41
+ const sortLevel = (nodes) => nodes.sort((a, b) => {
42
+ if (a.order != null && b.order != null)
43
+ return a.order - b.order;
44
+ return a.name.localeCompare(b.name);
45
+ });
46
+ const result = [];
47
+ function dfs(parentId) {
48
+ const nodes = children.get(parentId) ?? [];
49
+ for (const node of sortLevel([...nodes])) {
50
+ result.push(node);
51
+ dfs(node.id);
52
+ }
53
+ }
54
+ dfs(null);
55
+ return result;
56
+ }
57
+ /** Convert category tree to FacetOption[] with indentation prefix for tier > 0. */
58
+ export function categoriesToFacetOptions(categories) {
59
+ return categories.map((cat) => ({
60
+ value: cat.slug,
61
+ label: (cat.tier ?? 0) === 0
62
+ ? cat.name
63
+ : `${" ".repeat(cat.tier ?? 1)}↳ ${cat.name}`,
64
+ }));
65
+ }
@@ -0,0 +1,6 @@
1
+ import type { StackedViewShellProps } from "../../../ui";
2
+ export interface AdminEventEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
3
+ eventId?: string;
4
+ onSaved?: (id: string) => void;
5
+ }
6
+ export declare function AdminEventEditorView({ eventId, onSaved, ...rest }: AdminEventEditorViewProps): import("react/jsx-runtime").JSX.Element;