@mohasinac/appkit 2.4.3 → 2.4.5

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 (49) hide show
  1. package/dist/constants/api-endpoints.d.ts +6 -0
  2. package/dist/constants/api-endpoints.js +2 -0
  3. package/dist/features/admin/components/AdminSublistingCategoriesView.d.ts +1 -0
  4. package/dist/features/admin/components/AdminSublistingCategoriesView.js +56 -0
  5. package/dist/features/admin/components/AdminSublistingCategoryEditorView.d.ts +7 -0
  6. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +83 -0
  7. package/dist/features/admin/components/index.d.ts +3 -0
  8. package/dist/features/admin/components/index.js +2 -0
  9. package/dist/features/auctions/components/AuctionDetailPageView.js +9 -1
  10. package/dist/features/auctions/schemas/index.d.ts +2 -2
  11. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
  12. package/dist/features/products/components/CustomFieldsEditor.d.ts +8 -0
  13. package/dist/features/products/components/CustomFieldsEditor.js +33 -0
  14. package/dist/features/products/components/CustomSectionTabContent.d.ts +4 -0
  15. package/dist/features/products/components/CustomSectionTabContent.js +13 -0
  16. package/dist/features/products/components/CustomSectionsEditor.d.ts +6 -0
  17. package/dist/features/products/components/CustomSectionsEditor.js +27 -0
  18. package/dist/features/products/components/ProductDetailPageView.js +9 -1
  19. package/dist/features/products/components/ProductForm.js +4 -2
  20. package/dist/features/products/components/ProductTabsShell.d.ts +8 -1
  21. package/dist/features/products/components/ProductTabsShell.js +19 -9
  22. package/dist/features/products/components/SublistingCarouselSection.d.ts +6 -0
  23. package/dist/features/products/components/SublistingCarouselSection.js +53 -0
  24. package/dist/features/products/components/SublistingCategorySelect.d.ts +7 -0
  25. package/dist/features/products/components/SublistingCategorySelect.js +40 -0
  26. package/dist/features/products/components/index.d.ts +6 -1
  27. package/dist/features/products/components/index.js +3 -0
  28. package/dist/features/products/repository/sublisting-categories.repository.d.ts +16 -0
  29. package/dist/features/products/repository/sublisting-categories.repository.js +126 -0
  30. package/dist/features/products/schemas/firestore.d.ts +20 -2
  31. package/dist/features/products/schemas/firestore.js +8 -0
  32. package/dist/features/products/schemas/index.d.ts +147 -56
  33. package/dist/features/products/schemas/index.js +24 -0
  34. package/dist/features/products/schemas/sublisting-categories.d.ts +45 -0
  35. package/dist/features/products/schemas/sublisting-categories.js +16 -0
  36. package/dist/features/products/types/index.d.ts +5 -0
  37. package/dist/features/search/schemas/index.d.ts +2 -2
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.js +8 -1
  40. package/dist/next/routing/route-map.d.ts +6 -0
  41. package/dist/next/routing/route-map.js +3 -0
  42. package/dist/repositories/index.d.ts +2 -0
  43. package/dist/repositories/index.js +1 -0
  44. package/dist/seed/sublisting-categories-seed-data.d.ts +5 -5
  45. package/dist/seed/sublisting-categories-seed-data.js +81 -237
  46. package/dist/styles.css +1 -0
  47. package/dist/tailwind-input.css +4 -0
  48. package/dist/tailwind-utilities.css +1 -0
  49. package/package.json +5 -4
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ sublistingCategoryId: string;
3
+ currentListingId: string;
4
+ }
5
+ export declare function SublistingCarouselSection({ sublistingCategoryId, currentListingId }: Props): import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+ import { ROUTES } from "../../../next";
6
+ import { Row, Text } from "../../../ui";
7
+ import { formatCurrency } from "../../../utils/number.formatter";
8
+ function getHref(listing) {
9
+ const slug = listing.slug ?? listing.id;
10
+ if (listing.isAuction)
11
+ return String(ROUTES.PUBLIC.AUCTION_DETAIL(slug));
12
+ if (listing.isPreOrder)
13
+ return String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(slug));
14
+ return String(ROUTES.PUBLIC.PRODUCT_DETAIL(slug));
15
+ }
16
+ function ListingThumb({ listing, isCurrent, }) {
17
+ const image = listing.images?.[0] ?? listing.mainImage ?? "";
18
+ const href = getHref(listing);
19
+ const price = formatCurrency(listing.price, listing.currency ?? "INR");
20
+ return (_jsxs(Link, { href: href, "aria-label": listing.title, className: "flex flex-col items-center gap-1.5 flex-shrink-0 w-16 group", children: [_jsx("div", { className: `w-14 h-14 rounded-full overflow-hidden border-2 transition-all ${isCurrent
21
+ ? "border-[var(--appkit-color-primary,#6366f1)] ring-2 ring-[var(--appkit-color-primary,#6366f1)]/30"
22
+ : "border-zinc-200 dark:border-zinc-700 group-hover:border-[var(--appkit-color-primary,#6366f1)]"}`, children: image ? (
23
+ // eslint-disable-next-line @next/next/no-img-element
24
+ _jsx("img", { src: image, alt: listing.title, className: "w-full h-full object-cover", loading: "lazy" })) : (_jsx("div", { className: "w-full h-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-zinc-400 text-xs", children: "\u25EF" })) }), _jsx(Text, { className: "text-[10px] text-center text-zinc-600 dark:text-zinc-400 leading-tight line-clamp-2 w-full", children: listing.title }), _jsx(Text, { className: "text-[10px] font-semibold text-zinc-800 dark:text-zinc-200", children: price })] }));
25
+ }
26
+ export function SublistingCarouselSection({ sublistingCategoryId, currentListingId }) {
27
+ const [open, setOpen] = useState(false);
28
+ const [category, setCategory] = useState(null);
29
+ const [listings, setListings] = useState([]);
30
+ const [loading, setLoading] = useState(false);
31
+ useEffect(() => {
32
+ if (!sublistingCategoryId)
33
+ return;
34
+ setLoading(true);
35
+ fetch(`/api/sublisting-categories/${encodeURIComponent(sublistingCategoryId)}`)
36
+ .then((r) => r.json())
37
+ .then((res) => {
38
+ setCategory(res.data?.category ?? null);
39
+ setListings(res.data?.listings ?? []);
40
+ })
41
+ .catch(() => { })
42
+ .finally(() => setLoading(false));
43
+ }, [sublistingCategoryId]);
44
+ if (!sublistingCategoryId || loading || (!category && !loading))
45
+ return null;
46
+ if (listings.length <= 1)
47
+ return null;
48
+ const others = listings.filter((l) => l.id !== currentListingId);
49
+ if (others.length === 0)
50
+ return null;
51
+ const label = category?.name ?? "More listings like this";
52
+ return (_jsxs("div", { className: "rounded-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50/60 dark:bg-zinc-800/40 overflow-hidden", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "w-full flex items-center justify-between px-4 py-3 text-left hover:bg-zinc-100/70 dark:hover:bg-zinc-800/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx("span", { className: "text-xs text-zinc-400 dark:text-zinc-500 mr-1", children: open ? "▼" : "▶" }), _jsxs(Text, { className: "text-sm font-medium text-zinc-800 dark:text-zinc-200", children: ["More listings like this:", " ", _jsx("span", { className: "text-[var(--appkit-color-primary,#6366f1)]", children: label })] }), _jsx("span", { className: "ml-1 rounded-full bg-zinc-200 dark:bg-zinc-700 px-2 py-0.5 text-xs text-zinc-600 dark:text-zinc-400", children: listings.length })] }), category && (_jsx(Link, { href: String(ROUTES.PUBLIC.SUBLISTING_CATEGORY(category.slug)), onClick: (e) => e.stopPropagation(), className: "text-xs text-[var(--appkit-color-primary,#6366f1)] hover:underline ml-3 flex-shrink-0", children: "View all \u2192" }))] }), open && (_jsx("div", { className: "px-4 pb-4 pt-1 overflow-x-auto", children: _jsx("div", { className: "flex gap-3 min-w-0", children: listings.map((listing) => (_jsx(ListingThumb, { listing: listing, isCurrent: listing.id === currentListingId }, listing.id))) }) }))] }));
53
+ }
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ value: string;
3
+ onChange: (id: string) => void;
4
+ disabled?: boolean;
5
+ }
6
+ export declare function SublistingCategorySelect({ value, onChange, disabled }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { Select, Stack, Text } from "../../../ui";
5
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
6
+ import { apiClient } from "../../../http";
7
+ export function SublistingCategorySelect({ value, onChange, disabled }) {
8
+ const [options, setOptions] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ useEffect(() => {
11
+ let cancelled = false;
12
+ apiClient
13
+ .get(`${ADMIN_ENDPOINTS.SUBLISTING_CATEGORIES}?pageSize=200&sorts=name`)
14
+ .then((res) => {
15
+ if (cancelled)
16
+ return;
17
+ const items = res?.data?.items ?? [];
18
+ const opts = [
19
+ { value: "", label: "— None —" },
20
+ ...items.map((item) => ({
21
+ value: String(item.id ?? ""),
22
+ label: item.itemCode
23
+ ? `${item.name} (${item.itemCode})`
24
+ : String(item.name ?? ""),
25
+ })),
26
+ ];
27
+ setOptions(opts);
28
+ })
29
+ .catch(() => {
30
+ if (!cancelled)
31
+ setOptions([{ value: "", label: "— None —" }]);
32
+ })
33
+ .finally(() => {
34
+ if (!cancelled)
35
+ setLoading(false);
36
+ });
37
+ return () => { cancelled = true; };
38
+ }, []);
39
+ return (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Sub-listing category" }), _jsx(Select, { options: loading ? [{ value: "", label: "Loading…" }] : options, value: value, onChange: (e) => onChange(e.target.value), disabled: disabled || loading, "aria-label": "Sub-listing category" }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: "Groups this listing with others for the same collectible (e.g. \u201CBase Set Charizard 108/120\u201D)." })] }));
40
+ }
@@ -37,5 +37,10 @@ export type { RelatedProductsProps } from "./RelatedProducts";
37
37
  export { ProductGalleryClient } from "./ProductGalleryClient";
38
38
  export type { ProductGalleryClientProps } from "./ProductGalleryClient";
39
39
  export { ProductTabsShell } from "./ProductTabsShell";
40
- export type { ProductTabsShellProps } from "./ProductTabsShell";
40
+ export type { ProductTabsShellProps, CustomTabDef } from "./ProductTabsShell";
41
+ export { CustomFieldsEditor } from "./CustomFieldsEditor";
42
+ export type { CustomFieldsEditorProps } from "./CustomFieldsEditor";
43
+ export { CustomSectionsEditor } from "./CustomSectionsEditor";
44
+ export type { CustomSectionsEditorProps } from "./CustomSectionsEditor";
45
+ export { CustomSectionTabContent } from "./CustomSectionTabContent";
41
46
  export { RelatedProductsCarousel } from "./RelatedProductsCarousel";
@@ -19,4 +19,7 @@ export { MakeOfferButton } from "./MakeOfferButton";
19
19
  export { RelatedProducts } from "./RelatedProducts";
20
20
  export { ProductGalleryClient } from "./ProductGalleryClient";
21
21
  export { ProductTabsShell } from "./ProductTabsShell";
22
+ export { CustomFieldsEditor } from "./CustomFieldsEditor";
23
+ export { CustomSectionsEditor } from "./CustomSectionsEditor";
24
+ export { CustomSectionTabContent } from "./CustomSectionTabContent";
22
25
  export { RelatedProductsCarousel } from "./RelatedProductsCarousel";
@@ -0,0 +1,16 @@
1
+ import { BaseRepository, type FirebaseSieveFields, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
2
+ import { type SublistingCategoryCreateInput, type SublistingCategoryDocument, type SublistingCategoryUpdateInput } from "../schemas/sublisting-categories";
3
+ export declare class SublistingCategoriesRepository extends BaseRepository<SublistingCategoryDocument> {
4
+ static readonly SIEVE_FIELDS: FirebaseSieveFields;
5
+ constructor();
6
+ generateId(name: string): string;
7
+ list(model: SieveModel): Promise<FirebaseSieveResult<SublistingCategoryDocument>>;
8
+ findBySlug(slug: string): Promise<SublistingCategoryDocument | null>;
9
+ create(input: SublistingCategoryCreateInput): Promise<SublistingCategoryDocument>;
10
+ update(id: string, input: SublistingCategoryUpdateInput): Promise<SublistingCategoryDocument>;
11
+ delete(id: string): Promise<void>;
12
+ /** Returns products/auctions/pre-orders with matching sublistingCategoryId, ordered by price asc. */
13
+ getListingsByCategoryId(categoryId: string, limit?: number): Promise<Record<string, unknown>[]>;
14
+ incrementProductCount(id: string, delta: number): Promise<void>;
15
+ }
16
+ export declare const sublistingCategoriesRepository: SublistingCategoriesRepository;
@@ -0,0 +1,126 @@
1
+ import { DatabaseError } from "../../../errors";
2
+ import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
3
+ import { SUBLISTING_CATEGORIES_COLLECTION, SUBLISTING_CATEGORY_PREFIX, } from "../schemas/sublisting-categories";
4
+ import { PRODUCT_COLLECTION } from "../schemas/firestore";
5
+ function slugify(name) {
6
+ return name
7
+ .toLowerCase()
8
+ .trim()
9
+ .replace(/[^a-z0-9]+/g, "-")
10
+ .replace(/^-+|-+$/g, "");
11
+ }
12
+ export class SublistingCategoriesRepository extends BaseRepository {
13
+ constructor() {
14
+ super(SUBLISTING_CATEGORIES_COLLECTION);
15
+ }
16
+ generateId(name) {
17
+ const base = slugify(name);
18
+ return base.startsWith(SUBLISTING_CATEGORY_PREFIX)
19
+ ? base
20
+ : `${SUBLISTING_CATEGORY_PREFIX}${base}`;
21
+ }
22
+ async list(model) {
23
+ return this.sieveQuery(model, SublistingCategoriesRepository.SIEVE_FIELDS);
24
+ }
25
+ async findBySlug(slug) {
26
+ try {
27
+ const snapshot = await this.db
28
+ .collection(this.collection)
29
+ .where("slug", "==", slug)
30
+ .limit(1)
31
+ .get();
32
+ if (snapshot.empty)
33
+ return null;
34
+ return this.mapDoc(snapshot.docs[0]);
35
+ }
36
+ catch (error) {
37
+ throw new DatabaseError(`Failed to find sublisting category by slug: ${error instanceof Error ? error.message : "Unknown error"}`);
38
+ }
39
+ }
40
+ async create(input) {
41
+ try {
42
+ const id = this.generateId(input.name);
43
+ const now = new Date();
44
+ const doc = {
45
+ ...input,
46
+ slug: id,
47
+ productCount: 0,
48
+ createdAt: now,
49
+ updatedAt: now,
50
+ };
51
+ await this.db
52
+ .collection(this.collection)
53
+ .doc(id)
54
+ .set(prepareForFirestore(doc));
55
+ return this.findByIdOrFail(id);
56
+ }
57
+ catch (error) {
58
+ throw new DatabaseError(`Failed to create sublisting category: ${error instanceof Error ? error.message : "Unknown error"}`);
59
+ }
60
+ }
61
+ async update(id, input) {
62
+ try {
63
+ await this.db
64
+ .collection(this.collection)
65
+ .doc(id)
66
+ .update({ ...input, updatedAt: new Date() });
67
+ return this.findByIdOrFail(id);
68
+ }
69
+ catch (error) {
70
+ throw new DatabaseError(`Failed to update sublisting category: ${error instanceof Error ? error.message : "Unknown error"}`);
71
+ }
72
+ }
73
+ async delete(id) {
74
+ try {
75
+ const batch = this.db.batch();
76
+ // Unlink all member products before deleting the category
77
+ const membersSnap = await this.db
78
+ .collection(PRODUCT_COLLECTION)
79
+ .where("sublistingCategoryId", "==", id)
80
+ .get();
81
+ for (const doc of membersSnap.docs) {
82
+ batch.update(doc.ref, { sublistingCategoryId: null, updatedAt: new Date() });
83
+ }
84
+ batch.delete(this.db.collection(this.collection).doc(id));
85
+ await batch.commit();
86
+ }
87
+ catch (error) {
88
+ throw new DatabaseError(`Failed to delete sublisting category: ${error instanceof Error ? error.message : "Unknown error"}`);
89
+ }
90
+ }
91
+ /** Returns products/auctions/pre-orders with matching sublistingCategoryId, ordered by price asc. */
92
+ async getListingsByCategoryId(categoryId, limit = 20) {
93
+ try {
94
+ const snap = await this.db
95
+ .collection(PRODUCT_COLLECTION)
96
+ .where("sublistingCategoryId", "==", categoryId)
97
+ .where("status", "==", "published")
98
+ .orderBy("price", "asc")
99
+ .limit(limit)
100
+ .get();
101
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
102
+ }
103
+ catch (error) {
104
+ throw new DatabaseError(`Failed to get listings for category ${categoryId}: ${error instanceof Error ? error.message : "Unknown error"}`);
105
+ }
106
+ }
107
+ async incrementProductCount(id, delta) {
108
+ try {
109
+ const { increment } = await import("../../../contracts/field-ops");
110
+ await this.db
111
+ .collection(this.collection)
112
+ .doc(id)
113
+ .update({ productCount: increment(delta), updatedAt: new Date() });
114
+ }
115
+ catch {
116
+ // Fire-and-forget — count drift is acceptable; a nightly job can reconcile
117
+ }
118
+ }
119
+ }
120
+ SublistingCategoriesRepository.SIEVE_FIELDS = {
121
+ name: { canFilter: true, canSort: true },
122
+ slug: { canFilter: true, canSort: false },
123
+ productCount: { canFilter: false, canSort: true },
124
+ createdAt: { canFilter: false, canSort: true },
125
+ };
126
+ export const sublistingCategoriesRepository = new SublistingCategoriesRepository();
@@ -15,6 +15,21 @@ export interface ProductSpecification {
15
15
  value: string;
16
16
  unit?: string;
17
17
  }
18
+ export type CustomFieldType = "text" | "number" | "boolean" | "url";
19
+ export interface CustomField {
20
+ key: string;
21
+ type: CustomFieldType;
22
+ value: string;
23
+ unit?: string;
24
+ }
25
+ export declare const MAX_CUSTOM_FIELDS = 50;
26
+ export declare const MAX_CUSTOM_SECTIONS = 3;
27
+ export interface CustomSection {
28
+ id: string;
29
+ title: string;
30
+ text?: string;
31
+ fields?: CustomField[];
32
+ }
18
33
  export interface ProductDocument {
19
34
  id: string;
20
35
  title: string;
@@ -84,6 +99,9 @@ export interface ProductDocument {
84
99
  howToUse?: string[];
85
100
  allowOffers?: boolean;
86
101
  minOfferPercent?: number;
102
+ customFields?: CustomField[];
103
+ customSections?: CustomSection[];
104
+ sublistingCategoryId?: string;
87
105
  createdAt: Date;
88
106
  updatedAt: Date;
89
107
  }
@@ -99,8 +117,8 @@ export declare const ProductStatusValues: {
99
117
  export declare const PRODUCT_COLLECTION: "products";
100
118
  export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "featured", "isAuction", "isPreOrder", "isPromoted", "isOnSale", "isSold", "createdAt"];
101
119
  export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
102
- export declare const PRODUCT_PUBLIC_FIELDS: readonly ["id", "title", "description", "category", "subcategory", "brand", "price", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "isAuction", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "isPreOrder", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "createdAt"];
103
- export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "isPreOrder", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "seoTitle", "seoDescription", "seoKeywords"];
120
+ export declare const PRODUCT_PUBLIC_FIELDS: readonly ["id", "title", "description", "category", "subcategory", "brand", "price", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "isAuction", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "isPreOrder", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "createdAt"];
121
+ export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "isPreOrder", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId"];
104
122
  export type ProductCreateInput = Omit<ProductDocument, "id" | "createdAt" | "updatedAt" | "availableQuantity" | "bidCount" | "currentBid" | "auctionOriginalEndDate">;
105
123
  export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_UPDATABLE_FIELDS)[number]>>;
106
124
  export type ProductAdminUpdateInput = Partial<Omit<ProductDocument, "id" | "createdAt">>;
@@ -2,6 +2,8 @@
2
2
  * Products Firestore Document Types & Constants
3
3
  */
4
4
  import { generateProductId, generateAuctionId, generatePreOrderId, } from "../../../utils/id-generators";
5
+ export const MAX_CUSTOM_FIELDS = 50;
6
+ export const MAX_CUSTOM_SECTIONS = 3;
5
7
  /** Runtime-accessible product status values — use instead of bare string literals. */
6
8
  export const ProductStatusValues = {
7
9
  DRAFT: "draft",
@@ -94,6 +96,9 @@ export const PRODUCT_PUBLIC_FIELDS = [
94
96
  "seoDescription",
95
97
  "seoKeywords",
96
98
  "viewCount",
99
+ "customFields",
100
+ "customSections",
101
+ "sublistingCategoryId",
97
102
  "createdAt",
98
103
  ];
99
104
  export const PRODUCT_UPDATABLE_FIELDS = [
@@ -133,6 +138,9 @@ export const PRODUCT_UPDATABLE_FIELDS = [
133
138
  "seoTitle",
134
139
  "seoDescription",
135
140
  "seoKeywords",
141
+ "customFields",
142
+ "customSections",
143
+ "sublistingCategoryId",
136
144
  ];
137
145
  export const productQueryHelpers = {
138
146
  byStore: (storeId) => ["storeId", "==", storeId],
@@ -174,6 +174,63 @@ export declare const productItemSchema: z.ZodObject<{
174
174
  pickupAddressId: z.ZodOptional<z.ZodString>;
175
175
  insurance: z.ZodOptional<z.ZodBoolean>;
176
176
  insuranceCost: z.ZodOptional<z.ZodNumber>;
177
+ customFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
178
+ key: z.ZodString;
179
+ type: z.ZodEnum<["text", "number", "boolean", "url"]>;
180
+ value: z.ZodString;
181
+ unit: z.ZodOptional<z.ZodString>;
182
+ }, "strip", z.ZodTypeAny, {
183
+ value: string;
184
+ type: "number" | "boolean" | "text" | "url";
185
+ key: string;
186
+ unit?: string | undefined;
187
+ }, {
188
+ value: string;
189
+ type: "number" | "boolean" | "text" | "url";
190
+ key: string;
191
+ unit?: string | undefined;
192
+ }>, "many">>;
193
+ customSections: z.ZodOptional<z.ZodArray<z.ZodObject<{
194
+ id: z.ZodString;
195
+ title: z.ZodString;
196
+ text: z.ZodOptional<z.ZodString>;
197
+ fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
198
+ key: z.ZodString;
199
+ type: z.ZodEnum<["text", "number", "boolean", "url"]>;
200
+ value: z.ZodString;
201
+ unit: z.ZodOptional<z.ZodString>;
202
+ }, "strip", z.ZodTypeAny, {
203
+ value: string;
204
+ type: "number" | "boolean" | "text" | "url";
205
+ key: string;
206
+ unit?: string | undefined;
207
+ }, {
208
+ value: string;
209
+ type: "number" | "boolean" | "text" | "url";
210
+ key: string;
211
+ unit?: string | undefined;
212
+ }>, "many">>;
213
+ }, "strip", z.ZodTypeAny, {
214
+ id: string;
215
+ title: string;
216
+ text?: string | undefined;
217
+ fields?: {
218
+ value: string;
219
+ type: "number" | "boolean" | "text" | "url";
220
+ key: string;
221
+ unit?: string | undefined;
222
+ }[] | undefined;
223
+ }, {
224
+ id: string;
225
+ title: string;
226
+ text?: string | undefined;
227
+ fields?: {
228
+ value: string;
229
+ type: "number" | "boolean" | "text" | "url";
230
+ key: string;
231
+ unit?: string | undefined;
232
+ }[] | undefined;
233
+ }>, "many">>;
177
234
  }, "strip", z.ZodTypeAny, {
178
235
  id: string;
179
236
  title: string;
@@ -209,24 +266,13 @@ export declare const productItemSchema: z.ZodObject<{
209
266
  features?: string[] | undefined;
210
267
  featured?: boolean | undefined;
211
268
  isPromoted?: boolean | undefined;
212
- originalPrice?: number | undefined;
213
- mainImage?: string | undefined;
214
- currentBid?: number | undefined;
215
- availableQuantity?: number | undefined;
216
- categorySlug?: string | undefined;
217
- sellerAvatar?: string | undefined;
218
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
219
- listingType?: "fixed" | "auction" | "standard" | "pre-order" | undefined;
220
269
  isAuction?: boolean | undefined;
221
270
  isPreOrder?: boolean | undefined;
222
- inStock?: boolean | undefined;
223
- stockCount?: number | undefined;
224
- reviewCount?: number | undefined;
225
- attributes?: Record<string, string> | undefined;
226
- publishedAt?: string | undefined;
227
- stockQuantity?: number | undefined;
228
271
  subcategory?: string | undefined;
229
272
  brand?: string | undefined;
273
+ stockQuantity?: number | undefined;
274
+ availableQuantity?: number | undefined;
275
+ mainImage?: string | undefined;
230
276
  specifications?: {
231
277
  value: string;
232
278
  name: string;
@@ -234,17 +280,14 @@ export declare const productItemSchema: z.ZodObject<{
234
280
  }[] | undefined;
235
281
  shippingInfo?: string | undefined;
236
282
  returnPolicy?: string | undefined;
237
- ingredients?: string[] | undefined;
238
- howToUse?: string[] | undefined;
239
- allowOffers?: boolean | undefined;
240
- minOfferPercent?: number | undefined;
241
- bulkDiscounts?: {
242
- quantity: number;
243
- discountPercent: number;
244
- }[] | undefined;
283
+ condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
284
+ insurance?: boolean | undefined;
285
+ insuranceCost?: number | undefined;
286
+ shippingPaidBy?: "seller" | "buyer" | undefined;
287
+ auctionEndDate?: string | Date | undefined;
245
288
  startingBid?: number | undefined;
289
+ currentBid?: number | undefined;
246
290
  bidCount?: number | undefined;
247
- auctionEndDate?: string | Date | undefined;
248
291
  reservePrice?: number | undefined;
249
292
  buyNowPrice?: number | undefined;
250
293
  minBidIncrement?: number | undefined;
@@ -258,12 +301,43 @@ export declare const productItemSchema: z.ZodObject<{
258
301
  preOrderCurrentCount?: number | undefined;
259
302
  preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship" | undefined;
260
303
  preOrderCancellable?: boolean | undefined;
304
+ pickupAddressId?: string | undefined;
261
305
  viewCount?: number | undefined;
262
306
  avgRating?: number | undefined;
263
- shippingPaidBy?: "seller" | "buyer" | undefined;
264
- pickupAddressId?: string | undefined;
265
- insurance?: boolean | undefined;
266
- insuranceCost?: number | undefined;
307
+ reviewCount?: number | undefined;
308
+ bulkDiscounts?: {
309
+ quantity: number;
310
+ discountPercent: number;
311
+ }[] | undefined;
312
+ ingredients?: string[] | undefined;
313
+ howToUse?: string[] | undefined;
314
+ allowOffers?: boolean | undefined;
315
+ minOfferPercent?: number | undefined;
316
+ customFields?: {
317
+ value: string;
318
+ type: "number" | "boolean" | "text" | "url";
319
+ key: string;
320
+ unit?: string | undefined;
321
+ }[] | undefined;
322
+ customSections?: {
323
+ id: string;
324
+ title: string;
325
+ text?: string | undefined;
326
+ fields?: {
327
+ value: string;
328
+ type: "number" | "boolean" | "text" | "url";
329
+ key: string;
330
+ unit?: string | undefined;
331
+ }[] | undefined;
332
+ }[] | undefined;
333
+ originalPrice?: number | undefined;
334
+ categorySlug?: string | undefined;
335
+ sellerAvatar?: string | undefined;
336
+ listingType?: "fixed" | "auction" | "standard" | "pre-order" | undefined;
337
+ inStock?: boolean | undefined;
338
+ stockCount?: number | undefined;
339
+ attributes?: Record<string, string> | undefined;
340
+ publishedAt?: string | undefined;
267
341
  }, {
268
342
  id: string;
269
343
  title: string;
@@ -299,24 +373,13 @@ export declare const productItemSchema: z.ZodObject<{
299
373
  features?: string[] | undefined;
300
374
  featured?: boolean | undefined;
301
375
  isPromoted?: boolean | undefined;
302
- originalPrice?: number | undefined;
303
- mainImage?: string | undefined;
304
- currentBid?: number | undefined;
305
- availableQuantity?: number | undefined;
306
- categorySlug?: string | undefined;
307
- sellerAvatar?: string | undefined;
308
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
309
- listingType?: "fixed" | "auction" | "standard" | "pre-order" | undefined;
310
376
  isAuction?: boolean | undefined;
311
377
  isPreOrder?: boolean | undefined;
312
- inStock?: boolean | undefined;
313
- stockCount?: number | undefined;
314
- reviewCount?: number | undefined;
315
- attributes?: Record<string, string> | undefined;
316
- publishedAt?: string | undefined;
317
- stockQuantity?: number | undefined;
318
378
  subcategory?: string | undefined;
319
379
  brand?: string | undefined;
380
+ stockQuantity?: number | undefined;
381
+ availableQuantity?: number | undefined;
382
+ mainImage?: string | undefined;
320
383
  specifications?: {
321
384
  value: string;
322
385
  name: string;
@@ -324,17 +387,14 @@ export declare const productItemSchema: z.ZodObject<{
324
387
  }[] | undefined;
325
388
  shippingInfo?: string | undefined;
326
389
  returnPolicy?: string | undefined;
327
- ingredients?: string[] | undefined;
328
- howToUse?: string[] | undefined;
329
- allowOffers?: boolean | undefined;
330
- minOfferPercent?: number | undefined;
331
- bulkDiscounts?: {
332
- quantity: number;
333
- discountPercent: number;
334
- }[] | undefined;
390
+ condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
391
+ insurance?: boolean | undefined;
392
+ insuranceCost?: number | undefined;
393
+ shippingPaidBy?: "seller" | "buyer" | undefined;
394
+ auctionEndDate?: string | Date | undefined;
335
395
  startingBid?: number | undefined;
396
+ currentBid?: number | undefined;
336
397
  bidCount?: number | undefined;
337
- auctionEndDate?: string | Date | undefined;
338
398
  reservePrice?: number | undefined;
339
399
  buyNowPrice?: number | undefined;
340
400
  minBidIncrement?: number | undefined;
@@ -348,12 +408,43 @@ export declare const productItemSchema: z.ZodObject<{
348
408
  preOrderCurrentCount?: number | undefined;
349
409
  preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship" | undefined;
350
410
  preOrderCancellable?: boolean | undefined;
411
+ pickupAddressId?: string | undefined;
351
412
  viewCount?: number | undefined;
352
413
  avgRating?: number | undefined;
353
- shippingPaidBy?: "seller" | "buyer" | undefined;
354
- pickupAddressId?: string | undefined;
355
- insurance?: boolean | undefined;
356
- insuranceCost?: number | undefined;
414
+ reviewCount?: number | undefined;
415
+ bulkDiscounts?: {
416
+ quantity: number;
417
+ discountPercent: number;
418
+ }[] | undefined;
419
+ ingredients?: string[] | undefined;
420
+ howToUse?: string[] | undefined;
421
+ allowOffers?: boolean | undefined;
422
+ minOfferPercent?: number | undefined;
423
+ customFields?: {
424
+ value: string;
425
+ type: "number" | "boolean" | "text" | "url";
426
+ key: string;
427
+ unit?: string | undefined;
428
+ }[] | undefined;
429
+ customSections?: {
430
+ id: string;
431
+ title: string;
432
+ text?: string | undefined;
433
+ fields?: {
434
+ value: string;
435
+ type: "number" | "boolean" | "text" | "url";
436
+ key: string;
437
+ unit?: string | undefined;
438
+ }[] | undefined;
439
+ }[] | undefined;
440
+ originalPrice?: number | undefined;
441
+ categorySlug?: string | undefined;
442
+ sellerAvatar?: string | undefined;
443
+ listingType?: "fixed" | "auction" | "standard" | "pre-order" | undefined;
444
+ inStock?: boolean | undefined;
445
+ stockCount?: number | undefined;
446
+ attributes?: Record<string, string> | undefined;
447
+ publishedAt?: string | undefined;
357
448
  }>;
358
449
  /** Base Zod schema for list-query parameters. */
359
450
  export declare const productListParamsSchema: z.ZodObject<{
@@ -381,8 +472,8 @@ export declare const productListParamsSchema: z.ZodObject<{
381
472
  perPage?: number | undefined;
382
473
  storeId?: string | undefined;
383
474
  featured?: boolean | undefined;
384
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
385
475
  isAuction?: boolean | undefined;
476
+ condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
386
477
  inStock?: boolean | undefined;
387
478
  }, {
388
479
  sort?: string | undefined;
@@ -395,7 +486,7 @@ export declare const productListParamsSchema: z.ZodObject<{
395
486
  perPage?: number | undefined;
396
487
  storeId?: string | undefined;
397
488
  featured?: boolean | undefined;
398
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
399
489
  isAuction?: boolean | undefined;
490
+ condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
400
491
  inStock?: boolean | undefined;
401
492
  }>;