@mohasinac/appkit 3.2.1 → 3.2.3

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.
@@ -4,7 +4,10 @@ import { categoriesRepository } from "../../../../repositories";
4
4
  import { CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, CATEGORIES_SITEMAP_LIMIT } from "../../../shared/features/categories/config";
5
5
  /** Full category document by slug — deduped per request via React.cache(). */
6
6
  export const getCategoryForDetail = cache(async (slug) => {
7
- return (await categoriesRepository.getCategoryBySlug(slug).catch(() => undefined)) ?? null;
7
+ const category = (await categoriesRepository.getCategoryBySlug(slug).catch(() => undefined)) ?? null;
8
+ if (category)
9
+ void categoriesRepository.incrementViewCount(category.id);
10
+ return category;
8
11
  });
9
12
  /** Flat list of all categories at tier 1 (roots) — for nav/sitemap. */
10
13
  export const listRootCategories = cache(async () => {
@@ -23,6 +23,7 @@ export interface AdminAnalyticsResult {
23
23
  revenue: number;
24
24
  orders: number;
25
25
  mainImage: string;
26
+ viewCount: number;
26
27
  }>;
27
28
  }
28
29
  export declare function runAdminAnalytics(_input: void, ctx: JobContext): Promise<AdminAnalyticsResult>;
@@ -82,6 +82,7 @@ export async function runAdminAnalytics(_input, ctx) {
82
82
  const topProductsWithImages = topProducts.map((p, i) => ({
83
83
  ...p,
84
84
  mainImage: topProductDocs[i]?.mainImage ?? "",
85
+ viewCount: topProductDocs[i]?.viewCount ?? 0,
85
86
  }));
86
87
  return {
87
88
  summary: {
@@ -424,6 +424,7 @@ export declare const CATEGORY_FIELDS: {
424
424
  readonly BUNDLE_STOCK_STATUS: "bundleStockStatus";
425
425
  readonly BUNDLE_QUERY_RESOLVED_AT: "bundleQueryResolvedAt";
426
426
  readonly DISPLAY: "display";
427
+ readonly VIEW_COUNT: "viewCount";
427
428
  readonly CREATED_AT: "createdAt";
428
429
  readonly UPDATED_AT: "updatedAt";
429
430
  readonly CATEGORY_TYPE_VALUES: {
@@ -465,6 +465,7 @@ export const CATEGORY_FIELDS = {
465
465
  BUNDLE_STOCK_STATUS: "bundleStockStatus",
466
466
  BUNDLE_QUERY_RESOLVED_AT: "bundleQueryResolvedAt",
467
467
  DISPLAY: "display",
468
+ VIEW_COUNT: "viewCount",
468
469
  CREATED_AT: "createdAt",
469
470
  UPDATED_AT: "updatedAt",
470
471
  CATEGORY_TYPE_VALUES: {
@@ -3,6 +3,7 @@ import type { AnalyticsTopProduct } from "../../types";
3
3
  export interface AdminTopProductsTableLabels {
4
4
  title?: string;
5
5
  orders?: string;
6
+ views?: string;
6
7
  view?: string;
7
8
  revenue?: string;
8
9
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Div, Heading, Row, Text } from "../../../../ui";
3
3
  export function AdminTopProductsTable({ products, labels = {}, formatRevenue = (n) => String(n), renderProductLink, className = "", }) {
4
- return (_jsxs(Div, { className: `border border-neutral-200 dark:border-neutral-800 sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-neutral-100 dark:border-neutral-800", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-neutral-500 dark:text-neutral-400", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }), _jsxs(Div, { className: "text-right ml-4", children: [_jsx(Text, { weight: "semibold", children: formatRevenue(product.revenue) }), renderProductLink?.(product)] })] }, product.productId))) })] }));
4
+ return (_jsxs(Div, { className: `border border-neutral-200 dark:border-neutral-800 sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-neutral-100 dark:border-neutral-800", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-neutral-500 dark:text-neutral-400", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }), product.viewCount != null && (_jsx(Div, { className: "text-right ml-4 hidden sm:block", children: _jsxs(Text, { size: "sm", color: "muted", children: [product.viewCount, " ", labels.views ?? "views"] }) })), _jsxs(Div, { className: "text-right ml-4", children: [_jsx(Text, { weight: "semibold", children: formatRevenue(product.revenue) }), renderProductLink?.(product)] })] }, product.productId))) })] }));
5
5
  }
