@mohasinac/appkit 4.8.1 → 4.9.0
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/features/categories/components/CategoryDetailPageView.js +24 -18
- package/dist/features/categories/components/CategoryGrid.js +5 -1
- package/dist/features/categories/repository/categories.repository.d.ts +8 -0
- package/dist/features/categories/repository/categories.repository.js +21 -0
- package/dist/features/categories/types/index.d.ts +4 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/package.json +1 -1
|
@@ -16,53 +16,59 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
16
16
|
const category = await categoriesRepository
|
|
17
17
|
.getCategoryBySlug(slug)
|
|
18
18
|
.catch(() => undefined);
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
19
|
+
// Roll up descendant categories so a parent category page shows products
|
|
20
|
+
// filed under any of its children too, not just products tagged with the
|
|
21
|
+
// parent's own id — parentIds stores the full ancestor chain, so a single
|
|
22
|
+
// array-contains query already returns the whole subtree (see
|
|
23
|
+
// categoriesRepository.getDescendantIds).
|
|
24
|
+
const descendantIds = category?.id
|
|
25
|
+
? await categoriesRepository.getDescendantIds(category.id).catch(() => [])
|
|
26
|
+
: [];
|
|
27
|
+
const categoriesIn = category?.id ? [category.id, ...descendantIds] : null;
|
|
22
28
|
const [productsResult, auctionsCountResult, preOrdersCountResult, prizeDrawsCountResult, bundlesResult, childCategories, rootSiblingCategories] = await Promise.all([
|
|
23
|
-
|
|
29
|
+
categoriesIn
|
|
24
30
|
? productRepository
|
|
25
31
|
.list({
|
|
26
|
-
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"),
|
|
32
|
+
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "standard")),
|
|
27
33
|
sorts: sortBy("createdAt", "DESC"),
|
|
28
34
|
page: 1,
|
|
29
35
|
pageSize: 24,
|
|
30
|
-
})
|
|
36
|
+
}, { categoriesIn })
|
|
31
37
|
.catch(() => null)
|
|
32
38
|
: Promise.resolve(null),
|
|
33
|
-
|
|
39
|
+
categoriesIn
|
|
34
40
|
? productRepository
|
|
35
41
|
.list({
|
|
36
|
-
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"),
|
|
42
|
+
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "auction")),
|
|
37
43
|
sorts: sortBy("auctionEndDate", "ASC"),
|
|
38
44
|
page: 1,
|
|
39
45
|
pageSize: 1,
|
|
40
|
-
})
|
|
46
|
+
}, { categoriesIn })
|
|
41
47
|
.catch(() => null)
|
|
42
48
|
: Promise.resolve(null),
|
|
43
|
-
|
|
49
|
+
categoriesIn
|
|
44
50
|
? productRepository
|
|
45
51
|
.list({
|
|
46
|
-
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"),
|
|
52
|
+
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "pre-order")),
|
|
47
53
|
sorts: sortBy("createdAt", "DESC"),
|
|
48
54
|
page: 1,
|
|
49
55
|
pageSize: 1,
|
|
50
|
-
})
|
|
56
|
+
}, { categoriesIn })
|
|
51
57
|
.catch(() => null)
|
|
52
58
|
: Promise.resolve(null),
|
|
53
|
-
|
|
59
|
+
categoriesIn
|
|
54
60
|
? productRepository
|
|
55
61
|
.list({
|
|
56
|
-
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"),
|
|
62
|
+
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "prize-draw")),
|
|
57
63
|
sorts: sortBy("createdAt", "DESC"),
|
|
58
64
|
page: 1,
|
|
59
65
|
pageSize: 1,
|
|
60
|
-
})
|
|
66
|
+
}, { categoriesIn })
|
|
61
67
|
.catch(() => null)
|
|
62
68
|
: Promise.resolve(null),
|
|
63
69
|
// SB-UNI-D — bundles fetched from the categories collection. We pull
|
|
64
70
|
// all active bundle rows; the carousel filters by category affinity.
|
|
65
|
-
|
|
71
|
+
categoriesIn
|
|
66
72
|
? categoriesRepository
|
|
67
73
|
.listByType("bundle", { activeOnly: true, limit: 50 })
|
|
68
74
|
.catch(() => [])
|
|
@@ -112,8 +118,8 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
112
118
|
averageRating: s.stats?.averageRating,
|
|
113
119
|
createdAt: s.createdAt,
|
|
114
120
|
}));
|
|
115
|
-
const productCount = productsResult?.total ?? category?.metrics?.productCount ?? 0;
|
|
116
|
-
const auctionCount = auctionsCountResult?.total ?? category?.metrics?.auctionCount ?? 0;
|
|
121
|
+
const productCount = productsResult?.total ?? category?.metrics?.totalProductCount ?? category?.metrics?.productCount ?? 0;
|
|
122
|
+
const auctionCount = auctionsCountResult?.total ?? category?.metrics?.totalAuctionCount ?? category?.metrics?.auctionCount ?? 0;
|
|
117
123
|
const preOrderCount = preOrdersCountResult?.total ?? 0;
|
|
118
124
|
const prizeDrawCount = prizeDrawsCountResult?.total ?? 0;
|
|
119
125
|
const bundleCount = bundlesResult?.length ?? 0;
|
|
@@ -12,7 +12,11 @@ const __O = {
|
|
|
12
12
|
};
|
|
13
13
|
const CLS_FEATURED_DOT = "absolute left-2 top-2 rounded-full bg-warning-surface p-[var(--appkit-space-1)] leading-none";
|
|
14
14
|
export function CategoryCard({ category, href, onClick, className = "", }) {
|
|
15
|
-
|
|
15
|
+
// totalProductCount is the rollup (self + all descendant categories),
|
|
16
|
+
// maintained incrementally by onProductWrite — prefer it over the
|
|
17
|
+
// own-node-only productCount so a parent category's card shows the same
|
|
18
|
+
// "N products" a visitor actually sees once they open the page.
|
|
19
|
+
const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? category.productCount ?? 0;
|
|
16
20
|
const inner = (_jsxs(Stack, { className: "h-full", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-[4/3] w-full ${__O.hidden} flex-shrink-0`, children: [category.display?.coverImage ? (_jsx(MediaImage, { src: category.display.coverImage, alt: category.name, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : category.display?.color ? (_jsx(DynamicBgDiv, { color: category.display.color, className: "h-full w-full opacity-80" })) : null, category.display?.icon && (_jsx(Row, { textSize: "4xl", className: "absolute inset-0", align: "center", justify: "center", children: category.display.icon })), category.isFeatured && (_jsx(Span, { size: "xs", className: CLS_FEATURED_DOT, children: "\u2605" }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3}.5`, children: [_jsx(Text, { color: "inverse", className: `leading-snug text-[var(--appkit-color-text)] dark:`, truncate: 2, size: "sm", weight: "semibold", children: category.name }), category.description && (_jsx(Text, { className: `mt-1 flex-1`, color: "muted", truncate: 2, size: "xs", children: category.description })), _jsxs(Row, { className: "mt-2", align: "center", justify: "between", gap: "sm", children: [_jsxs(Text, { size: "xs", color: "faint", children: [productCount.toLocaleString(), " ", productCount === 1 ? "item" : "items"] }), _jsxs(Span, { layout: "inline-flex", gap: "xs", size: "xs", weight: "medium", border: "default", className: "group-hover:bg-primary group-hover:border-primary group-hover:text-white transition-colors", rounded: "md", padding: "pill-sm-tall", color: "muted", children: ["Browse ", _jsx(ArrowRight, { className: "h-3 w-3" })] })] })] })] }));
|
|
17
21
|
const cardClass = `group relative flex flex-col overflow-hidden rounded-xl border border-neutral-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] shadow-sm transition hover:shadow-md h-full ${className}`;
|
|
18
22
|
if (href) {
|
|
@@ -12,6 +12,14 @@ export declare class CategoriesRepository extends BaseRepository<CategoryDocumen
|
|
|
12
12
|
getCategoriesByTier(tier: number): Promise<CategoryDocument[]>;
|
|
13
13
|
getCategoriesByRootId(rootId: string): Promise<CategoryDocument[]>;
|
|
14
14
|
getChildren(parentId: string): Promise<CategoryDocument[]>;
|
|
15
|
+
/**
|
|
16
|
+
* IDs of every descendant category at any depth (not just direct
|
|
17
|
+
* children) — `parentIds` stores the full ancestor chain, so a single
|
|
18
|
+
* array-contains query on that field already returns the whole subtree
|
|
19
|
+
* with no recursion needed. Used to expand a parent category's product
|
|
20
|
+
* listing/count to include products filed under any of its children.
|
|
21
|
+
*/
|
|
22
|
+
getDescendantIds(categoryId: string): Promise<string[]>;
|
|
15
23
|
getFeaturedCategories(): Promise<CategoryDocument[]>;
|
|
16
24
|
getBrandCategories(limit?: number): Promise<CategoryDocument[]>;
|
|
17
25
|
updateMetrics(categoryId: string, productDelta: number, auctionDelta: number, productId?: string): Promise<void>;
|
|
@@ -161,6 +161,27 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
161
161
|
throw new DatabaseError(`Failed to retrieve children categories: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* IDs of every descendant category at any depth (not just direct
|
|
166
|
+
* children) — `parentIds` stores the full ancestor chain, so a single
|
|
167
|
+
* array-contains query on that field already returns the whole subtree
|
|
168
|
+
* with no recursion needed. Used to expand a parent category's product
|
|
169
|
+
* listing/count to include products filed under any of its children.
|
|
170
|
+
*/
|
|
171
|
+
async getDescendantIds(categoryId) {
|
|
172
|
+
try {
|
|
173
|
+
const snapshot = await this.db
|
|
174
|
+
.collection(this.collection)
|
|
175
|
+
.where("parentIds", "array-contains", categoryId)
|
|
176
|
+
.limit(100)
|
|
177
|
+
.get();
|
|
178
|
+
return snapshot.docs.map((doc) => doc.id);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
void normalizeError(error);
|
|
182
|
+
throw new DatabaseError(`Failed to retrieve descendant categories: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
164
185
|
async getFeaturedCategories() {
|
|
165
186
|
try {
|
|
166
187
|
const snapshot = await this.db
|
|
@@ -14,6 +14,10 @@ export interface CategoryDisplay {
|
|
|
14
14
|
export interface CategoryMetrics {
|
|
15
15
|
productCount: number;
|
|
16
16
|
auctionCount?: number;
|
|
17
|
+
/** Rollup — this category's own productCount plus every descendant category's, maintained incrementally by onProductWrite. */
|
|
18
|
+
totalProductCount?: number;
|
|
19
|
+
/** Rollup — same as totalProductCount but for auctions. */
|
|
20
|
+
totalAuctionCount?: number;
|
|
17
21
|
totalItemCount: number;
|
|
18
22
|
lastUpdated?: string;
|
|
19
23
|
}
|
|
@@ -22,7 +22,7 @@ function CategoryChip({ category }) {
|
|
|
22
22
|
const iconSrc = category.display?.icon;
|
|
23
23
|
const coverImage = category.display?.coverImage;
|
|
24
24
|
const initial = category.name[0]?.toUpperCase() ?? "?";
|
|
25
|
-
const productCount = category.metrics?.productCount ?? 0;
|
|
25
|
+
const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? 0;
|
|
26
26
|
return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: resolveMediaUrl(coverImage), alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: resolveMediaUrl(iconSrc), alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
|
|
27
27
|
}
|
|
28
28
|
const CTA_CLASSES = {
|