@mohasinac/appkit 2.7.30 → 2.7.31

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 (73) hide show
  1. package/dist/_internal/client/features/filters/filter-load-options.d.ts +29 -0
  2. package/dist/_internal/client/features/filters/filter-load-options.js +98 -0
  3. package/dist/_internal/server/features/classified/adapters.js +1 -1
  4. package/dist/_internal/server/features/digital-code/adapters.js +1 -1
  5. package/dist/_internal/server/features/live/adapters.js +1 -1
  6. package/dist/_internal/shared/actions/action-registry.js +194 -1
  7. package/dist/client.d.ts +2 -0
  8. package/dist/client.js +1 -0
  9. package/dist/constants/api-endpoints.d.ts +6 -0
  10. package/dist/constants/api-endpoints.js +2 -0
  11. package/dist/constants/field-names.d.ts +1 -0
  12. package/dist/constants/field-names.js +1 -0
  13. package/dist/features/about/components/PublicProfileView.js +2 -2
  14. package/dist/features/admin/components/AdminAddressEditorView.d.ts +7 -0
  15. package/dist/features/admin/components/AdminAddressEditorView.js +117 -0
  16. package/dist/features/admin/components/AdminCategoryEditorView.js +12 -9
  17. package/dist/features/admin/components/AdminFeatureFlagsView.js +58 -13
  18. package/dist/features/admin/components/AdminProductEditorView.d.ts +3 -1
  19. package/dist/features/admin/components/AdminProductEditorView.js +12 -9
  20. package/dist/features/admin/components/AdminReturnRequestsView.js +5 -4
  21. package/dist/features/admin/components/AdminReviewsView.js +5 -4
  22. package/dist/features/admin/components/index.d.ts +2 -0
  23. package/dist/features/admin/components/index.js +1 -0
  24. package/dist/features/auctions/components/AuctionBidsTable.d.ts +13 -0
  25. package/dist/features/auctions/components/AuctionBidsTable.js +62 -0
  26. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  27. package/dist/features/auctions/components/AuctionFilters.d.ts +7 -1
  28. package/dist/features/auctions/components/AuctionFilters.js +3 -2
  29. package/dist/features/auctions/components/index.d.ts +2 -0
  30. package/dist/features/auctions/components/index.js +1 -0
  31. package/dist/features/cart/components/CartView.js +2 -1
  32. package/dist/features/cart/components/CheckoutView.js +2 -1
  33. package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
  34. package/dist/features/categories/components/BrandDetailTabs.js +29 -15
  35. package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
  36. package/dist/features/categories/components/CategoryDetailTabs.js +30 -15
  37. package/dist/features/classified/components/ClassifiedFilters.d.ts +5 -1
  38. package/dist/features/classified/components/ClassifiedFilters.js +3 -2
  39. package/dist/features/digital-codes/components/DigitalCodeFilters.d.ts +5 -1
  40. package/dist/features/digital-codes/components/DigitalCodeFilters.js +3 -2
  41. package/dist/features/events/repository/event-entry.repository.d.ts +1 -0
  42. package/dist/features/events/repository/event-entry.repository.js +5 -0
  43. package/dist/features/filters/index.d.ts +2 -0
  44. package/dist/features/filters/index.js +1 -0
  45. package/dist/features/layout/AppLayoutShell.d.ts +1 -1
  46. package/dist/features/layout/AppLayoutShell.js +1 -1
  47. package/dist/features/live/components/LiveItemFilters.d.ts +5 -1
  48. package/dist/features/live/components/LiveItemFilters.js +3 -2
  49. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
  50. package/dist/features/pre-orders/components/PreOrderFilters.d.ts +7 -1
  51. package/dist/features/pre-orders/components/PreOrderFilters.js +3 -2
  52. package/dist/features/products/columns/productTableColumns.js +1 -1
  53. package/dist/features/products/components/CompareOverlay.d.ts +2 -0
  54. package/dist/features/products/components/CompareOverlay.js +1 -1
  55. package/dist/features/products/components/ProductFilters.d.ts +7 -1
  56. package/dist/features/products/components/ProductFilters.js +3 -2
  57. package/dist/features/products/components/ProductForm.js +6 -6
  58. package/dist/features/products/components/ProductGrid.js +1 -1
  59. package/dist/features/products/repository/products.repository.d.ts +4 -0
  60. package/dist/features/products/repository/products.repository.js +22 -3
  61. package/dist/features/products/schemas/firestore.d.ts +7 -1
  62. package/dist/features/products/types/index.d.ts +4 -0
  63. package/dist/features/stores/components/StoreDetailLayoutView.js +16 -2
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.js +6 -0
  66. package/dist/next/routing/route-map.d.ts +14 -0
  67. package/dist/next/routing/route-map.js +7 -0
  68. package/dist/seed/products-auctions-seed-data.js +40 -40
  69. package/dist/seed/products-preorders-seed-data.js +16 -16
  70. package/dist/seed/products-prize-draws-seed-data.js +4 -4
  71. package/dist/seed/products-standard-seed-data.js +202 -202
  72. package/dist/tailwind-utilities.css +1 -1
  73. package/package.json +1 -1
@@ -12,25 +12,40 @@ function tabLabel(label, count) {
12
12
  return label;
13
13
  return `${label} (${count.toLocaleString()})`;
14
14
  }
15
- export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], counts, }) {
16
- const [activeTab, setActiveTab] = useState("products");
15
+ /** Maps CATEGORY_PAGE_TABS id listing type / category type key for flag filtering. */
16
+ const TAB_TYPE_MAP = {
17
+ products: { kind: "listing", type: "standard" },
18
+ auctions: { kind: "listing", type: "auction" },
19
+ "pre-orders": { kind: "listing", type: "pre-order" },
20
+ "prize-draws": { kind: "listing", type: "prize-draw" },
21
+ bundles: { kind: "category", type: "bundle" },
22
+ };
23
+ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
24
+ const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
25
+ const mapping = TAB_TYPE_MAP[t.id];
26
+ if (!mapping)
27
+ return true;
28
+ if (mapping.kind === "listing" && enabledListingTypes) {
29
+ return enabledListingTypes.includes(mapping.type);
30
+ }
31
+ if (mapping.kind === "category" && enabledCategoryTypes) {
32
+ return enabledCategoryTypes.includes(mapping.type);
33
+ }
34
+ return true;
35
+ });
36
+ const firstTabId = (visibleTabs[0]?.id ?? "products");
37
+ const [activeTab, setActiveTab] = useState(firstTabId);
17
38
  const countFor = (id) => {
18
39
  switch (id) {
19
- case "products":
20
- return counts?.products;
21
- case "auctions":
22
- return counts?.auctions;
23
- case "pre-orders":
24
- return counts?.preOrders;
25
- case "prize-draws":
26
- return counts?.prizeDraws;
27
- case "bundles":
28
- return counts?.bundles;
29
- default:
30
- return undefined;
40
+ case "products": return counts?.products;
41
+ case "auctions": return counts?.auctions;
42
+ case "pre-orders": return counts?.preOrders;
43
+ case "prize-draws": return counts?.prizeDraws;
44
+ case "bundles": return counts?.bundles;
45
+ default: return undefined;
31
46
  }
32
47
  };
