@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.
- package/dist/_internal/client/features/filters/filter-load-options.d.ts +29 -0
- package/dist/_internal/client/features/filters/filter-load-options.js +98 -0
- package/dist/_internal/server/features/classified/adapters.js +1 -1
- package/dist/_internal/server/features/digital-code/adapters.js +1 -1
- package/dist/_internal/server/features/live/adapters.js +1 -1
- package/dist/_internal/shared/actions/action-registry.js +194 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +1 -0
- package/dist/features/about/components/PublicProfileView.js +2 -2
- package/dist/features/admin/components/AdminAddressEditorView.d.ts +7 -0
- package/dist/features/admin/components/AdminAddressEditorView.js +117 -0
- package/dist/features/admin/components/AdminCategoryEditorView.js +12 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +58 -13
- package/dist/features/admin/components/AdminProductEditorView.d.ts +3 -1
- package/dist/features/admin/components/AdminProductEditorView.js +12 -9
- package/dist/features/admin/components/AdminReturnRequestsView.js +5 -4
- package/dist/features/admin/components/AdminReviewsView.js +5 -4
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +13 -0
- package/dist/features/auctions/components/AuctionBidsTable.js +62 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/AuctionFilters.d.ts +7 -1
- package/dist/features/auctions/components/AuctionFilters.js +3 -2
- package/dist/features/auctions/components/index.d.ts +2 -0
- package/dist/features/auctions/components/index.js +1 -0
- package/dist/features/cart/components/CartView.js +2 -1
- package/dist/features/cart/components/CheckoutView.js +2 -1
- package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/BrandDetailTabs.js +29 -15
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +30 -15
- package/dist/features/classified/components/ClassifiedFilters.d.ts +5 -1
- package/dist/features/classified/components/ClassifiedFilters.js +3 -2
- package/dist/features/digital-codes/components/DigitalCodeFilters.d.ts +5 -1
- package/dist/features/digital-codes/components/DigitalCodeFilters.js +3 -2
- package/dist/features/events/repository/event-entry.repository.d.ts +1 -0
- package/dist/features/events/repository/event-entry.repository.js +5 -0
- package/dist/features/filters/index.d.ts +2 -0
- package/dist/features/filters/index.js +1 -0
- package/dist/features/layout/AppLayoutShell.d.ts +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/live/components/LiveItemFilters.d.ts +5 -1
- package/dist/features/live/components/LiveItemFilters.js +3 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrderFilters.d.ts +7 -1
- package/dist/features/pre-orders/components/PreOrderFilters.js +3 -2
- package/dist/features/products/columns/productTableColumns.js +1 -1
- package/dist/features/products/components/CompareOverlay.d.ts +2 -0
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/ProductFilters.d.ts +7 -1
- package/dist/features/products/components/ProductFilters.js +3 -2
- package/dist/features/products/components/ProductForm.js +6 -6
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/products/repository/products.repository.d.ts +4 -0
- package/dist/features/products/repository/products.repository.js +22 -3
- package/dist/features/products/schemas/firestore.d.ts +7 -1
- package/dist/features/products/types/index.d.ts +4 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +16 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/next/routing/route-map.d.ts +14 -0
- package/dist/next/routing/route-map.js +7 -0
- package/dist/seed/products-auctions-seed-data.js +40 -40
- package/dist/seed/products-preorders-seed-data.js +16 -16
- package/dist/seed/products-prize-draws-seed-data.js +4 -4
- package/dist/seed/products-standard-seed-data.js +202 -202
- package/dist/tailwind-utilities.css +1 -1
- 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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
case "
|
|
22
|
-
|
|
23
|
-
case "
|
|
24
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 `
|
|
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 ?? "
|
|
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
|
|
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
|
|
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",
|
|
@@ -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
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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;
|