@@ -373,6 +373,7 @@ export interface SiteSettingsDocument {
373
373
  auction?: boolean;
374
374
  "pre-order"?: boolean;
375
375
  "prize-draw"?: boolean;
376
+ bundle?: boolean;
376
377
  classified?: boolean;
377
378
  "digital-code"?: boolean;
378
379
  live?: boolean;
@@ -213,6 +213,7 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
213
213
  auction: true,
214
214
  "pre-order": true,
215
215
  "prize-draw": true,
216
+ bundle: true,
216
217
  classified: true,
217
218
  "digital-code": true,
218
219
  live: true,
@@ -44,6 +44,7 @@ export interface AnalyticsTopProduct {
44
44
  title: string;
45
45
  revenue: number;
46
46
  orders: number;
47
+ viewCount?: number;
47
48
  mainImage?: string;
48
49
  }
49
50
  export interface AnalyticsSummary {
@@ -55,6 +55,7 @@ export declare class CategoriesRepository extends BaseRepository<CategoryDocumen
55
55
  * SB-UNI-B — derive the canonical `sublisting-{slug}` ID from a
56
56
  * human-entered category name.
57
57
  */
58
+ incrementViewCount(categoryId: string): Promise<void>;
58
59
  generateSublistingId(name: string): string;
59
60
  }
60
61
  export declare const categoriesRepository: CategoriesRepository;
@@ -453,6 +453,17 @@ export class CategoriesRepository extends BaseRepository {
453
453
  * SB-UNI-B — derive the canonical `sublisting-{slug}` ID from a
454
454
  * human-entered category name.
455
455
  */
456
+ async incrementViewCount(categoryId) {
457
+ try {
458
+ await this.db
459
+ .collection(this.collection)
460
+ .doc(categoryId)
461
+ .update({ [CATEGORY_FIELDS.VIEW_COUNT]: increment(1) });
462
+ }
463
+ catch {
464
+ // View analytics must not break category read path.
465
+ }
466
+ }
456
467
  generateSublistingId(name) {
457
468
  const base = name
458
469
  .toLowerCase()
@@ -102,6 +102,7 @@ export interface CategoryDocument extends BaseDocument {
102
102
  */
103
103
  subtreeSize: number;
104
104
  metrics: CategoryDocumentMetrics;
105
+ viewCount?: number;
105
106
  isFeatured: boolean;
106
107
  featuredPriority?: number;
107
108
  /** @deprecated Use categoryType === "brand". Will be removed once seed + admin form fully migrate. */
@@ -240,6 +241,7 @@ export declare const CATEGORY_FIELDS: {
240
241
  readonly FEATURED_PRIORITY: "featuredPriority";
241
242
  readonly SEO: "seo";
242
243
  readonly DISPLAY: "display";
244
+ readonly VIEW_COUNT: "viewCount";
243
245
  readonly IS_ACTIVE: "isActive";
244
246
  readonly IS_SEARCHABLE: "isSearchable";
245
247
  readonly CATEGORY_TYPE: "categoryType";
@@ -176,6 +176,7 @@ export const CATEGORY_FIELDS = {
176
176
  FEATURED_PRIORITY: "featuredPriority",
177
177
  SEO: "seo",
178
178
  DISPLAY: "display",
179
+ VIEW_COUNT: "viewCount",
179
180
  IS_ACTIVE: "isActive",
180
181
  IS_SEARCHABLE: "isSearchable",
181
182
  CATEGORY_TYPE: "categoryType",
@@ -390,6 +390,7 @@ export const siteSettingsSeedData = {
390
390
  auction: false,
391
391
  "pre-order": false,
392
392
  "prize-draw": false,
393
+ bundle: false,
393
394
  classified: false,
394
395
  "digital-code": false,
395
396
  live: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"