33
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex border-b border-zinc-200 dark:border-slate-700 mb-6 overflow-x-auto", children: CATEGORY_PAGE_TABS.map((t) => (_jsx("button", { type: "button", onClick: () => setActiveTab(t.id), className: `px-5 py-2.5 text-sm font-medium whitespace-nowrap transition-colors -mb-px border-b-2 ${activeTab === t.id
48
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex border-b border-zinc-200 dark:border-slate-700 mb-6 overflow-x-auto", children: visibleTabs.map((t) => (_jsx("button", { type: "button", onClick: () => setActiveTab(t.id), className: `px-5 py-2.5 text-sm font-medium whitespace-nowrap transition-colors -mb-px border-b-2 ${activeTab === t.id
34
49
  ? "border-primary text-primary"
35
50
  : "border-transparent text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100"}`, children: tabLabel(t.label, countFor(t.id)) }, t.id))) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles }))] }));
36
51
  }
@@ -1,8 +1,12 @@
1
1
  import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
+ import type { AsyncFacetSectionProps } from "../../filters/AsyncFacetSection";
4
+ type LoadOptionsFn = AsyncFacetSectionProps["loadOptions"];
3
5
  export interface ClassifiedFiltersProps {
4
6
  table: UrlTable;
5
7
  categoryOptions?: FacetOption[];
8
+ loadCategoryOptions?: LoadOptionsFn;
6
9
  currencyPrefix?: string;
7
10
  }
8
- export declare function ClassifiedFilters({ table, categoryOptions, currencyPrefix, }: ClassifiedFiltersProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function ClassifiedFilters({ table, categoryOptions, loadCategoryOptions, currencyPrefix, }: ClassifiedFiltersProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
4
+ import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
4
5
  import { RangeFilter } from "../../filters/RangeFilter";
5
6
  import { Div } from "../../../ui";
6
7
  const NEGOTIABLE_OPTIONS = [
@@ -9,11 +10,11 @@ const NEGOTIABLE_OPTIONS = [
9
10
  const SHIPPING_OPTIONS = [
10
11
  { value: "true", label: "Accepts shipping" },
11
12
  ];
12
- export function ClassifiedFilters({ table, categoryOptions = [], currencyPrefix = "₹", }) {
13
+ export function ClassifiedFilters({ table, categoryOptions = [], loadCategoryOptions, currencyPrefix = "₹", }) {
13
14
  const selectedCategories = table.get("category")
14
15
  ? table.get("category").split("|").filter(Boolean)
15
16
  : [];
16
17
  const selectedNegotiable = table.get("negotiable") ? [table.get("negotiable")] : [];
17
18
  const selectedShipping = table.get("acceptsShipping") ? [table.get("acceptsShipping")] : [];
18
- return (_jsxs(Div, { children: [categoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Category", options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })), _jsxs("div", { className: "border-t border-[var(--appkit-color-border)] pt-4 mt-4", children: [_jsx("label", { className: "block text-sm font-medium text-[var(--appkit-color-text)] mb-2", children: "City / Area" }), _jsx("input", { type: "text", value: table.get("city") || "", onChange: (e) => table.set("city", e.target.value), placeholder: "e.g. Mumbai", className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-[var(--appkit-color-text-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]" })] }), _jsx(FilterFacetSection, { title: "Negotiable", options: NEGOTIABLE_OPTIONS, selected: selectedNegotiable, onChange: (vals) => table.set("negotiable", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(FilterFacetSection, { title: "Shipping", options: SHIPPING_OPTIONS, selected: selectedShipping, onChange: (vals) => table.set("acceptsShipping", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: "Asking Price", minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 100, minPlaceholder: "Min price", maxPlaceholder: "Max price", defaultCollapsed: false })] }));
19
+ return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: "Category", loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: "Category", options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsxs("div", { className: "border-t border-[var(--appkit-color-border)] pt-4 mt-4", children: [_jsx("label", { className: "block text-sm font-medium text-[var(--appkit-color-text)] mb-2", children: "City / Area" }), _jsx("input", { type: "text", value: table.get("city") || "", onChange: (e) => table.set("city", e.target.value), placeholder: "e.g. Mumbai", className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-[var(--appkit-color-text-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]" })] }), _jsx(FilterFacetSection, { title: "Negotiable", options: NEGOTIABLE_OPTIONS, selected: selectedNegotiable, onChange: (vals) => table.set("negotiable", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(FilterFacetSection, { title: "Shipping", options: SHIPPING_OPTIONS, selected: selectedShipping, onChange: (vals) => table.set("acceptsShipping", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: "Asking Price", minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 100, minPlaceholder: "Min price", maxPlaceholder: "Max price", defaultCollapsed: false })] }));
19
20
  }
@@ -1,8 +1,12 @@
1
1
  import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
+ import type { AsyncFacetSectionProps } from "../../filters/AsyncFacetSection";
4
+ type LoadOptionsFn = AsyncFacetSectionProps["loadOptions"];
3
5
  export interface DigitalCodeFiltersProps {
4
6
  table: UrlTable;
5
7
  categoryOptions?: FacetOption[];
8
+ loadCategoryOptions?: LoadOptionsFn;
6
9
  currencyPrefix?: string;
7
10
  }
8
- export declare function DigitalCodeFilters({ table, categoryOptions, currencyPrefix, }: DigitalCodeFiltersProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function DigitalCodeFilters({ table, categoryOptions, loadCategoryOptions, currencyPrefix, }: DigitalCodeFiltersProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -1,16 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
4
+ import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
4
5
  import { RangeFilter } from "../../filters/RangeFilter";
5
6
  import { Div } from "../../../ui";
6
7
  const DELIVERY_OPTIONS = [
7
8
  { value: "auto-claim", label: "Instant (auto-claim)" },
8
9
  { value: "manual-email", label: "Manual (sent by seller)" },
9
10
  ];
10
- export function DigitalCodeFilters({ table, categoryOptions = [], currencyPrefix = "₹", }) {
11
+ export function DigitalCodeFilters({ table, categoryOptions = [], loadCategoryOptions, currencyPrefix = "₹", }) {
11
12
  const selectedCategories = table.get("category")
12
13
  ? table.get("category").split("|").filter(Boolean)
13
14
  : [];
14
15
  const selectedDelivery = table.get("deliveryMethod") ? [table.get("deliveryMethod")] : [];
15
- return (_jsxs(Div, { children: [categoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Category", options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })), _jsx(FilterFacetSection, { title: "Delivery Method", options: DELIVERY_OPTIONS, selected: selectedDelivery, onChange: (vals) => table.set("deliveryMethod", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: "Price per Code", minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 50000, step: 50, minPlaceholder: "Min price", maxPlaceholder: "Max price", defaultCollapsed: false })] }));
16
+ return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: "Category", loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: "Category", options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: "Delivery Method", options: DELIVERY_OPTIONS, selected: selectedDelivery, onChange: (vals) => table.set("deliveryMethod", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: "Price per Code", minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 50000, step: 50, minPlaceholder: "Min price", maxPlaceholder: "Max price", defaultCollapsed: false })] }));
16
17
  }
@@ -6,6 +6,7 @@ declare class EventEntryRepository extends BaseRepository<EventEntryDocument> {
6
6
  constructor();
7
7
  protected mapDoc<D = EventEntryDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
8
8
  listForEvent(eventId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
9
+ listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
9
10
  hasUserEntered(eventId: string, userId: string): Promise<boolean>;
10
11
  countUserEntries(eventId: string, userId: string): Promise<number>;
11
12
  getLeaderboard(eventId: string, limit?: number): Promise<EventEntryDocument[]>;
@@ -18,6 +18,11 @@ class EventEntryRepository extends BaseRepository {
18
18
  baseQuery: this.getCollection().where(EVENT_ENTRY_FIELDS.EVENT_ID, "==", eventId),
19
19
  });
20
20
  }
21
+ async listForUser(userId, model) {
22
+ return this.sieveQuery(model, EventEntryRepository.SIEVE_FIELDS, {
23
+ baseQuery: this.getCollection().where(EVENT_ENTRY_FIELDS.USER_ID, "==", userId),
24
+ });
25
+ }
21
26
  async hasUserEntered(eventId, userId) {
22
27
  try {
23
28
  const snapshot = await this.getCollection()
@@ -1,5 +1,7 @@
1
1
  export { FilterFacetSection } from "./FilterFacetSection";
2
2
  export type { FilterFacetSectionProps, FacetOption, } from "./FilterFacetSection";
3
+ export { AsyncFacetSection } from "./AsyncFacetSection";
4
+ export type { AsyncFacetSectionProps } from "./AsyncFacetSection";
3
5
  export { RangeFilter } from "./RangeFilter";
4
6
  export type { RangeFilterProps } from "./RangeFilter";
5
7
  export { SwitchFilter } from "./SwitchFilter";
@@ -1,5 +1,6 @@
1
1
  // Public API for @mohasinac/feat-filters
2
2
  export { FilterFacetSection } from "./FilterFacetSection";
3
+ export { AsyncFacetSection } from "./AsyncFacetSection";
3
4
  export { RangeFilter } from "./RangeFilter";
4
5
  export { SwitchFilter } from "./SwitchFilter";
5
6
  export { FilterPanel } from "./FilterPanel";
@@ -90,7 +90,7 @@ export interface AppLayoutShellProps {
90
90
  eventBannerSlot?: React.ReactNode;
91
91
  /**
92
92
  * Override className for the main content wrapper div.
93
- * When set, replaces the default `container mx-auto w-full max-w-screen-2xl px-4 py-6 …`
93
+ * When set, replaces the default `w-full px-4 py-6 …`
94
94
  * Use `"w-full"` for dashboard layouts that provide their own padding via DashboardLayoutClient.
95
95
  */
96
96
  contentClassName?: string;
@@ -147,5 +147,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
147
147
  opacity: darkBackground.overlay?.opacity ?? 0,
148
148
  },
149
149
  };
150
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { className: contentClassName ?? "container mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 lg:px-8", children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
150
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { className: contentClassName ?? "w-full px-4 py-6 md:px-6 lg:px-8", children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
151
151
  }
@@ -1,8 +1,12 @@
1
1
  import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
+ import type { AsyncFacetSectionProps } from "../../filters/AsyncFacetSection";
4
+ type LoadOptionsFn = AsyncFacetSectionProps["loadOptions"];
3
5
  export interface LiveItemFiltersProps {
4
6
  table: UrlTable;
5
7
  categoryOptions?: FacetOption[];
8
+ loadCategoryOptions?: LoadOptionsFn;
6
9
  currencyPrefix?: string;
7
10
  }
8
- export declare function LiveItemFilters({ table, categoryOptions, currencyPrefix, }: LiveItemFiltersProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function LiveItemFilters({ table, categoryOptions, loadCategoryOptions, currencyPrefix, }: LiveItemFiltersProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
4
+ import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
4
5
  import { RangeFilter } from "../../filters/RangeFilter";
5
6
  import { Div } from "../../../ui";
6
7
  const SEX_OPTIONS = [
@@ -13,11 +14,11 @@ const TRANSPORT_OPTIONS = [
13
14
  { value: "courier", label: "Live-animal courier" },
14
15
  { value: "specialist", label: "Specialist transport" },
15
16
  ];
16
- export function LiveItemFilters({ table, categoryOptions = [], currencyPrefix = "₹", }) {
17
+ export function LiveItemFilters({ table, categoryOptions = [], loadCategoryOptions, currencyPrefix = "₹", }) {
17
18
  const selectedCategories = table.get("category")
18
19
  ? table.get("category").split("|").filter(Boolean)
19
20
  : [];
20
21
  const selectedSex = table.get("liveSex") ? [table.get("liveSex")] : [];
21
22
  const selectedTransport = table.get("liveTransportMethod") ? [table.get("liveTransportMethod")] : [];
22
- return (_jsxs(Div, { children: [categoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Category", options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })), _jsxs("div", { className: "border-t border-[var(--appkit-color-border)] pt-4 mt-4", children: [_jsx("label", { className: "block text-sm font-medium text-[var(--appkit-color-text)] mb-2", children: "Species / Animal" }), _jsx("input", { type: "text", value: table.get("species") || "", onChange: (e) => table.set("species", e.target.value), placeholder: "e.g. Axolotl, Parrot", className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-[var(--appkit-color-text-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]" })] }), _jsxs("div", { className: "border-t border-[var(--appkit-color-border)] pt-4 mt-4", children: [_jsx("label", { className: "block text-sm font-medium text-[var(--appkit-color-text)] mb-2", children: "Jurisdiction (state/UT)" }), _jsx("input", { type: "text", value: table.get("jurisdiction") || "", onChange: (e) => table.set("jurisdiction", e.target.value), placeholder: "e.g. Maharashtra", className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-[var(--appkit-color-text-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]" })] }), _jsx(FilterFacetSection, { title: "Sex", options: SEX_OPTIONS, selected: selectedSex, onChange: (vals) => table.set("liveSex", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(FilterFacetSection, { title: "Transport Method", options: TRANSPORT_OPTIONS, selected: selectedTransport, onChange: (vals) => table.set("liveTransportMethod", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: "Price", minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 200000, step: 100, minPlaceholder: "Min price", maxPlaceholder: "Max price", defaultCollapsed: false })] }));
23
+ return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: "Category", loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: "Category", options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsxs("div", { className: "border-t border-[var(--appkit-color-border)] pt-4 mt-4", children: [_jsx("label", { className: "block text-sm font-medium text-[var(--appkit-color-text)] mb-2", children: "Species / Animal" }), _jsx("input", { type: "text", value: table.get("species") || "", onChange: (e) => table.set("species", e.target.value), placeholder: "e.g. Axolotl, Parrot", className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-[var(--appkit-color-text-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]" })] }), _jsxs("div", { className: "border-t border-[var(--appkit-color-border)] pt-4 mt-4", children: [_jsx("label", { className: "block text-sm font-medium text-[var(--appkit-color-text)] mb-2", children: "Jurisdiction (state/UT)" }), _jsx("input", { type: "text", value: table.get("jurisdiction") || "", onChange: (e) => table.set("jurisdiction", e.target.value), placeholder: "e.g. Maharashtra", className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-[var(--appkit-color-text-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]" })] }), _jsx(FilterFacetSection, { title: "Sex", options: SEX_OPTIONS, selected: selectedSex, onChange: (vals) => table.set("liveSex", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(FilterFacetSection, { title: "Transport Method", options: TRANSPORT_OPTIONS, selected: selectedTransport, onChange: (vals) => table.set("liveTransportMethod", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: "Price", minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 200000, step: 100, minPlaceholder: "Min price", maxPlaceholder: "Max price", defaultCollapsed: false })] }));
23
24
  }
@@ -112,8 +112,8 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
112
112
  const storeHref = storeSlug
113
113
  ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug))
114
114
  : null;
115
- const category = typeof p.category === "string" ? p.category : null;
116
- const categoryName = typeof p.categoryName === "string" ? p.categoryName : null;
115
+ const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? p.categorySlugs[0] : (typeof p.category === "string" ? p.category : null);
116
+ const categoryName = Array.isArray(p.categoryNames) && p.categoryNames.length > 0 ? p.categoryNames[0] : (typeof p.categoryName === "string" ? p.categoryName : null);
117
117
  const brand = typeof p.brand === "string" ? p.brand : null;
118
118
  const brandSlug = typeof p.brandSlug === "string" ? p.brandSlug : null;
119
119
  const features = Array.isArray(p.features) ? p.features : [];
@@ -1,10 +1,16 @@
1
1
  import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
+ import type { AsyncFacetSectionProps } from "../../filters/AsyncFacetSection";
4
+ type LoadOptionsFn = AsyncFacetSectionProps["loadOptions"];
3
5
  export interface PreOrderFiltersProps {
4
6
  table: UrlTable;
5
7
  categoryOptions?: FacetOption[];
6
8
  brandOptions?: FacetOption[];
7
9
  storeOptions?: FacetOption[];
10
+ loadCategoryOptions?: LoadOptionsFn;
11
+ loadBrandOptions?: LoadOptionsFn;
12
+ loadStoreOptions?: LoadOptionsFn;
8
13
  currencyPrefix?: string;
9
14
  }
10
- export declare function PreOrderFilters({ table, categoryOptions, brandOptions, storeOptions, currencyPrefix, }: PreOrderFiltersProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function PreOrderFilters({ table, categoryOptions, brandOptions, storeOptions, loadCategoryOptions, loadBrandOptions, loadStoreOptions, currencyPrefix, }: PreOrderFiltersProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -2,9 +2,10 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslations } from "next-intl";
4
4
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
5
+ import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
5
6
  import { RangeFilter } from "../../filters/RangeFilter";
6
7
  import { Div } from "../../../ui";
7
- export function PreOrderFilters({ table, categoryOptions = [], brandOptions = [], storeOptions = [], currencyPrefix = "", }) {
8
+ export function PreOrderFilters({ table, categoryOptions = [], brandOptions = [], storeOptions = [], loadCategoryOptions, loadBrandOptions, loadStoreOptions, currencyPrefix = "", }) {
8
9
  const t = useTranslations("filters");
9
10
  const selectedCategories = table.get("category")
10
11
  ? table.get("category").split("|").filter(Boolean)
@@ -21,5 +22,5 @@ export function PreOrderFilters({ table, categoryOptions = [], brandOptions = []
21
22
  const selectedStores = table.get("storeId")
22
23
  ? table.get("storeId").split("|").filter(Boolean)
23
24
  : [];
24
- return (_jsxs(Div, { children: [categoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })), brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })), _jsx(FilterFacetSection, { title: t("productionStatus"), options: productionStatusOptions, selected: selectedProductionStatuses, onChange: (vals) => table.set("preOrderProductionStatus", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 100, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), storeOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("store"), options: storeOptions, selected: selectedStores, onChange: (vals) => table.set("storeId", vals[0] ?? ""), searchable: storeOptions.length > 4, selectionMode: "single", defaultCollapsed: storeOptions.length > 6 })), _jsx(RangeFilter, { title: "Delivery Date Range", 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: false })] }));
25
+ return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set("brand", vals[0] ?? ""), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: t("productionStatus"), options: productionStatusOptions, selected: selectedProductionStatuses, onChange: (vals) => table.set("preOrderProductionStatus", vals[0] ?? ""), searchable: false, selectionMode: "single", defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get("minPrice"), maxValue: table.get("maxPrice"), onMinChange: (v) => table.set("minPrice", v), onMaxChange: (v) => table.set("maxPrice", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 100, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedStores, onChange: (vals) => table.set("storeId", vals[0] ?? ""), defaultCollapsed: false })) : storeOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: storeOptions, selected: selectedStores, onChange: (vals) => table.set("storeId", vals[0] ?? ""), searchable: storeOptions.length > 4, selectionMode: "single", defaultCollapsed: storeOptions.length > 6 })) : null, _jsx(RangeFilter, { title: "Delivery Date Range", 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: false })] }));
25
26
  }
@@ -23,7 +23,7 @@ export function getProductTableColumns({ labels, onEdit, onDelete, currencyCode,
23
23
  header: labels.category,
24
24
  sortable: true,
25
25
  width: "12%",
26
- render: (product) => _jsx(Span, { children: product.category ?? "-" }),
26
+ render: (product) => _jsx(Span, { children: (Array.isArray(product.categorySlugs) ? product.categorySlugs[0] : product.category) ?? "-" }),
27
27
  },
28
28
  {
29
29
  key: "price",
@@ -12,6 +12,8 @@ export interface CompareProductLike {
12
12
  brand?: string;
13
13
  category?: string;
14
14
  categoryName?: string;
15
+ categorySlugs?: string[];
16
+ categoryNames?: string[];
15
17
  storeName?: string;
16
18
  storeSlug?: string;
17
19
  /** Canonical discriminator (SB1-G Phase 4). */
@@ -70,7 +70,7 @@ function CompareColumn({ item, productType, labels, onRemove, onClose }) {
70
70
  const img = item.mainImage ?? item.images?.[0];
71
71
  return (_jsxs(Stack, { gap: "md", className: COLUMN_CARD_CLASS, children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100 hover:underline line-clamp-2", children: item.title ?? item.id }), onRemove && (_jsx("button", { type: "button", onClick: () => onRemove(item.id), "aria-label": `${labels.remove} ${item.title ?? item.id}`, className: "shrink-0 rounded-full p-1 text-zinc-400 hover:bg-zinc-100 hover:text-rose-500 dark:hover:bg-zinc-800", children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsx(FieldRow, { label: labels.field.image, children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, children: _jsx(Div, { className: "relative aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: img ? (
72
72
  // eslint-disable-next-line @next/next/no-img-element
73
- _jsx("img", { src: img, alt: item.title ?? item.id, className: "h-full w-full object-cover" })) : null }) }) }), _jsx(FieldRow, { label: labels.field.price, children: _jsx(Text, { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: priceLabel(item) }) }), _jsx(FieldRow, { label: labels.field.condition, children: _jsx(Text, { children: item.condition ? _jsx("span", { className: CHIP_CLASS, children: item.condition }) : "—" }) }), _jsx(FieldRow, { label: labels.field.brand, children: _jsx(Text, { children: item.brand ? _jsx("span", { className: CHIP_CLASS, children: item.brand }) : "—" }) }), _jsx(FieldRow, { label: labels.field.category, children: _jsx(Text, { children: item.categoryName || item.category ? (_jsx("span", { className: CHIP_CLASS, children: item.categoryName ?? item.category })) : ("—") }) }), _jsx(FieldRow, { label: labels.field.store, children: _jsx(Text, { className: "text-sm text-zinc-700 dark:text-zinc-300", children: item.storeName ?? "—" }) }), _jsx(Button, { asChild: true, variant: "primary", size: "sm", className: "mt-auto", children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, onClick: onClose, children: labels.view }) })] }));
73
+ _jsx("img", { src: img, alt: item.title ?? item.id, className: "h-full w-full object-cover" })) : null }) }) }), _jsx(FieldRow, { label: labels.field.price, children: _jsx(Text, { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: priceLabel(item) }) }), _jsx(FieldRow, { label: labels.field.condition, children: _jsx(Text, { children: item.condition ? _jsx("span", { className: CHIP_CLASS, children: item.condition }) : "—" }) }), _jsx(FieldRow, { label: labels.field.brand, children: _jsx(Text, { children: item.brand ? _jsx("span", { className: CHIP_CLASS, children: item.brand }) : "—" }) }), _jsx(FieldRow, { label: labels.field.category, children: _jsx(Text, { children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) || (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) ? (_jsx("span", { className: CHIP_CLASS, children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) ?? (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) })) : ("—") }) }), _jsx(FieldRow, { label: labels.field.store, children: _jsx(Text, { className: "text-sm text-zinc-700 dark:text-zinc-300", children: item.storeName ?? "—" }) }), _jsx(Button, { asChild: true, variant: "primary", size: "sm", className: "mt-auto", children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, onClick: onClose, children: labels.view }) })] }));
74
74
  }
75
75
  function useFetchedProducts(productIds, passedItems, isOpen) {
76
76
  const [fetched, setFetched] = useState([]);
@@ -1,5 +1,7 @@
1
1
  import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
+ import type { AsyncFacetSectionProps } from "../../filters/AsyncFacetSection";
4
+ type LoadOptionsFn = AsyncFacetSectionProps["loadOptions"];
3
5
  export type { FacetOption, UrlTable };
4
6
  export type ProductFilterVariant = "admin" | "seller" | "public";
5
7
  export declare const PRODUCT_FILTER_KEYS: {
@@ -81,6 +83,10 @@ export interface ProductFiltersProps {
81
83
  /** @deprecated use storeOptions */
82
84
  sellerOptions?: FacetOption[];
83
85
  tagOptions?: FacetOption[];
86
+ /** Async load functions — when provided, use AsyncFacetSection instead of static FilterFacetSection */
87
+ loadCategoryOptions?: LoadOptionsFn;
88
+ loadBrandOptions?: LoadOptionsFn;
89
+ loadStoreOptions?: LoadOptionsFn;
84
90
  /** Sublisting category options — separate from main category filter */
85
91
  sublistingCategoryOptions?: FacetOption[];
86
92
  /** Feature badge options for filtering by product features */
@@ -94,4 +100,4 @@ export interface ProductFiltersProps {
94
100
  /** Show "part of a bundle" switch filter (default true) */
95
101
  showBundleFilter?: boolean;
96
102
  }
97
- export declare function ProductFilters({ table, categoryOptions, brandOptions, storeOptions, sellerOptions, tagOptions, sublistingCategoryOptions, featureOptions, showStatus, variant, statusOptions, currencyPrefix, showShipping, showBundleFilter, }: ProductFiltersProps): import("react/jsx-runtime").JSX.Element;
103
+ export declare function ProductFilters({ table, categoryOptions, brandOptions, storeOptions, sellerOptions, tagOptions, sublistingCategoryOptions, featureOptions, showStatus, variant, statusOptions, currencyPrefix, showShipping, showBundleFilter, loadCategoryOptions, loadBrandOptions, loadStoreOptions, }: ProductFiltersProps): import("react/jsx-runtime").JSX.Element;
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslations } from "next-intl";
4
4
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
5
+ import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
5
6
  import { RangeFilter } from "../../filters/RangeFilter";
6
7
  import { SwitchFilter } from "../../filters/SwitchFilter";
7
8
  import { Div } from "../../../ui";
@@ -89,7 +90,7 @@ export function getProductSortOptions(variant) {
89
90
  return PRODUCT_PUBLIC_SORT_OPTIONS;
90
91
  }
91
92
  }
92
- export function ProductFilters({ table, categoryOptions = [], brandOptions = [], storeOptions = [], sellerOptions, tagOptions = [], sublistingCategoryOptions = [], featureOptions = [], showStatus = false, variant, statusOptions, currencyPrefix = "", showShipping = true, showBundleFilter = true, }) {
93
+ export function ProductFilters({ table, categoryOptions = [], brandOptions = [], storeOptions = [], sellerOptions, tagOptions = [], sublistingCategoryOptions = [], featureOptions = [], showStatus = false, variant, statusOptions, currencyPrefix = "", showShipping = true, showBundleFilter = true, loadCategoryOptions, loadBrandOptions, loadStoreOptions, }) {
93
94
  const resolvedStoreOptions = storeOptions.length > 0 ? storeOptions : (sellerOptions ?? []);
94
95
  const t = useTranslations("filters");
95
96
  const conditionOptions = [
@@ -114,5 +115,5 @@ export function ProductFilters({ table, categoryOptions = [], brandOptions = [],
114
115
  const selectedFeatures = splitPipe(TABLE_KEYS.FEATURES);
115
116
  const resolvedVariant = variant ?? (showStatus ? "admin" : "public");
116
117
  const shouldShowStatus = resolvedVariant !== "public" || showStatus;
117
- return (_jsxs(Div, { children: [categoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })), _jsx(FilterFacetSection, { title: t("condition"), options: conditionOptions, selected: selectedConditions, onChange: (vals) => table.set(TABLE_KEYS.CONDITION, vals.join("|")), searchable: false, defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get(TABLE_KEYS.MIN_PRICE), maxValue: table.get(TABLE_KEYS.MAX_PRICE), onMinChange: (v) => table.set(TABLE_KEYS.MIN_PRICE, v), onMaxChange: (v) => table.set(TABLE_KEYS.MAX_PRICE, v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })), resolvedStoreOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("store"), options: resolvedStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), searchable: resolvedStoreOptions.length > 4, defaultCollapsed: resolvedStoreOptions.length > 6 })), showShipping && (_jsx(SwitchFilter, { title: t("shipping"), label: t("freeShippingOnly"), checked: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (v) => table.set(TABLE_KEYS.FREE_SHIPPING, v ? "true" : ""), defaultCollapsed: false })), tagOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("tags"), options: tagOptions, selected: selectedTags, onChange: (vals) => table.set(TABLE_KEYS.TAGS, vals.join("|")), searchable: tagOptions.length > 4, defaultCollapsed: tagOptions.length > 6 })), shouldShowStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions ?? defaultStatusOptions, selected: selectedStatuses, onChange: (vals) => table.set(TABLE_KEYS.STATUS, vals.join("|")), searchable: false, defaultCollapsed: false })), sublistingCategoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Sublisting Type", options: sublistingCategoryOptions, selected: selectedSublistings, onChange: (vals) => table.set(TABLE_KEYS.SUBLISTING_CATEGORY, vals.join("|")), searchable: sublistingCategoryOptions.length > 4, defaultCollapsed: false })), showBundleFilter && (_jsx(SwitchFilter, { title: "Bundles", label: "Part of a bundle only", checked: table.get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true", onChange: (v) => table.set(TABLE_KEYS.IS_PART_OF_BUNDLE, v ? "true" : ""), defaultCollapsed: false })), featureOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Features", options: featureOptions, selected: selectedFeatures, onChange: (vals) => table.set(TABLE_KEYS.FEATURES, vals.join("|")), searchable: featureOptions.length > 4, defaultCollapsed: featureOptions.length > 4 }))] }));
118
+ return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: t("condition"), options: conditionOptions, selected: selectedConditions, onChange: (vals) => table.set(TABLE_KEYS.CONDITION, vals.join("|")), searchable: false, defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get(TABLE_KEYS.MIN_PRICE), maxValue: table.get(TABLE_KEYS.MAX_PRICE), onMinChange: (v) => table.set(TABLE_KEYS.MIN_PRICE, v), onMaxChange: (v) => table.set(TABLE_KEYS.MAX_PRICE, v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), defaultCollapsed: false })) : resolvedStoreOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: resolvedStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), searchable: resolvedStoreOptions.length > 4, defaultCollapsed: resolvedStoreOptions.length > 6 })) : null, showShipping && (_jsx(SwitchFilter, { title: t("shipping"), label: t("freeShippingOnly"), checked: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (v) => table.set(TABLE_KEYS.FREE_SHIPPING, v ? "true" : ""), defaultCollapsed: false })), tagOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("tags"), options: tagOptions, selected: selectedTags, onChange: (vals) => table.set(TABLE_KEYS.TAGS, vals.join("|")), searchable: tagOptions.length > 4, defaultCollapsed: tagOptions.length > 6 })), shouldShowStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions ?? defaultStatusOptions, selected: selectedStatuses, onChange: (vals) => table.set(TABLE_KEYS.STATUS, vals.join("|")), searchable: false, defaultCollapsed: false })), sublistingCategoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Sublisting Type", options: sublistingCategoryOptions, selected: selectedSublistings, onChange: (vals) => table.set(TABLE_KEYS.SUBLISTING_CATEGORY, vals.join("|")), searchable: sublistingCategoryOptions.length > 4, defaultCollapsed: false })), showBundleFilter && (_jsx(SwitchFilter, { title: "Bundles", label: "Part of a bundle only", checked: table.get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true", onChange: (v) => table.set(TABLE_KEYS.IS_PART_OF_BUNDLE, v ? "true" : ""), defaultCollapsed: false })), featureOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Features", options: featureOptions, selected: selectedFeatures, onChange: (vals) => table.set(TABLE_KEYS.FEATURES, vals.join("|")), searchable: featureOptions.length > 4, defaultCollapsed: featureOptions.length > 4 }))] }));
118
119
  }
@@ -43,7 +43,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
43
43
  type: "product-image",
44
44
  index: galleryIndexRef.current,
45
45
  name: product.title || "product",
46
- category: product.category || "uncategorized",
46
+ category: (product.categorySlugs?.[0] ?? product.category) || "uncategorized",
47
47
  store: product.storeName || "store",
48
48
  });
49
49
  };
@@ -52,7 +52,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
52
52
  type: "product-video",
53
53
  index: 1,
54
54
  name: product.title || "product",
55
- category: product.category || "uncategorized",
55
+ category: (product.categorySlugs?.[0] ?? product.category) || "uncategorized",
56
56
  store: product.storeName || "store",
57
57
  });
58
58
  };
@@ -63,10 +63,10 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
63
63
  editable: !isReadonly,
64
64
  }) })) : (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: t("formDescription") }), _jsx(RichTextEditor, { value: normalizeRichTextHtml(product.description || ""), onChange: (value) => update({ description: value }), disabled: isReadonly, placeholder: "Enter product description" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Rich text is supported for product descriptions." })] })), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsx(_Fragment, { children: renderCategorySelector({
65
65
  label: t("formCategory"),
66
- value: product.category || "",
67
- onChange: (value) => update({ category: value }),
66
+ value: (product.categorySlugs?.[0] ?? product.category) || "",
67
+ onChange: (value) => update({ categorySlugs: value ? [value] : [] }),
68
68
  disabled: isReadonly,
69
- }) })) : (_jsx(FormField, { name: "category", label: t("formCategory"), type: "text", value: product.category || "", onChange: (value) => update({ category: value }), disabled: isReadonly })), _jsx(FormField, { name: "subcategory", label: t("formSubcategory"), type: "text", value: product.subcategory || "", onChange: (value) => update({ subcategory: value }), disabled: isReadonly, placeholder: "e.g. Smartphones" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: t("formBrand") }), _jsx("div", { className: "flex gap-3 flex-wrap", children: ["single", "unbranded", "mixed"].map((mode) => (_jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "brandMode", value: mode, checked: (product.brandMode ?? "single") === mode, onChange: () => {
69
+ }) })) : (_jsx(FormField, { name: "category", label: t("formCategory"), type: "text", value: (product.categorySlugs?.[0] ?? product.category) || "", onChange: (value) => update({ categorySlugs: value ? [value] : [] }), disabled: isReadonly })), _jsx(FormField, { name: "subcategory", label: t("formSubcategory"), type: "text", value: product.subcategory || "", onChange: (value) => update({ subcategory: value }), disabled: isReadonly, placeholder: "e.g. Smartphones" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: t("formBrand") }), _jsx("div", { className: "flex gap-3 flex-wrap", children: ["single", "unbranded", "mixed"].map((mode) => (_jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "brandMode", value: mode, checked: (product.brandMode ?? "single") === mode, onChange: () => {
70
70
  if (mode === "unbranded") {
71
71
  update({ brandMode: "unbranded", brand: undefined, brands: [] });
72
72
  }
@@ -88,7 +88,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
88
88
  type: "product-image",
89
89
  index: 1,
90
90
  name: product.title || "product",
91
- category: product.category || "uncategorized",
91
+ category: (product.categorySlugs?.[0] ?? product.category) || "uncategorized",
92
92
  store: product.storeName || "store",
93
93
  }), onChange: (url) => update({ mainImage: url }), label: t("formMainImage"), helperText: "Recommended: 800x800px (1:1)" })), isReadonly && product.mainImage && (_jsx(FormField, { name: "mainImage", label: t("formMainImage"), type: "text", value: product.mainImage, onChange: () => { }, disabled: true })), !isReadonly && (_jsx(MediaUploadList, { label: t("formGalleryImages"), value: galleryImages, onChange: (fields) => update({ images: fields.map((f) => f.url) }), onUpload: handleGalleryUpload, accept: "image/*", maxItems: 5, maxSizeMB: 10, helperText: t("formGalleryImagesHelper"), onAbort: onMediaAbort })), !isReadonly && (_jsx(MediaUploadField, { label: t("formVideo"), value: product.video?.url || "", onChange: (url) => update({
94
94
  video: url
@@ -59,7 +59,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
59
59
  isWishlisted
60
60
  ? "bg-rose-500 text-white hover:bg-rose-600"
61
61
  : "bg-white/90 dark:bg-slate-800/90 text-zinc-500 dark:text-zinc-400 hover:text-rose-500 hover:bg-white dark:hover:bg-slate-800",
62
- ].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3 pt-2.5", children: [_jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-semibold text-zinc-900 dark:text-white leading-snug`, children: product.title }), (product.categoryName || product.brand) && (_jsxs(Row, { className: "mt-1 gap-1 flex-wrap", children: [product.categoryName && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[100px]", children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" }) }), product.categoryName] })), product.brand && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[90px]", children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] })), (() => {
62
+ ].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3 pt-2.5", children: [_jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-semibold text-zinc-900 dark:text-white leading-snug`, children: product.title }), ((Array.isArray(product.categoryNames) ? product.categoryNames[0] : product.categoryName) || product.brand) && (_jsxs(Row, { className: "mt-1 gap-1 flex-wrap", children: [(Array.isArray(product.categoryNames) ? product.categoryNames[0] : product.categoryName) && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[100px]", children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" }) }), (Array.isArray(product.categoryNames) ? product.categoryNames[0] : product.categoryName)] })), product.brand && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[90px]", children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] })), (() => {
63
63
  const seller = safeDisplayName(product.storeName, "");
64
64
  return seller ? (_jsxs(Text, { className: "mt-0.5 text-[11px] text-zinc-400 dark:text-zinc-500 truncate", children: ["by ", seller] })) : null;
65
65
  })(), product.rating !== undefined && (_jsxs(Row, { className: "mt-1 gap-1 items-center", children: [_jsx(Span, { className: "text-[11px] text-amber-400", children: "\u2605" }), _jsxs(Span, { className: "text-[11px] text-zinc-500 dark:text-zinc-400", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto pt-2", children: [_jsxs(Row, { className: "items-baseline gap-2", children: [_jsx(Span, { className: "text-base font-bold text-primary dark:text-primary-400", children: formatCurrency(product.price, getDefaultCurrency()) }), product.originalPrice && product.originalPrice > product.price && (_jsx(Span, { className: "text-xs text-zinc-400 line-through dark:text-zinc-500", children: formatCurrency(product.originalPrice, getDefaultCurrency()) }))] }), (() => {
@@ -62,6 +62,10 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
62
62
  canFilter: boolean;
63
63
  canSort: boolean;
64
64
  };
65
+ categorySlugs: {
66
+ canFilter: boolean;
67
+ canSort: boolean;
68
+ };
65
69
  subcategory: {
66
70
  canFilter: boolean;
67
71
  canSort: boolean;
@@ -71,7 +71,12 @@ export class ProductRepository extends BaseRepository {
71
71
  return product;
72
72
  }
73
73
  mapDoc(snap) {
74
- return super.mapDoc(snap);
74
+ const doc = super.mapDoc(snap);
75
+ // Backward compat: normalise legacy `category` string into `categorySlugs[]`.
76
+ if (!doc.categorySlugs || doc.categorySlugs.length === 0) {
77
+ doc.categorySlugs = doc.category ? [doc.category] : [];
78
+ }
79
+ return doc;
75
80
  }
76
81
  async update(id, data) {
77
82
  this.cacheInvalidateForId(id);
@@ -116,7 +121,10 @@ export class ProductRepository extends BaseRepository {
116
121
  return this.findBy(PRODUCT_FIELDS.FEATURED, true);
117
122
  }
118
123
  async findByCategory(category) {
119
- return this.findBy(PRODUCT_FIELDS.CATEGORY, category);
124
+ const snap = await this.getCollection()
125
+ .where(PRODUCT_FIELDS.CATEGORY_SLUGS, "array-contains", category)
126
+ .get();
127
+ return snap.docs.map((d) => this.mapDoc(d));
120
128
  }
121
129
  async findBySlug(slug) {
122
130
  const key = this.cacheKeyBySlug(slug);
@@ -313,7 +321,7 @@ export class ProductRepository extends BaseRepository {
313
321
  baseQuery = baseQuery.where(PRODUCT_FIELDS.STORE_ID, "==", opts.storeId);
314
322
  }
315
323
  if (opts?.categoriesIn && opts.categoriesIn.length > 0) {
316
- baseQuery = baseQuery.where(PRODUCT_FIELDS.CATEGORY, "in", opts.categoriesIn);
324
+ baseQuery = baseQuery.where(PRODUCT_FIELDS.CATEGORY_SLUGS, "array-contains-any", opts.categoriesIn);
317
325
  }
318
326
  return this.sieveQuery(model, ProductRepository.SIEVE_FIELDS, {
319
327
  baseQuery,
@@ -410,6 +418,7 @@ ProductRepository.SIEVE_FIELDS = {
410
418
  title: { canFilter: true, canSort: true },
411
419
  slug: { canFilter: true, canSort: false },
412
420
  category: { canFilter: true, canSort: true },
421
+ categorySlugs: { canFilter: true, canSort: false },
413
422
  subcategory: { canFilter: true, canSort: true },
414
423
  brand: { canFilter: true, canSort: true },
415
424
  condition: { canFilter: true, canSort: false },
@@ -504,6 +513,16 @@ ProductRepository.FILTER_ALIASES = {
504
513
  return "";
505
514
  }
506
515
  },
516
+ /**
517
+ * Backward-compat: `category==slug` → `categorySlugs @= slug`
518
+ * Allows existing consumers using the old single-value filter to keep
519
+ * working after the category → categorySlugs[] migration (Track C).
520
+ */
521
+ category: (value, operator) => {
522
+ if (operator !== "==")
523
+ return "";
524
+ return `${PRODUCT_FIELDS.CATEGORY_SLUGS}@=${value}`;
525
+ },
507
526
  /** Shorthand for the homepage promoted strip. `promoted==true` only. */
508
527
  promoted: (value, operator) => {
509
528
  if (operator !== "==" || value !== "true")
@@ -111,7 +111,13 @@ export interface ProductDocument {
111
111
  seoTitle?: string;
112
112
  seoDescription?: string;
113
113
  seoKeywords?: string[];
114
- category: string;
114
+ /** Primary category FK slugs (1-to-many). Use categorySlugs[0] for display. */
115
+ categorySlugs: string[];
116
+ /** Display-name cache parallel to categorySlugs[]. */
117
+ categoryNames?: string[];
118
+ /** @deprecated Read-only alias — normalised from legacy docs on load. */
119
+ category?: string;
120
+ /** @deprecated Read-only alias. */
115
121
  categoryName?: string;
116
122
  subcategory?: string;
117
123
  brand?: string;
@@ -41,8 +41,12 @@ export interface ProductItem {
41
41
  isPromoted?: boolean;
42
42
  currentBid?: number;
43
43
  availableQuantity?: number;
44
+ /** @deprecated Use categorySlugs[0] */
44
45
  category?: string;
46
+ /** @deprecated Use categoryNames[0] */
45
47
  categoryName?: string;
48
+ categorySlugs?: string[];
49
+ categoryNames?: string[];
46
50
  categorySlug?: string;
47
51
  sellerAvatar?: string;
48
52
  status: ProductStatus;