@mohasinac/appkit 4.37.1 → 4.39.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/_internal/client/features/digital-code/CodeRevealPanel.d.ts +20 -1
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +21 -1
- package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.d.ts +40 -0
- package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.js +67 -0
- package/dist/_internal/server/features/bundles/og.d.ts +0 -1
- package/dist/_internal/server/features/bundles/og.js +2 -18
- package/dist/_internal/server/features/catalogue/og.d.ts +0 -1
- package/dist/_internal/server/features/catalogue/og.js +1 -9
- package/dist/_internal/server/features/checkout/actions.js +18 -0
- package/dist/_internal/server/features/classified/og.d.ts +0 -1
- package/dist/_internal/server/features/classified/og.js +1 -9
- package/dist/_internal/server/features/digital-code/download.d.ts +60 -0
- package/dist/_internal/server/features/digital-code/download.js +128 -0
- package/dist/_internal/server/features/digital-code/og.d.ts +0 -1
- package/dist/_internal/server/features/digital-code/og.js +1 -9
- package/dist/_internal/server/features/digital-code/pool.d.ts +101 -0
- package/dist/_internal/server/features/digital-code/pool.js +208 -0
- package/dist/_internal/server/features/digital-code/upload.d.ts +51 -0
- package/dist/_internal/server/features/digital-code/upload.js +144 -0
- package/dist/_internal/server/features/grouped/og.d.ts +7 -4
- package/dist/_internal/server/features/grouped/og.js +7 -6
- package/dist/_internal/server/features/item-requests/og.d.ts +0 -1
- package/dist/_internal/server/features/item-requests/og.js +2 -18
- package/dist/_internal/server/features/live/og.d.ts +0 -1
- package/dist/_internal/server/features/live/og.js +1 -9
- package/dist/_internal/server/features/prize-draws/og.js +5 -10
- package/dist/_internal/server/features/products/og.js +5 -2
- package/dist/_internal/server/features/seo/sitemap.js +10 -2
- package/dist/_internal/server/features/shared/listing-adapters.d.ts +1 -2
- package/dist/_internal/server/features/shared/listing-adapters.js +5 -3
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +40 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +153 -40
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +33 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +93 -25
- package/dist/_internal/server/notifications/send-recorder.d.ts +79 -0
- package/dist/_internal/server/notifications/send-recorder.js +126 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +31 -4
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/config.js +2 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/config.js +3 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/classified/config.js +3 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +3 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/live/config.js +3 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +3 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/config.js +2 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
- package/dist/client/observability/reportClientError.js +11 -1
- package/dist/client.d.ts +6 -0
- package/dist/client.js +10 -0
- package/dist/core/hooks/useSyncManager.js +11 -1
- package/dist/features/account/hooks/useNotifications.js +25 -5
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +12 -3
- package/dist/features/auctions/components/LiveBidPrice.js +2 -2
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +1 -1
- package/dist/features/auctions/components/LiveMinIncrement.js +8 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useAuctions.js +12 -1
- package/dist/features/auctions/hooks/useRealtimeBids.d.ts +3 -0
- package/dist/features/auctions/hooks/useRealtimeBids.js +138 -36
- package/dist/features/blog/components/BlogIndexPageView.js +10 -1
- package/dist/features/blog/repository/blog.repository.d.ts +14 -0
- package/dist/features/blog/repository/blog.repository.js +23 -1
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +2 -2
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -2
- package/dist/features/categories/components/BundleCollage.js +10 -5
- package/dist/features/categories/components/BundleDetailView.js +7 -3
- package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
- package/dist/features/categories/components/CategoryProductsListing.js +4 -1
- package/dist/features/categories/components/CategoryTree.js +1 -1
- package/dist/features/categories/repository/categories.repository.d.ts +34 -2
- package/dist/features/categories/repository/categories.repository.js +31 -9
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
- package/dist/features/contact/email.js +36 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -4
- package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
- package/dist/features/orders/repository/orders.repository.d.ts +21 -1
- package/dist/features/orders/repository/orders.repository.js +26 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +2 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +4 -1
- package/dist/features/products/actions/product-actions.d.ts +9 -0
- package/dist/features/products/actions/product-actions.js +11 -0
- package/dist/features/products/columns/productTableColumns.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +4 -1
- package/dist/features/products/components/BidHistory.js +2 -2
- package/dist/features/products/components/CompareOverlay.js +2 -2
- package/dist/features/products/components/GroupMemberPicker.js +5 -5
- package/dist/features/products/components/ListingBottomActions.js +12 -3
- package/dist/features/products/components/MakeOfferButton.js +7 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +4 -4
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
- package/dist/features/products/components/PrizeDrawCollage.js +16 -5
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +4 -1
- package/dist/features/products/components/PrizeRevealModal.js +2 -2
- package/dist/features/products/components/ProductDetailActions.js +10 -3
- package/dist/features/products/components/ProductFilters.js +2 -2
- package/dist/features/products/components/ProductGrid.js +5 -2
- package/dist/features/products/components/ProductsIndexListing.js +12 -1
- package/dist/features/products/components/ProductsIndexPageView.js +26 -0
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -2
- package/dist/features/products/constants/listing-tabs.d.ts +36 -12
- package/dist/features/products/constants/listing-tabs.js +38 -14
- package/dist/features/products/constants/sieve.d.ts +14 -0
- package/dist/features/products/constants/sieve.js +39 -0
- package/dist/features/products/repository/products.repository.d.ts +35 -0
- package/dist/features/products/repository/products.repository.js +116 -3
- package/dist/features/products/schemas/firestore.d.ts +37 -0
- package/dist/features/seller/actions/seller-actions.js +4 -13
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -1
- package/dist/features/seller/components/SellerProductShell.js +11 -2
- package/dist/features/stores/api/[storeSlug]/reviews/route.d.ts +6 -1
- package/dist/features/stores/api/[storeSlug]/reviews/route.js +89 -30
- package/dist/features/stores/components/StoresIndexPageView.js +17 -1
- package/dist/features/tester/seed-data/authored/admin__users-trust.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__bidding.js +5 -4
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +85 -85
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +2 -2
- package/dist/features/tester/seed-data/authored/buying__offers.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +53 -15
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +6 -6
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +6 -4
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +8 -8
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +8 -8
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +16 -0
- package/dist/features/tester/seed-data/authored/seo__og-images.js +17 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +41 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +36 -6
- package/dist/features/tester/utils/phases.js +78 -12
- package/dist/features/wishlist/components/WishlistPage.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/providers/db-firebase/rtdb-paths.d.ts +7 -0
- package/dist/providers/db-firebase/rtdb-paths.js +7 -0
- package/dist/react/contexts/SessionContext.js +40 -1
- package/dist/react/hooks/useCanSeePrices.d.ts +37 -0
- package/dist/react/hooks/useCanSeePrices.js +40 -0
- package/dist/seed/addresses-seed-data.js +36 -2
- package/dist/seed/users-seed-data.js +10 -2
- package/dist/seo/json-ld.d.ts +21 -0
- package/dist/seo/json-ld.js +45 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +12 -0
- package/dist/styles.css +41 -0
- package/dist/ui/components/FilePickerButton.d.ts +32 -0
- package/dist/ui/components/FilePickerButton.js +23 -0
- package/dist/ui/components/GatedPrice.d.ts +66 -0
- package/dist/ui/components/GatedPrice.js +80 -0
- package/dist/ui/components/GatedPrice.style.css +40 -0
- package/dist/ui/components/PriceDisplay.d.ts +12 -0
- package/dist/ui/components/PriceDisplay.js +17 -17
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +8 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { Div, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
|
|
5
|
+
import { Div, PricesOnly, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
|
|
6
6
|
import { MediaImage } from "../../media/MediaImage";
|
|
7
7
|
import { GroupMemberPicker } from "./GroupMemberPicker";
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -23,7 +23,7 @@ function MemberThumb({ member, isCurrent }) {
|
|
|
23
23
|
const price = formatCurrency(member.price, member.currency ?? "INR");
|
|
24
24
|
return (_jsxs(Link, { href: href, "aria-label": member.title, className: "flex flex-col items-center gap-[var(--appkit-space-1-5)] flex-shrink-0 w-16 group", children: [_jsxs(Div, { rounded: "full", className: `relative w-14 h-14 overflow-hidden border-2 transition-all ${isCurrent
|
|
25
25
|
? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
|
|
26
|
-
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
|
|
26
|
+
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: _jsx(PricesOnly, { children: price }) })] }));
|
|
27
27
|
}
|
|
28
28
|
export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle }) {
|
|
29
29
|
const [open, setOpen] = useState(false);
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { ROUTES } from "../../../next";
|
|
6
|
-
import { Div, Row, Span, Text } from "../../../ui";
|
|
6
|
+
import { Div, PricesOnly, Row, Span, Text } from "../../../ui";
|
|
7
7
|
import { MediaImage } from "../../media/MediaImage";
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
9
9
|
import { normalizeListingType } from "../utils/listing-type";
|
|
@@ -23,7 +23,7 @@ function ListingThumb({ listing, isCurrent, }) {
|
|
|
23
23
|
const price = formatCurrency(listing.price, listing.currency ?? "INR");
|
|
24
24
|
return (_jsxs(Link, { href: href, "aria-label": listing.title, className: "flex flex-col items-center gap-[var(--appkit-space-1-5)] flex-shrink-0 w-16 group", children: [_jsx(Div, { rounded: "full", className: `relative w-14 h-14 overflow-hidden border-2 transition-all ${isCurrent
|
|
25
25
|
? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
|
|
26
|
-
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: image ? (_jsx(MediaImage, { src: image, alt: listing.title, size: "avatar" })) : (_jsx(Row, { textSize: "xs", color: "faint", className: "w-full h-full", surface: "subtle", align: "center", justify: "center", children: "\u00E2\u2014\u00AF" })) }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: listing.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
|
|
26
|
+
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: image ? (_jsx(MediaImage, { src: image, alt: listing.title, size: "avatar" })) : (_jsx(Row, { textSize: "xs", color: "faint", className: "w-full h-full", surface: "subtle", align: "center", justify: "center", children: "\u00E2\u2014\u00AF" })) }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: listing.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: _jsx(PricesOnly, { children: price }) })] }));
|
|
27
27
|
}
|
|
28
28
|
export function SublistingCarouselSection({ sublistingCategoryId, currentListingId }) {
|
|
29
29
|
const [open, setOpen] = useState(false);
|
|
@@ -33,16 +33,33 @@ export interface ListingTab {
|
|
|
33
33
|
entity?: "stores";
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* The generic `/products` browse page spans
|
|
36
|
+
* The types the generic `/products` browse page spans — the GENERAL CATALOGUE,
|
|
37
|
+
* not an index of everything.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* DERIVED from `ListingTypePlugin.inGeneralCatalogue`, never hand-written: a
|
|
40
|
+
* literal array here is how ten enumerations of one union drifted apart in
|
|
41
|
+
* Root Cause #61, and `audit-listing-type-tab-coverage`'s STAYS_DERIVED rule
|
|
42
|
+
* blocks one.
|
|
43
|
+
*
|
|
44
|
+
* ## History — this spanned all nine types from 2026-08-21 until now
|
|
45
|
+
*
|
|
46
|
+
* The widening was a fix for Root Cause #61 and cited three reasons. Two have
|
|
47
|
+
* since been solved elsewhere, and the third is an accepted cost:
|
|
48
|
+
*
|
|
49
|
+
* 1. "Five of nine types unreachable from the main catalogue." Still true for
|
|
50
|
+
* `classified`/`digital-code`/`live`, which have dedicated pages but no
|
|
51
|
+
* `MAIN_NAV_ITEMS` entry — which is exactly why `inGeneralCatalogue` keeps
|
|
52
|
+
* those three here. The other five are all in the main nav.
|
|
53
|
+
* 2. "Seven legacy `/search/.../tab/<type>` redirects all point at /products."
|
|
54
|
+
* No longer true: that route derives `tabSlug -> browseRoute` from this
|
|
55
|
+
* same registry, and `/search` maps every resource type to its own page.
|
|
56
|
+
* Neither falls through to `/products` for a typed request.
|
|
57
|
+
* 3. "Cross-type Compare + bulk-cart only exist on this page." Still true, and
|
|
58
|
+
* accepted: you can no longer compare an auction against a standard product.
|
|
59
|
+
*
|
|
60
|
+
* A type appearing both here and in the main nav is the bug this fixes — the
|
|
61
|
+
* same item reachable from two places with two different chromes (an auction
|
|
62
|
+
* card in the general grid renders Buy-Now chrome, not bid affordances).
|
|
46
63
|
*/
|
|
47
64
|
export declare const GENERIC_PRODUCT_LISTING_TYPES: readonly ListingType[];
|
|
48
65
|
/** The listing types the combined `/art` page spans. */
|
|
@@ -60,9 +77,16 @@ export type SellerListingTabId = string;
|
|
|
60
77
|
* In-page type-filter chips on the generic `/products` browse page.
|
|
61
78
|
*
|
|
62
79
|
* Multi-select (checkbox semantics) — no "All" sentinel chip, because "none
|
|
63
|
-
* checked" already means "every type". A sentinel
|
|
64
|
-
* express the same state and would have to be cleared
|
|
65
|
-
* was ticked.
|
|
80
|
+
* checked" already means "every type IN THE GENERAL CATALOGUE". A sentinel
|
|
81
|
+
* would be a second way to express the same state and would have to be cleared
|
|
82
|
+
* whenever a real chip was ticked.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 Derived from `GENERIC_PRODUCT_LISTING_TYPES`, NOT `ALL_LISTING_TYPES`. The
|
|
85
|
+
* chips must offer exactly the types the page can actually return: a chip for a
|
|
86
|
+
* type outside the span intersects to empty, and `listPublicProducts` then falls
|
|
87
|
+
* back to the full span — i.e. ticking it would silently widen the results
|
|
88
|
+
* instead of narrowing them. `ADMIN_PRODUCT_LISTING_TYPE_TABS` is a separate
|
|
89
|
+
* constant and correctly stays all-nine.
|
|
66
90
|
*/
|
|
67
91
|
export declare const PRODUCT_TYPE_FILTER_TABS: readonly ListingTab[];
|
|
68
92
|
/** In-page type-filter chips on the combined `/art` (Art & Stickers) page. */
|
|
@@ -22,18 +22,35 @@
|
|
|
22
22
|
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
23
23
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
24
24
|
/**
|
|
25
|
-
* The generic `/products` browse page spans
|
|
25
|
+
* The types the generic `/products` browse page spans — the GENERAL CATALOGUE,
|
|
26
|
+
* not an index of everything.
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* DERIVED from `ListingTypePlugin.inGeneralCatalogue`, never hand-written: a
|
|
29
|
+
* literal array here is how ten enumerations of one union drifted apart in
|
|
30
|
+
* Root Cause #61, and `audit-listing-type-tab-coverage`'s STAYS_DERIVED rule
|
|
31
|
+
* blocks one.
|
|
32
|
+
*
|
|
33
|
+
* ## History — this spanned all nine types from 2026-08-21 until now
|
|
34
|
+
*
|
|
35
|
+
* The widening was a fix for Root Cause #61 and cited three reasons. Two have
|
|
36
|
+
* since been solved elsewhere, and the third is an accepted cost:
|
|
37
|
+
*
|
|
38
|
+
* 1. "Five of nine types unreachable from the main catalogue." Still true for
|
|
39
|
+
* `classified`/`digital-code`/`live`, which have dedicated pages but no
|
|
40
|
+
* `MAIN_NAV_ITEMS` entry — which is exactly why `inGeneralCatalogue` keeps
|
|
41
|
+
* those three here. The other five are all in the main nav.
|
|
42
|
+
* 2. "Seven legacy `/search/.../tab/<type>` redirects all point at /products."
|
|
43
|
+
* No longer true: that route derives `tabSlug -> browseRoute` from this
|
|
44
|
+
* same registry, and `/search` maps every resource type to its own page.
|
|
45
|
+
* Neither falls through to `/products` for a typed request.
|
|
46
|
+
* 3. "Cross-type Compare + bulk-cart only exist on this page." Still true, and
|
|
47
|
+
* accepted: you can no longer compare an auction against a standard product.
|
|
48
|
+
*
|
|
49
|
+
* A type appearing both here and in the main nav is the bug this fixes — the
|
|
50
|
+
* same item reachable from two places with two different chromes (an auction
|
|
51
|
+
* card in the general grid renders Buy-Now chrome, not bid affordances).
|
|
35
52
|
*/
|
|
36
|
-
export const GENERIC_PRODUCT_LISTING_TYPES = ALL_LISTING_TYPES;
|
|
53
|
+
export const GENERIC_PRODUCT_LISTING_TYPES = ALL_LISTING_TYPES.filter((t) => pluginFor(t).inGeneralCatalogue);
|
|
37
54
|
/** The listing types the combined `/art` page spans. */
|
|
38
55
|
export const ART_STICKERS_LISTING_TYPES = ["art", "stickers"];
|
|
39
56
|
/**
|
|
@@ -87,10 +104,17 @@ export const SELLER_LISTING_TABS = [
|
|
|
87
104
|
* In-page type-filter chips on the generic `/products` browse page.
|
|
88
105
|
*
|
|
89
106
|
* Multi-select (checkbox semantics) — no "All" sentinel chip, because "none
|
|
90
|
-
* checked" already means "every type". A sentinel
|
|
91
|
-
* express the same state and would have to be cleared
|
|
92
|
-
* was ticked.
|
|
107
|
+
* checked" already means "every type IN THE GENERAL CATALOGUE". A sentinel
|
|
108
|
+
* would be a second way to express the same state and would have to be cleared
|
|
109
|
+
* whenever a real chip was ticked.
|
|
110
|
+
*
|
|
111
|
+
* 🛑 Derived from `GENERIC_PRODUCT_LISTING_TYPES`, NOT `ALL_LISTING_TYPES`. The
|
|
112
|
+
* chips must offer exactly the types the page can actually return: a chip for a
|
|
113
|
+
* type outside the span intersects to empty, and `listPublicProducts` then falls
|
|
114
|
+
* back to the full span — i.e. ticking it would silently widen the results
|
|
115
|
+
* instead of narrowing them. `ADMIN_PRODUCT_LISTING_TYPE_TABS` is a separate
|
|
116
|
+
* constant and correctly stays all-nine.
|
|
93
117
|
*/
|
|
94
|
-
export const PRODUCT_TYPE_FILTER_TABS =
|
|
118
|
+
export const PRODUCT_TYPE_FILTER_TABS = GENERIC_PRODUCT_LISTING_TYPES.map(chipTab);
|
|
95
119
|
/** In-page type-filter chips on the combined `/art` (Art & Stickers) page. */
|
|
96
120
|
export const ART_STICKERS_TYPE_FILTER_TABS = ART_STICKERS_LISTING_TYPES.map(chipTab);
|
|
@@ -16,6 +16,20 @@ export declare const BASE_SORT_OPTIONS: readonly [{
|
|
|
16
16
|
readonly value: string;
|
|
17
17
|
readonly label: "Price: High to Low";
|
|
18
18
|
}];
|
|
19
|
+
/**
|
|
20
|
+
* Drop the price sorts when the viewer may not see prices.
|
|
21
|
+
*
|
|
22
|
+
* Ordering a list by price discloses the ranking of every hidden amount, and
|
|
23
|
+
* with paging that is most of the way to a price list — so the option is not
|
|
24
|
+
* offered to signed-out visitors, matching the hidden price-range facet.
|
|
25
|
+
*
|
|
26
|
+
* 🛑 This removes the CONTROL, not the capability. A hand-written
|
|
27
|
+
* `?sort=price:asc` still sorts, exactly as `?f=price>N` still filters — both
|
|
28
|
+
* are the documented, accepted residue of gating the render rather than
|
|
29
|
+
* withholding the field (see CLAUDE.md § "Public Data Projections"). What it
|
|
30
|
+
* buys is that no casual visitor is handed the lever.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withoutPriceSorts(options: readonly SortOption[], canSeePrices: boolean): readonly SortOption[];
|
|
19
33
|
export declare const STANDARD_SORT_OPTIONS: readonly [{
|
|
20
34
|
readonly value: string;
|
|
21
35
|
readonly label: "Newest First";
|
|
@@ -19,6 +19,45 @@ const BASE_PRICE_SORTS = [
|
|
|
19
19
|
];
|
|
20
20
|
/** Time + price sorts, the common subset across most listing-type pickers. */
|
|
21
21
|
export const BASE_SORT_OPTIONS = [...BASE_TIME_SORTS, ...BASE_PRICE_SORTS];
|
|
22
|
+
/**
|
|
23
|
+
* Drop the price sorts when the viewer may not see prices.
|
|
24
|
+
*
|
|
25
|
+
* Ordering a list by price discloses the ranking of every hidden amount, and
|
|
26
|
+
* with paging that is most of the way to a price list — so the option is not
|
|
27
|
+
* offered to signed-out visitors, matching the hidden price-range facet.
|
|
28
|
+
*
|
|
29
|
+
* 🛑 This removes the CONTROL, not the capability. A hand-written
|
|
30
|
+
* `?sort=price:asc` still sorts, exactly as `?f=price>N` still filters — both
|
|
31
|
+
* are the documented, accepted residue of gating the render rather than
|
|
32
|
+
* withholding the field (see CLAUDE.md § "Public Data Projections"). What it
|
|
33
|
+
* buys is that no casual visitor is handed the lever.
|
|
34
|
+
*/
|
|
35
|
+
export function withoutPriceSorts(options, canSeePrices) {
|
|
36
|
+
if (canSeePrices)
|
|
37
|
+
return options;
|
|
38
|
+
// Exact match on the field, both directions. `sortBy()` encodes direction as
|
|
39
|
+
// a leading "-", so stripping it yields the bare field name — substring
|
|
40
|
+
// matching would catch an unrelated `pricePerEntry` sort by accident.
|
|
41
|
+
return options.filter((o) => !MONEY_SORT_FIELDS.includes(o.value.replace(/^-/, "")));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every sort field that orders by an AMOUNT, matched as a substring of the
|
|
45
|
+
* sort value (`sortBy()` emits `field` / `-field`, so one entry covers both
|
|
46
|
+
* directions).
|
|
47
|
+
*
|
|
48
|
+
* `currentBid`, `startingBid` and `buyNowPrice` are here for the same reason
|
|
49
|
+
* `price` is: "Highest Current Bid" ranks every hidden bid on the page, which
|
|
50
|
+
* is the disclosure the gate exists to prevent. `bidCount` is deliberately
|
|
51
|
+
* ABSENT — the number of bids is not an amount, it is already displayed
|
|
52
|
+
* ungated beside every auction, and hiding it would remove a real signal for
|
|
53
|
+
* no gain.
|
|
54
|
+
*/
|
|
55
|
+
const MONEY_SORT_FIELDS = [
|
|
56
|
+
PRODUCT_FIELDS.PRICE,
|
|
57
|
+
PRODUCT_FIELDS.CURRENT_BID,
|
|
58
|
+
PRODUCT_FIELDS.STARTING_BID,
|
|
59
|
+
PRODUCT_FIELDS.BUY_NOW_PRICE,
|
|
60
|
+
];
|
|
22
61
|
// ---------------------------------------------------------------------------
|
|
23
62
|
// Standard Products
|
|
24
63
|
// ---------------------------------------------------------------------------
|
|
@@ -12,6 +12,41 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
|
|
|
12
12
|
private cacheInvalidateForId;
|
|
13
13
|
findById(id: string): Promise<ProductDocument | null>;
|
|
14
14
|
protected mapDoc<D = ProductDocument>(snap: DocumentSnapshot): D;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a product's taxonomy into the shape every READ path expects:
|
|
17
|
+
* `categorySlugs` as the FULL ancestor chain (self first, root last), and a
|
|
18
|
+
* brand recorded as both its id and its display name.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 WHY THIS LIVES IN THE REPOSITORY. The seller and admin create/update
|
|
21
|
+
* paths send a SINGLE category id (`category: "category-x-starters"`) — and
|
|
22
|
+
* `productCreateSchema` (src/validation/request-schemas.ts:129) declares only
|
|
23
|
+
* that scalar, with no `.passthrough()`, so an inbound `categorySlugs` array
|
|
24
|
+
* is stripped before it ever reaches Firestore. `mapDoc` then backfills
|
|
25
|
+
* `categorySlugs = [category]` **on read only**, which is invisible to a
|
|
26
|
+
* query: the category page runs `array-contains-any` against the STORED
|
|
27
|
+
* field, so a UI-created listing was unreachable from its own leaf category
|
|
28
|
+
* and from every ancestor of it. CLAUDE.md recorded the ancestor half of this
|
|
29
|
+
* as an outstanding follow-up; the leaf half was never noticed because every
|
|
30
|
+
* product in the database came from the seed, which hand-writes the chain
|
|
31
|
+
* (measured 2026-09-14: 95/95 products carried a correct chain, so the defect
|
|
32
|
+
* is LATENT — it fires the first time anyone creates a listing through the
|
|
33
|
+
* UI, which is precisely why no page looks broken today).
|
|
34
|
+
*
|
|
35
|
+
* There are ~14 independent write paths — two seller actions, two admin
|
|
36
|
+
* actions, four in `_internal/server/features/products/actions.ts`, the
|
|
37
|
+
* duplicate route, group-children, catalogue promotion, shipment linking and
|
|
38
|
+
* the WhatsApp import. Deriving at each one is Root Cause #75's shape: the
|
|
39
|
+
* copy made before a branch was added never learns about it. `create` and
|
|
40
|
+
* `update` are the only two methods every one of them funnels through.
|
|
41
|
+
*
|
|
42
|
+
* 🛑 It reads `categories` through `this.db` rather than importing
|
|
43
|
+
* `categoriesRepository`. No repository in this codebase imports another
|
|
44
|
+
* feature's repository, and the reason is the Turbopack import-chain trap
|
|
45
|
+
* (Root Cause #6/#18) — a raw collection read has neither problem.
|
|
46
|
+
*/
|
|
47
|
+
private deriveTaxonomy;
|
|
48
|
+
/** The keys whose presence means a write could change the product's taxonomy. */
|
|
49
|
+
private static readonly TAXONOMY_KEYS;
|
|
15
50
|
update(id: string, data: Partial<ProductDocument>): Promise<ProductDocument>;
|
|
16
51
|
create(input: ProductCreateInput): Promise<ProductDocument>;
|
|
17
52
|
findByStore(storeId: string): Promise<ProductDocument[]>;
|
|
@@ -10,6 +10,9 @@ import { matchesAllSearchTerms, parseSearchTxtQuery, } from "../../../utils/sear
|
|
|
10
10
|
// wrapper. Three copies existed and two were missing the same three fields.
|
|
11
11
|
import { buildProductSearchTxt } from "../../../utils/search-txt-builders";
|
|
12
12
|
import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
|
|
13
|
+
// Collection NAME only — deliberately not `categoriesRepository`. See
|
|
14
|
+
// deriveTaxonomy()'s header for why this is a raw read.
|
|
15
|
+
import { CATEGORIES_COLLECTION } from "../../categories/schemas/firestore";
|
|
13
16
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
14
17
|
/**
|
|
15
18
|
* Canonical listing-kind tokens used in `listingType`. Note: the public Sieve
|
|
@@ -92,12 +95,109 @@ export class ProductRepository extends BaseRepository {
|
|
|
92
95
|
}
|
|
93
96
|
return doc;
|
|
94
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a product's taxonomy into the shape every READ path expects:
|
|
100
|
+
* `categorySlugs` as the FULL ancestor chain (self first, root last), and a
|
|
101
|
+
* brand recorded as both its id and its display name.
|
|
102
|
+
*
|
|
103
|
+
* 🛑 WHY THIS LIVES IN THE REPOSITORY. The seller and admin create/update
|
|
104
|
+
* paths send a SINGLE category id (`category: "category-x-starters"`) — and
|
|
105
|
+
* `productCreateSchema` (src/validation/request-schemas.ts:129) declares only
|
|
106
|
+
* that scalar, with no `.passthrough()`, so an inbound `categorySlugs` array
|
|
107
|
+
* is stripped before it ever reaches Firestore. `mapDoc` then backfills
|
|
108
|
+
* `categorySlugs = [category]` **on read only**, which is invisible to a
|
|
109
|
+
* query: the category page runs `array-contains-any` against the STORED
|
|
110
|
+
* field, so a UI-created listing was unreachable from its own leaf category
|
|
111
|
+
* and from every ancestor of it. CLAUDE.md recorded the ancestor half of this
|
|
112
|
+
* as an outstanding follow-up; the leaf half was never noticed because every
|
|
113
|
+
* product in the database came from the seed, which hand-writes the chain
|
|
114
|
+
* (measured 2026-09-14: 95/95 products carried a correct chain, so the defect
|
|
115
|
+
* is LATENT — it fires the first time anyone creates a listing through the
|
|
116
|
+
* UI, which is precisely why no page looks broken today).
|
|
117
|
+
*
|
|
118
|
+
* There are ~14 independent write paths — two seller actions, two admin
|
|
119
|
+
* actions, four in `_internal/server/features/products/actions.ts`, the
|
|
120
|
+
* duplicate route, group-children, catalogue promotion, shipment linking and
|
|
121
|
+
* the WhatsApp import. Deriving at each one is Root Cause #75's shape: the
|
|
122
|
+
* copy made before a branch was added never learns about it. `create` and
|
|
123
|
+
* `update` are the only two methods every one of them funnels through.
|
|
124
|
+
*
|
|
125
|
+
* 🛑 It reads `categories` through `this.db` rather than importing
|
|
126
|
+
* `categoriesRepository`. No repository in this codebase imports another
|
|
127
|
+
* feature's repository, and the reason is the Turbopack import-chain trap
|
|
128
|
+
* (Root Cause #6/#18) — a raw collection read has neither problem.
|
|
129
|
+
*/
|
|
130
|
+
async deriveTaxonomy(input) {
|
|
131
|
+
const out = {};
|
|
132
|
+
const cats = this.db.collection(CATEGORIES_COLLECTION);
|
|
133
|
+
const leaf = (Array.isArray(input.categorySlugs) && input.categorySlugs[0]) ||
|
|
134
|
+
input.category ||
|
|
135
|
+
null;
|
|
136
|
+
/*
|
|
137
|
+
* The brand selector's option value is the brand ROW's id (`brand-beyblade`),
|
|
138
|
+
* but `ProductDocument.brand` is matched by DISPLAY NAME —
|
|
139
|
+
* `BrandDetailPageView` filters `sieveFilter("brand", EQ, brand.name)`, and
|
|
140
|
+
* CLAUDE.md's § "Brand matching is by DISPLAY NAME" says so explicitly. So a
|
|
141
|
+
* seller-created listing stored `brand: "brand-beyblade"` while every seeded
|
|
142
|
+
* one stored `brand: "Beyblade"`, and the brand page matched neither the new
|
|
143
|
+
* row nor counted it. Resolving the id back to its name fixes that; a value
|
|
144
|
+
* that is ALREADY a display name does not resolve as a document id and is
|
|
145
|
+
* left exactly as it was.
|
|
146
|
+
*/
|
|
147
|
+
const brandRef = input.brandSlug || input.brand || null;
|
|
148
|
+
const [catSnap, brandSnap] = await Promise.all([
|
|
149
|
+
leaf ? cats.doc(leaf).get() : Promise.resolve(null),
|
|
150
|
+
brandRef ? cats.doc(brandRef).get() : Promise.resolve(null),
|
|
151
|
+
]);
|
|
152
|
+
if (catSnap?.exists) {
|
|
153
|
+
const cat = catSnap.data();
|
|
154
|
+
// `parentIds` is ordered root-first / nearest-last (category-tree.ts:107),
|
|
155
|
+
// and the stored chain is self-first / root-last. Reversing is what makes
|
|
156
|
+
// the derived value byte-identical to what the seed hand-writes — verified
|
|
157
|
+
// against products-standard-seed-data.ts:197.
|
|
158
|
+
out.categorySlugs = [leaf, ...(cat.parentIds ?? []).slice().reverse()];
|
|
159
|
+
out.category = leaf;
|
|
160
|
+
const names = [
|
|
161
|
+
cat.name,
|
|
162
|
+
...(cat.ancestors ?? []).map((a) => a?.name).reverse(),
|
|
163
|
+
].filter((n) => !!n);
|
|
164
|
+
if (names.length)
|
|
165
|
+
out.categoryNames = names;
|
|
166
|
+
}
|
|
167
|
+
else if (leaf) {
|
|
168
|
+
// Never destroy a chain we cannot improve on. A leaf that is not a real
|
|
169
|
+
// category row is a data problem to report, not a reason to blank the field.
|
|
170
|
+
serverLogger.warn("Product category id does not resolve to a category", {
|
|
171
|
+
leaf,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (brandSnap?.exists) {
|
|
175
|
+
const brand = brandSnap.data();
|
|
176
|
+
if (brand.categoryType === "brand") {
|
|
177
|
+
out.brandSlug = brandRef;
|
|
178
|
+
if (brand.name)
|
|
179
|
+
out.brand = brand.name;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
95
184
|
async update(id, data) {
|
|
96
185
|
this.cacheInvalidateForId(id);
|
|
97
186
|
const current = await super.findById(id);
|
|
98
|
-
|
|
187
|
+
/*
|
|
188
|
+
* Only re-derive when the write actually names a taxonomy field. Every
|
|
189
|
+
* stock decrement, view increment and status flip would otherwise pay two
|
|
190
|
+
* extra Firestore reads, which Rule #6 budgets at ~3 round-trips for a
|
|
191
|
+
* whole request.
|
|
192
|
+
*/
|
|
193
|
+
const touchesTaxonomy = ProductRepository.TAXONOMY_KEYS.some((k) => k in data);
|
|
194
|
+
const derived = touchesTaxonomy
|
|
195
|
+
? await this.deriveTaxonomy({ ...current, ...data })
|
|
196
|
+
: {};
|
|
197
|
+
const merged = { ...current, ...data, ...derived };
|
|
99
198
|
const updated = await super.update(id, {
|
|
100
199
|
...data,
|
|
200
|
+
...derived,
|
|
101
201
|
searchTxt: buildProductSearchTxt(merged),
|
|
102
202
|
});
|
|
103
203
|
this.cacheSet(updated);
|
|
@@ -117,12 +217,18 @@ export class ProductRepository extends BaseRepository {
|
|
|
117
217
|
}
|
|
118
218
|
});
|
|
119
219
|
const barcodeId = input.barcodeId ?? await generateBarcodeId(id);
|
|
220
|
+
// Always derive on create — one extra read per listing created, and it is
|
|
221
|
+
// what makes a UI-created product reachable from its own category page at
|
|
222
|
+
// all. A seed row already carrying the correct chain derives to the same
|
|
223
|
+
// value, so this is a no-op for every existing fixture.
|
|
224
|
+
const derived = await this.deriveTaxonomy(input);
|
|
225
|
+
const withTaxonomy = { ...input, ...derived };
|
|
120
226
|
const productData = {
|
|
121
|
-
...
|
|
227
|
+
...withTaxonomy,
|
|
122
228
|
barcodeId,
|
|
123
229
|
slug: id,
|
|
124
230
|
availableQuantity: input.stockQuantity,
|
|
125
|
-
searchTxt: buildProductSearchTxt(
|
|
231
|
+
searchTxt: buildProductSearchTxt(withTaxonomy),
|
|
126
232
|
createdAt: new Date(),
|
|
127
233
|
updatedAt: new Date(),
|
|
128
234
|
};
|
|
@@ -525,6 +631,13 @@ export class ProductRepository extends BaseRepository {
|
|
|
525
631
|
}
|
|
526
632
|
}
|
|
527
633
|
ProductRepository.CACHE_TTL_MS = 30000;
|
|
634
|
+
/** The keys whose presence means a write could change the product's taxonomy. */
|
|
635
|
+
ProductRepository.TAXONOMY_KEYS = [
|
|
636
|
+
"categorySlugs",
|
|
637
|
+
"category",
|
|
638
|
+
"brand",
|
|
639
|
+
"brandSlug",
|
|
640
|
+
];
|
|
528
641
|
ProductRepository.SIEVE_FIELDS = {
|
|
529
642
|
id: { canFilter: true, canSort: false },
|
|
530
643
|
title: { canFilter: true, canSort: true },
|
|
@@ -429,9 +429,46 @@ export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_U
|
|
|
429
429
|
export type ProductAdminUpdateInput = Partial<Omit<ProductDocument, "id" | "createdAt">>;
|
|
430
430
|
export declare const PRODUCT_CODES_SUBCOLLECTION: "codes";
|
|
431
431
|
export type ProductCodeStatus = "available" | "claimed" | "revoked";
|
|
432
|
+
/**
|
|
433
|
+
* What the buyer actually receives. A "digital code" listing delivers digital
|
|
434
|
+
* CONTENT — a redemption string is only the commonest shape of it.
|
|
435
|
+
*
|
|
436
|
+
* `"code"` is the default and is what every pre-existing document is, so no
|
|
437
|
+
* migration is needed and no existing listing changes behaviour.
|
|
438
|
+
*/
|
|
439
|
+
export type ProductCodeContentKind = "code" | "image" | "file";
|
|
432
440
|
export interface ProductCodeDocument extends BaseDocument {
|
|
433
441
|
productId: string;
|
|
442
|
+
/**
|
|
443
|
+
* The redemption string. Required for `contentKind: "code"`, empty for the
|
|
444
|
+
* asset kinds — a QR is not a string.
|
|
445
|
+
*/
|
|
434
446
|
code: string;
|
|
447
|
+
/** Defaults to `"code"` when absent. */
|
|
448
|
+
contentKind?: ProductCodeContentKind;
|
|
449
|
+
/**
|
|
450
|
+
* 🛑 A RAW FIREBASE STORAGE PATH, NEVER a `/media/{slug}` URL, and never a
|
|
451
|
+
* signed URL.
|
|
452
|
+
*
|
|
453
|
+
* Two facts make this load-bearing:
|
|
454
|
+
* - `storage.rules` is `allow read: if true` for every object in the
|
|
455
|
+
* bucket, and
|
|
456
|
+
* - `GET /api/media/[...slug]` applies NO authentication at all — it is a
|
|
457
|
+
* bare exported handler with no session read, by design, because it
|
|
458
|
+
* exists to serve public product photos through a watermarker.
|
|
459
|
+
*
|
|
460
|
+
* So anything reachable at a `/media/` slug is world-readable, and image
|
|
461
|
+
* filenames in this codebase are content-derived and therefore guessable.
|
|
462
|
+
* Putting a paid QR there would publish it. Instead the bytes are streamed by
|
|
463
|
+
* `GET /api/orders/{orderId}/code/asset`, which re-checks that the caller
|
|
464
|
+
* owns the order — the same shape as the invoice route, the one existing
|
|
465
|
+
* precedent for a gated download here.
|
|
466
|
+
*/
|
|
467
|
+
assetPath?: string;
|
|
468
|
+
/** Original upload name, used for the `Content-Disposition` filename. */
|
|
469
|
+
fileName?: string;
|
|
470
|
+
/** Verified by magic bytes at upload, replayed as the response Content-Type. */
|
|
471
|
+
contentType?: string;
|
|
435
472
|
status: ProductCodeStatus;
|
|
436
473
|
orderId?: string;
|
|
437
474
|
claimedByUserId?: string;
|
|
@@ -448,19 +448,10 @@ export async function listSellerOrders(userId, params) {
|
|
|
448
448
|
hasMore: false,
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
items: [],
|
|
456
|
-
total: 0,
|
|
457
|
-
page: 1,
|
|
458
|
-
pageSize: params?.pageSize ?? 20,
|
|
459
|
-
totalPages: 0,
|
|
460
|
-
hasMore: false,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
return orderRepository.listForSeller(productIds, {
|
|
451
|
+
// No products query: orders are scoped by `storeId`, the field a cart is
|
|
452
|
+
// split on. Fetching every product to build a `productId in [...]` clause was
|
|
453
|
+
// both an extra round trip and a hard 30-value Firestore cap.
|
|
454
|
+
return orderRepository.listForSeller(store.id, {
|
|
464
455
|
filters: params?.filters,
|
|
465
456
|
sorts: params?.sorts ?? "-createdAt",
|
|
466
457
|
page: params?.page ?? 1,
|
|
@@ -94,6 +94,12 @@ export interface SellerProductShellProps {
|
|
|
94
94
|
listingType?: ProductListingMode;
|
|
95
95
|
initialValues?: SellerProductDraft;
|
|
96
96
|
productId?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The digital-content pool manager, injected by the consumer so appkit never
|
|
99
|
+
* hard-codes an API path. Rendered in EDIT MODE only — a pool is keyed on a
|
|
100
|
+
* product id that does not exist until the listing has been created once.
|
|
101
|
+
*/
|
|
102
|
+
renderDigitalContentPool?: (productId: string) => React.ReactNode;
|
|
97
103
|
onSave: (draft: SellerProductDraft) => Promise<ProductActionResult>;
|
|
98
104
|
onPublish: (draft: SellerProductDraft) => Promise<ProductActionResult>;
|
|
99
105
|
onDiscard?: () => void;
|
|
@@ -129,4 +135,4 @@ export interface SellerProductShellProps {
|
|
|
129
135
|
*/
|
|
130
136
|
previewSlot?: () => React.ReactNode;
|
|
131
137
|
}
|
|
132
|
-
export declare function SellerProductShell({ mode, listingType, initialValues, onSave, onPublish, onDiscard, isLoading, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, }: SellerProductShellProps): React.JSX.Element;
|
|
138
|
+
export declare function SellerProductShell({ mode, listingType, initialValues, productId, onSave, onPublish, onDiscard, isLoading, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, renderDigitalContentPool, }: SellerProductShellProps): React.JSX.Element;
|
|
@@ -351,7 +351,13 @@ const EDIT_SECTIONS = [
|
|
|
351
351
|
{ id: "publish", label: "Publish" },
|
|
352
352
|
];
|
|
353
353
|
// ── Main SellerProductShell ───────────────────────────────────────────────
|
|
354
|
-
export function SellerProductShell({ mode, listingType = "standard", initialValues,
|
|
354
|
+
export function SellerProductShell({ mode, listingType = "standard", initialValues,
|
|
355
|
+
/*
|
|
356
|
+
* `productId` was declared on the props interface and NEVER DESTRUCTURED —
|
|
357
|
+
* dead from the day it was added, because nothing in the shell needed it.
|
|
358
|
+
* The digital-content pool is the first thing that does.
|
|
359
|
+
*/
|
|
360
|
+
productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, renderDigitalContentPool, }) {
|
|
355
361
|
const [draft, setDraft] = useState(initialValues ?? { status: "draft", condition: "new" });
|
|
356
362
|
const [formMode, setFormMode] = useState(mode === "create" && listingType === "standard" ? "quick" : "full");
|
|
357
363
|
/*
|
|
@@ -583,5 +589,8 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
583
589
|
...EDIT_SECTIONS,
|
|
584
590
|
...(typeSpecificSection ? [{ id: typeSpecificSection.id, label: typeSpecificSection.label }] : []),
|
|
585
591
|
];
|
|
586
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} })
|
|
592
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} }), mode === "edit" &&
|
|
593
|
+
listingType === "digital-code" &&
|
|
594
|
+
productId &&
|
|
595
|
+
renderDigitalContentPool?.(productId)] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "returns", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Returns" }), _jsx(StepReturns, { values: draft, onChange: update })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
|
|
587
596
|
}
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* export { storeReviewsGET as GET } from "@mohasinac/feat-stores";
|
|
8
8
|
* ```
|
|
9
9
|
*
|
|
10
|
-
* Aggregates approved
|
|
10
|
+
* Aggregates every approved review filed against a store, by `storeId`.
|
|
11
11
|
* Returns: reviews[], averageRating, totalReviews, ratingDistribution.
|
|
12
12
|
*
|
|
13
|
+
* It used to aggregate "up to 20 published products by itemsSold" and fan out a
|
|
14
|
+
* review query per product — and since no product carries `itemsSold`, that
|
|
15
|
+
* `orderBy` matched nothing and the endpoint returned zero reviews for every
|
|
16
|
+
* store. See the block comment at the query for the full account.
|
|
17
|
+
*
|
|
13
18
|
* Requires `db` registered in providers.config via `registerProviders()`.
|
|
14
19
|
* Collections: "stores", "products", "reviews"
|
|
15
20
|
*/
|