@mohasinac/appkit 4.53.1 → 4.53.2
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/.build-stamp.json +3 -3
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
- package/dist/_internal/server/features/price-index/market.d.ts +57 -0
- package/dist/_internal/server/features/price-index/market.js +120 -0
- package/dist/_internal/server/features/products/list-public.d.ts +12 -0
- package/dist/_internal/server/features/products/list-public.js +10 -1
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
- package/dist/_internal/server/features/products/listing-tab-counts.js +128 -10
- package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
- package/dist/_internal/server/features/stores/tabs.js +5 -3
- package/dist/_internal/shared/search/match.js +28 -2
- package/dist/constants/api-endpoints.d.ts +7 -0
- package/dist/constants/api-endpoints.js +7 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -7
- package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
- package/dist/features/categories/components/BrandDetailPageView.js +14 -6
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +18 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +17 -7
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
- package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +10 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +10 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +8 -7
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
- package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
- package/dist/features/price-index/lookup-target.d.ts +48 -0
- package/dist/features/price-index/lookup-target.js +107 -0
- package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +2 -1
- package/dist/features/products/components/CompareOverlay.js +2 -1
- package/dist/features/products/components/GroupSettingsPanel.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +7 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -0
- package/dist/features/products/repository/products.repository.js +5 -1
- package/dist/features/products/utils/group-member-image.d.ts +9 -2
- package/dist/features/products/utils/group-member-image.js +2 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +12 -0
- package/dist/seed/products-beyblade-x-seed-data.js +36 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +8 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +133 -27
|
@@ -27,6 +27,7 @@ import { safeRead } from "../../../errors/safe-read.js";
|
|
|
27
27
|
import { breadcrumbJsonLd, collectionPageJsonLd, faqJsonLd, } from "../../../seo/json-ld.js";
|
|
28
28
|
import { toStoreListItem } from "../../../_internal/server/features/stores/adapters.js";
|
|
29
29
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
30
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
30
31
|
const __O = {
|
|
31
32
|
hidden: "overflow-hidden",
|
|
32
33
|
};
|
|
@@ -170,7 +171,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
170
171
|
// Four of the ten tabs used to have no count at all and could never hide.
|
|
171
172
|
categoriesIn
|
|
172
173
|
? listingTabCounts(CATEGORY_PAGE_TABS, { categoriesIn })
|
|
173
|
-
: Promise.resolve({}),
|
|
174
|
+
: Promise.resolve({ available: {}, total: {} }),
|
|
174
175
|
// SB-UNI-D — bundles fetched from the categories collection. We pull
|
|
175
176
|
// all active bundle rows; the carousel filters by category affinity.
|
|
176
177
|
categoriesIn
|
|
@@ -419,13 +420,22 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
419
420
|
// `tabCounts` is keyed by tabSlug and already covers every listing type plus
|
|
420
421
|
// bundles; `stores` is added here because this page resolves its store list
|
|
421
422
|
// eagerly to render the tab body anyway.
|
|
422
|
-
|
|
423
|
+
// Two maps, not one: the BADGE is what the default (available) scope will
|
|
424
|
+
// render, while HIDING must stay keyed on the total — a tab whose rows have
|
|
425
|
+
// all ended still has a browsable archive behind the Ended/All scopes.
|
|
426
|
+
// `stores` is added to both because this page resolves its store list
|
|
427
|
+
// eagerly to render the tab body anyway, and a store has no availability
|
|
428
|
+
// scope of its own.
|
|
429
|
+
const counts = { ...tabCounts.available, stores: storeCount };
|
|
430
|
+
const totals = { ...tabCounts.total, stores: storeCount };
|
|
423
431
|
// Header pills read the same numbers the tabs do. `?? 0` is safe here (a pill
|
|
424
432
|
// simply doesn't render) — unlike the tab bar, where an unknown count must
|
|
425
433
|
// stay visible rather than collapse to "hide me".
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
434
|
+
// Header pills read TOTALS, deliberately: "47 products" is a statement about
|
|
435
|
+
// what this category holds, not about what one scope is currently showing.
|
|
436
|
+
const productCount = totals.products ?? category?.metrics?.totalProductCount ?? category?.metrics?.productCount ?? 0;
|
|
437
|
+
const auctionCount = totals.auctions ?? category?.metrics?.totalAuctionCount ?? category?.metrics?.auctionCount ?? 0;
|
|
438
|
+
const preOrderCount = totals["pre-orders"] ?? 0;
|
|
429
439
|
/*
|
|
430
440
|
* G3b — OWN vs CHILDREN, so one number stops answering two questions.
|
|
431
441
|
*
|
|
@@ -465,7 +475,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
465
475
|
const mosaicImages = hasCover
|
|
466
476
|
? []
|
|
467
477
|
: (productsResult?.items ?? [])
|
|
468
|
-
.map((row) => row.mainImage
|
|
478
|
+
.map((row) => row.mainImage || productImageUrls(row.images)[0] || null)
|
|
469
479
|
.filter((src) => !!src)
|
|
470
480
|
.slice(0, 5);
|
|
471
481
|
const hasMosaic = mosaicImages.length >= MOSAIC_MIN;
|
|
@@ -587,5 +597,5 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
587
597
|
imageUrl: coverOf(b),
|
|
588
598
|
countLabel: countOf(b),
|
|
589
599
|
} }) }) }));
|
|
590
|
-
})(), directChildrenWithListings.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: _jsxs(Div, { padding: "b-2xs", children: [parentLadder.length > 0 && (_jsx(Div, { padding: "b-2xs", children: _jsx(SubcategoryCardRow, { items: parentLadder, label: "Parent category" }) })), _jsx(SubcategoryCardRow, { items: toSubcategoryItems(directChildrenWithListings, childCategories) })] }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
|
|
600
|
+
})(), directChildrenWithListings.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: _jsxs(Div, { padding: "b-2xs", children: [parentLadder.length > 0 && (_jsx(Div, { padding: "b-2xs", children: _jsx(SubcategoryCardRow, { items: parentLadder, label: "Parent category" }) })), _jsx(SubcategoryCardRow, { items: toSubcategoryItems(directChildrenWithListings, childCategories) })] }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, totals: totals, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
|
|
591
601
|
}
|
|
@@ -17,9 +17,15 @@ export interface CategoryDetailTabsProps {
|
|
|
17
17
|
* were structurally uncountable and could never hide (Root Cause #61).
|
|
18
18
|
*/
|
|
19
19
|
counts?: Record<string, number | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Every published row per tab, all availability scopes. Drives hide-at-zero
|
|
22
|
+
* and the "N ended — show all" hint; see `ListingTabCountResult`. Omitted,
|
|
23
|
+
* it falls back to `counts`, i.e. the pre-split behaviour.
|
|
24
|
+
*/
|
|
25
|
+
totals?: Record<string, number | undefined>;
|
|
20
26
|
/** Enabled listing types (e.g. ["standard","auction","pre-order"]). When omitted, all tabs shown. */
|
|
21
27
|
enabledListingTypes?: string[];
|
|
22
28
|
/** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
|
|
23
29
|
enabledCategoryTypes?: string[];
|
|
24
30
|
}
|
|
25
|
-
export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, initialStores, counts, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): React.JSX.Element;
|
|
31
|
+
export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, initialStores, counts, totals, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): React.JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { EmptyState, Tabs, TabsList, TabsTrigger } from "../../../ui/index.js";
|
|
5
|
+
import { AvailabilityScopeHint } from "../../products/components/AvailabilityScopeHint.js";
|
|
5
6
|
import { CategoryProductsListing } from "./CategoryProductsListing.js";
|
|
6
7
|
import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing.js";
|
|
7
8
|
import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing.js";
|
|
@@ -29,6 +30,21 @@ const TAB_TYPE_MAP = {
|
|
|
29
30
|
bundles: { kind: "category", type: "bundle" },
|
|
30
31
|
stores: { kind: "entity", type: "stores" },
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The listing types a tab spans, for the availability-scope hint.
|
|
35
|
+
*
|
|
36
|
+
* Derived from `TAB_TYPE_MAP` rather than hand-written: that map is already
|
|
37
|
+
* built from `ALL_LISTING_TYPES`, so a tenth type is covered without a second
|
|
38
|
+
* enumeration (Root Cause #61). The combined Art & Stickers tab is the one
|
|
39
|
+
* entry that spans two, and `GENERIC_TAB_LISTING_TYPES` already says so.
|
|
40
|
+
*/
|
|
41
|
+
function typesForTab(id) {
|
|
42
|
+
const generic = GENERIC_TAB_LISTING_TYPES[id];
|
|
43
|
+
if (generic)
|
|
44
|
+
return generic;
|
|
45
|
+
const mapping = TAB_TYPE_MAP[id];
|
|
46
|
+
return mapping?.kind === "listing" ? [mapping.type] : [];
|
|
47
|
+
}
|
|
32
48
|
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
33
49
|
const GENERIC_TAB_LISTING_TYPES = {
|
|
34
50
|
[pluginFor("standard").tabSlug]: ["standard"],
|
|
@@ -50,8 +66,12 @@ const GENERIC_TAB_LISTING_TYPES = {
|
|
|
50
66
|
*/
|
|
51
67
|
[pluginFor("spares").tabSlug]: ["spares"],
|
|
52
68
|
};
|
|
53
|
-
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
69
|
+
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, totals, enabledListingTypes, enabledCategoryTypes, }) {
|
|
54
70
|
const countFor = (id) => counts?.[id];
|
|
71
|
+
// Falls back to the badge when no totals map is supplied, so a caller that
|
|
72
|
+
// has not been migrated keeps today's hide behaviour rather than hiding
|
|
73
|
+
// everything.
|
|
74
|
+
const totalFor = (id) => totals?.[id] ?? counts?.[id];
|
|
55
75
|
const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
|
|
56
76
|
const mapping = TAB_TYPE_MAP[t.id];
|
|
57
77
|
if (mapping) {
|
|
@@ -64,11 +84,13 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
|
|
|
64
84
|
return false;
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
87
|
+
// 🛑 TOTAL, not the badge. The badge counts what the default (available)
|
|
88
|
+
// scope renders, and a tab whose rows have all ended badges 0 while still
|
|
89
|
+
// holding a browsable archive behind the Ended/All scopes — hiding it
|
|
90
|
+
// would strand that archive. `undefined` still means the count query
|
|
91
|
+
// FAILED, so the tab stays visible: an empty tab is a far smaller error
|
|
92
|
+
// than hiding a tab that holds real listings (Root Cause #59).
|
|
93
|
+
const count = totalFor(t.id);
|
|
72
94
|
return count === undefined || count > 0;
|
|
73
95
|
});
|
|
74
96
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
@@ -78,5 +100,5 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
|
|
|
78
100
|
if (visibleTabs.length === 0) {
|
|
79
101
|
return (_jsx(EmptyState, { title: "Nothing listed here yet", description: "This category has no active listings right now. Check back soon." }));
|
|
80
102
|
}
|
|
81
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
|
|
103
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), _jsx(AvailabilityScopeHint, { total: totalFor(activeTab), available: countFor(activeTab), types: typesForTab(activeTab) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
|
|
82
104
|
}
|
|
@@ -23,6 +23,7 @@ import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal.j
|
|
|
23
23
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
24
24
|
import { DEFAULT_MIN_OFFER_PERCENT, offersEnabledFor, resolveOfferBounds, } from "../../../_internal/shared/features/offers/config.js";
|
|
25
25
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
26
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
26
27
|
function toDescriptionHtml(raw) {
|
|
27
28
|
if (!raw)
|
|
28
29
|
return "";
|
|
@@ -43,7 +44,15 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
|
|
|
43
44
|
const currency = p.currency || getDefaultCurrency();
|
|
44
45
|
const title = String(p.title ?? p.name ?? "Classified Listing");
|
|
45
46
|
const price = typeof p.price === "number" ? p.price : null;
|
|
46
|
-
|
|
47
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
48
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
49
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
50
|
+
const galleryUrls = productImageUrls(p.images);
|
|
51
|
+
const images = galleryUrls.length > 0
|
|
52
|
+
? galleryUrls
|
|
53
|
+
: typeof p.mainImage === "string"
|
|
54
|
+
? [p.mainImage]
|
|
55
|
+
: [];
|
|
47
56
|
const meta = product.classified;
|
|
48
57
|
const location = meta?.meetupArea ? [meta.meetupArea.locality, meta.meetupArea.city].filter(Boolean).join(", ") : null;
|
|
49
58
|
const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? String(p.categorySlugs[0]) : (typeof p.category === "string" ? p.category : null);
|
|
@@ -21,6 +21,7 @@ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListing
|
|
|
21
21
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data.js";
|
|
22
22
|
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel.js";
|
|
23
23
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
24
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
24
25
|
function toDescriptionHtml(raw) {
|
|
25
26
|
if (!raw)
|
|
26
27
|
return "";
|
|
@@ -41,7 +42,15 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
41
42
|
const currency = p.currency || getDefaultCurrency();
|
|
42
43
|
const title = String(p.title ?? p.name ?? "Digital Code");
|
|
43
44
|
const price = typeof p.price === "number" ? p.price : null;
|
|
44
|
-
|
|
45
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
46
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
47
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
48
|
+
const galleryUrls = productImageUrls(p.images);
|
|
49
|
+
const images = galleryUrls.length > 0
|
|
50
|
+
? galleryUrls
|
|
51
|
+
: typeof p.mainImage === "string"
|
|
52
|
+
? [p.mainImage]
|
|
53
|
+
: [];
|
|
45
54
|
const meta = product.digitalCode;
|
|
46
55
|
const codesLeft = meta?.codesAvailable ?? 0;
|
|
47
56
|
const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? String(p.categorySlugs[0]) : (typeof p.category === "string" ? p.category : null);
|
|
@@ -24,6 +24,7 @@ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListing
|
|
|
24
24
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data.js";
|
|
25
25
|
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel.js";
|
|
26
26
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
27
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
27
28
|
function toDescriptionHtml(raw) {
|
|
28
29
|
if (!raw)
|
|
29
30
|
return "";
|
|
@@ -44,7 +45,15 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
44
45
|
const currency = p.currency || getDefaultCurrency();
|
|
45
46
|
const title = String(p.title ?? p.name ?? "Live Listing");
|
|
46
47
|
const price = typeof p.price === "number" ? p.price : null;
|
|
47
|
-
|
|
48
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
49
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
50
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
51
|
+
const galleryUrls = productImageUrls(p.images);
|
|
52
|
+
const images = galleryUrls.length > 0
|
|
53
|
+
? galleryUrls
|
|
54
|
+
: typeof p.mainImage === "string"
|
|
55
|
+
? [p.mainImage]
|
|
56
|
+
: [];
|
|
48
57
|
const productVideo = p.video;
|
|
49
58
|
const meta = product.liveItem;
|
|
50
59
|
const jurisdictions = meta?.jurisdictionAllowed ?? [];
|
|
@@ -27,6 +27,7 @@ import { ProductGalleryClient } from "../../products/components/ProductGalleryCl
|
|
|
27
27
|
import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges.js";
|
|
28
28
|
import { isFinalSale } from "../../products/constants/final-sale.js";
|
|
29
29
|
import { FeatureBadgeList } from "../../products/components/FeatureBadge.js";
|
|
30
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
30
31
|
import { ShareButton } from "../../products/components/ShareButton.js";
|
|
31
32
|
import { SublistingCarouselSection } from "../../products/components/SublistingCarouselSection.js";
|
|
32
33
|
import { HistoryTracker } from "../../history/components/HistoryTracker.js";
|
|
@@ -86,8 +87,12 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
|
|
|
86
87
|
const currency = p.currency || getDefaultCurrency();
|
|
87
88
|
const title = String(p.title ?? p.name ?? "Pre-Order Item");
|
|
88
89
|
const price = typeof p.price === "number" ? p.price : null;
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
91
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
92
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
93
|
+
const galleryUrls = productImageUrls(p.images);
|
|
94
|
+
const images = galleryUrls.length > 0
|
|
95
|
+
? galleryUrls
|
|
91
96
|
: typeof p.mainImage === "string"
|
|
92
97
|
? [p.mainImage]
|
|
93
98
|
: [];
|
|
@@ -153,11 +158,7 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
|
|
|
153
158
|
]);
|
|
154
159
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "preorder", snapshot: {
|
|
155
160
|
title: typeof p.title === "string" ? p.title : undefined,
|
|
156
|
-
thumb: typeof p.mainImage === "string"
|
|
157
|
-
? p.mainImage
|
|
158
|
-
: Array.isArray(p.images) && typeof p.images[0] === "string"
|
|
159
|
-
? p.images[0]
|
|
160
|
-
: undefined,
|
|
161
|
+
thumb: typeof p.mainImage === "string" ? p.mainImage : images[0],
|
|
161
162
|
price: typeof p.price === "number" ? p.price : undefined,
|
|
162
163
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
163
164
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
@@ -79,6 +79,27 @@ export interface PriceIndexToolResponse {
|
|
|
79
79
|
truncated: boolean;
|
|
80
80
|
minConfidentSamples: number;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The competitor-crawl band for one leaf. Mirrors `PriceIndexMarketRow`.
|
|
84
|
+
*
|
|
85
|
+
* Every field but `median` and `categoryId` is optional because the crawl
|
|
86
|
+
* genuinely did not measure them everywhere, and an absent figure must render
|
|
87
|
+
* as absent. See `price-bands.md`: "a band with `n=2` is not evidence".
|
|
88
|
+
*/
|
|
89
|
+
export interface PriceIndexMarketRowView {
|
|
90
|
+
categoryId: string;
|
|
91
|
+
median: number;
|
|
92
|
+
p25?: number;
|
|
93
|
+
p75?: number;
|
|
94
|
+
sampleSize?: number;
|
|
95
|
+
asOf?: string;
|
|
96
|
+
soldOutShare?: number;
|
|
97
|
+
source?: string;
|
|
98
|
+
inherited: boolean;
|
|
99
|
+
viaCategoryId?: string;
|
|
100
|
+
viaName?: string;
|
|
101
|
+
taxBasis: string;
|
|
102
|
+
}
|
|
82
103
|
export interface PriceIndexToolViewProps {
|
|
83
104
|
/** Seeded from `?q=` so a shared link reproduces the lookup. */
|
|
84
105
|
initialQuery?: string;
|