@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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { withoutPriceSorts } from "../../products/constants/sieve";
|
|
4
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
3
5
|
import { useState, useCallback } from "react";
|
|
4
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
7
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
@@ -32,6 +34,7 @@ function renderCategoryProductGrid(props) {
|
|
|
32
34
|
}
|
|
33
35
|
export function CategoryProductsListing({ categorySlug, categoryId, brandName, listingTypes = ["standard"], initialData, }) {
|
|
34
36
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
|
|
37
|
+
const { canSeePrices } = useCanSeePrices();
|
|
35
38
|
const { showToast } = useToast();
|
|
36
39
|
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
37
40
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
@@ -114,7 +117,7 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, l
|
|
|
114
117
|
pushCartOp({ op: "add", productId: product.id, quantity: 1, ...snapshot });
|
|
115
118
|
showToast("Added to cart", "success");
|
|
116
119
|
}, [localCart, showToast]);
|
|
117
|
-
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => {
|
|
120
|
+
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: withoutPriceSorts(PRODUCT_PUBLIC_SORT_OPTIONS, canSeePrices), onSortChange: (v) => {
|
|
118
121
|
table.set("sort", v);
|
|
119
122
|
}, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(Div, { padding: "y-sm", children: _jsx(AvailabilityTabs, { types: listingTypes }) }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryProductGrid({
|
|
120
123
|
isLoading, products: products, view,
|
|
@@ -20,7 +20,7 @@ function CategoryTreeNode({ item, children = [], activeId, onSelect, depth = 0,
|
|
|
20
20
|
if (hasChildren)
|
|
21
21
|
setExpanded((p) => !p);
|
|
22
22
|
onSelect?.(item);
|
|
23
|
-
}, className: `cursor-pointer py-[var(--appkit-space-1-5)] transition select-none ${depthPl} ${isActive ? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400" : " hover:bg-surface-hover text-[var(--appkit-color-text-muted)]"}`, align: "center", gap: "xs", padding: "x-xs", rounded: "md", children: [hasChildren && (_jsx(Span, { size: "xs", "aria-hidden": "true", className: `transition-transform duration-150 ${expanded ? "rotate-90" : ""}`, color: "faint", children: "\u25B6" })), !hasChildren && _jsx(Span, { className: "w-3" }), item.display?.icon && (_jsx(Span, { size: "base", "aria-hidden": "true", className: "flex-shrink-0", children: item.display.icon })), _jsx(Text, { className: "flex-1 truncate", children: item.name }), item.metrics && (_jsx(Span, { size: "xs", className: "ml-auto flex-shrink-0 tabular-nums", color: "faint", children: item.metrics.productCount }))] }), hasChildren && expanded && (_jsx(Div, { role: "group", children: children.map((child) => (_jsx(CategoryTreeNode, { item: child, activeId: activeId, onSelect: onSelect, depth: depth + 1 }, child.id))) }))] }));
|
|
23
|
+
}, className: `cursor-pointer py-[var(--appkit-space-1-5)] transition select-none ${depthPl} ${isActive ? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400" : " hover:bg-surface-hover text-[var(--appkit-color-text-muted)]"}`, align: "center", gap: "xs", padding: "x-xs", rounded: "md", children: [hasChildren && (_jsx(Span, { size: "xs", "aria-hidden": "true", className: `transition-transform duration-150 ${expanded ? "rotate-90" : ""}`, color: "faint", children: "\u25B6" })), !hasChildren && _jsx(Span, { className: "w-3" }), item.display?.icon && (_jsx(Span, { size: "base", "aria-hidden": "true", className: "flex-shrink-0", children: item.display.icon })), _jsx(Text, { className: "flex-1 truncate", children: item.name }), item.metrics && (_jsx(Span, { size: "xs", className: "ml-auto flex-shrink-0 tabular-nums", color: "faint", children: item.metrics.totalProductCount ?? item.metrics.productCount }))] }), hasChildren && expanded && (_jsx(Div, { role: "group", children: children.map((child) => (_jsx(CategoryTreeNode, { item: child, activeId: activeId, onSelect: onSelect, depth: depth + 1 }, child.id))) }))] }));
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* CategoryTree — hierarchical expandable navigation for categories.
|
|
@@ -32,9 +32,41 @@ export declare class CategoriesRepository extends BaseRepository<CategoryDocumen
|
|
|
32
32
|
*/
|
|
33
33
|
updateMetricsInBatch(batch: import("firebase-admin/firestore").WriteBatch, categoryId: string, parentIds: string[], productDelta: number, auctionDelta: number, productId?: string): void;
|
|
34
34
|
/**
|
|
35
|
-
* Cloud Functions: full-overwrite category metrics (
|
|
35
|
+
* Cloud Functions: full-overwrite category metrics (nightly reconciliation).
|
|
36
|
+
*
|
|
37
|
+
* 🛑 THE OWN COUNT AND THE ROLLUP ARE SEPARATE ARGUMENTS, and they must be.
|
|
38
|
+
*
|
|
39
|
+
* This method used to take ONE pair of numbers and write it to both
|
|
40
|
+
* `productCount` and `totalProductCount` — so it could not express the
|
|
41
|
+
* distinction the live trigger (`updateMetricsInBatch`, just above) carefully
|
|
42
|
+
* maintains: `productCount` is the category's OWN items, `totalProductCount`
|
|
43
|
+
* is own + every descendant's. The nightly job therefore overwrote correct
|
|
44
|
+
* values with wrong ones every night:
|
|
45
|
+
*
|
|
46
|
+
* - on a leaf it was accidentally right (a leaf has no descendants);
|
|
47
|
+
* - on an ancestor it wrote the DESCENDANT sum into both, so a category
|
|
48
|
+
* with 5 of its own and 15 below reported `productCount: 15` (should be
|
|
49
|
+
* 5) and `totalProductCount: 15` (should be 20 — its own 5 were never
|
|
50
|
+
* added, because the caller's aggregate only walked other rows' leaves);
|
|
51
|
+
* - on a row that is both a leaf and an ancestor, `setMetrics` was called
|
|
52
|
+
* twice and last-write-won.
|
|
53
|
+
*
|
|
54
|
+
* Measured on production 2026-09-14 before the fix: **19 of 65 category rows
|
|
55
|
+
* disagreed with a recount**, every one of them low — the root read 56 against
|
|
56
|
+
* a true 65. Nothing errored, and no page looked broken; a wrong number is
|
|
57
|
+
* simply a number.
|
|
36
58
|
*/
|
|
37
|
-
setMetrics(categoryId: string,
|
|
59
|
+
setMetrics(categoryId: string, m: {
|
|
60
|
+
/** Items filed DIRECTLY under this category. */
|
|
61
|
+
productCount: number;
|
|
62
|
+
auctionCount: number;
|
|
63
|
+
/** Own + every descendant's. Equals the own count on a leaf. */
|
|
64
|
+
totalProductCount: number;
|
|
65
|
+
totalAuctionCount: number;
|
|
66
|
+
/** Own only — the ids are used for spot-checking, never for the rollup. */
|
|
67
|
+
productIds: string[];
|
|
68
|
+
auctionIds: string[];
|
|
69
|
+
}): Promise<void>;
|
|
38
70
|
/** SB-UNI — discriminator-keyed listing (sublistings, brands, bundles). */
|
|
39
71
|
listByType(type: import("../types").CategoryType, opts?: {
|
|
40
72
|
limit?: number;
|
|
@@ -382,20 +382,42 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
/**
|
|
385
|
-
* Cloud Functions: full-overwrite category metrics (
|
|
385
|
+
* Cloud Functions: full-overwrite category metrics (nightly reconciliation).
|
|
386
|
+
*
|
|
387
|
+
* 🛑 THE OWN COUNT AND THE ROLLUP ARE SEPARATE ARGUMENTS, and they must be.
|
|
388
|
+
*
|
|
389
|
+
* This method used to take ONE pair of numbers and write it to both
|
|
390
|
+
* `productCount` and `totalProductCount` — so it could not express the
|
|
391
|
+
* distinction the live trigger (`updateMetricsInBatch`, just above) carefully
|
|
392
|
+
* maintains: `productCount` is the category's OWN items, `totalProductCount`
|
|
393
|
+
* is own + every descendant's. The nightly job therefore overwrote correct
|
|
394
|
+
* values with wrong ones every night:
|
|
395
|
+
*
|
|
396
|
+
* - on a leaf it was accidentally right (a leaf has no descendants);
|
|
397
|
+
* - on an ancestor it wrote the DESCENDANT sum into both, so a category
|
|
398
|
+
* with 5 of its own and 15 below reported `productCount: 15` (should be
|
|
399
|
+
* 5) and `totalProductCount: 15` (should be 20 — its own 5 were never
|
|
400
|
+
* added, because the caller's aggregate only walked other rows' leaves);
|
|
401
|
+
* - on a row that is both a leaf and an ancestor, `setMetrics` was called
|
|
402
|
+
* twice and last-write-won.
|
|
403
|
+
*
|
|
404
|
+
* Measured on production 2026-09-14 before the fix: **19 of 65 category rows
|
|
405
|
+
* disagreed with a recount**, every one of them low — the root read 56 against
|
|
406
|
+
* a true 65. Nothing errored, and no page looked broken; a wrong number is
|
|
407
|
+
* simply a number.
|
|
386
408
|
*/
|
|
387
|
-
async setMetrics(categoryId,
|
|
409
|
+
async setMetrics(categoryId, m) {
|
|
388
410
|
await this.db
|
|
389
411
|
.collection(this.collection)
|
|
390
412
|
.doc(categoryId)
|
|
391
413
|
.update({
|
|
392
|
-
"metrics.productCount": productCount,
|
|
393
|
-
"metrics.auctionCount": auctionCount,
|
|
394
|
-
"metrics.totalProductCount":
|
|
395
|
-
"metrics.totalAuctionCount":
|
|
396
|
-
"metrics.totalItemCount":
|
|
397
|
-
"metrics.productIds": productIds,
|
|
398
|
-
"metrics.auctionIds": auctionIds,
|
|
414
|
+
"metrics.productCount": m.productCount,
|
|
415
|
+
"metrics.auctionCount": m.auctionCount,
|
|
416
|
+
"metrics.totalProductCount": m.totalProductCount,
|
|
417
|
+
"metrics.totalAuctionCount": m.totalAuctionCount,
|
|
418
|
+
"metrics.totalItemCount": m.totalProductCount + m.totalAuctionCount,
|
|
419
|
+
"metrics.productIds": m.productIds,
|
|
420
|
+
"metrics.auctionIds": m.auctionIds,
|
|
399
421
|
"metrics.lastUpdated": new Date(),
|
|
400
422
|
updatedAt: new Date(),
|
|
401
423
|
});
|
|
@@ -8,7 +8,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
9
9
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
10
10
|
import { safeDisplayName } from "../../../security";
|
|
11
|
-
import { Container, Div, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
|
|
11
|
+
import { Container, Div, GatedPrice, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
|
|
12
12
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
|
|
13
13
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
|
|
14
14
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
@@ -94,7 +94,7 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
|
|
|
94
94
|
price: price ?? undefined,
|
|
95
95
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
96
96
|
storeName: storeName ?? undefined,
|
|
97
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLASSIFIED), className: CLS_BREADCRUMB_LINK, children: "Classifieds" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_CLASSIFIED_BADGE, children: "Classified" }), meta?.negotiable && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Negotiable" })), meta?.acceptsShipping && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Shipping available" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), location && (_jsxs(Row, { color: "muted", textSize: "sm", align: "center", gap: "xs", children: [_jsx(Span, { children: "\uD83D\uDCCD" }), _jsx(Span, { children: location }), meta?.meetupArea?.pincode && _jsxs(Span, { children: ["\u00B7 ", meta.meetupArea.pincode] })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
97
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLASSIFIED), className: CLS_BREADCRUMB_LINK, children: "Classifieds" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_CLASSIFIED_BADGE, children: "Classified" }), meta?.negotiable && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Negotiable" })), meta?.acceptsShipping && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Shipping available" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), location && (_jsxs(Row, { color: "muted", textSize: "sm", align: "center", gap: "xs", children: [_jsx(Span, { children: "\uD83D\uDCCD" }), _jsx(Span, { children: location }), meta?.meetupArea?.pincode && _jsxs(Span, { children: ["\u00B7 ", meta.meetupArea.pincode] })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
98
98
|
id: s.id,
|
|
99
99
|
label: s.title,
|
|
100
100
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
@@ -13,6 +13,7 @@ function renderToStaticMarkup(el) {
|
|
|
13
13
|
}
|
|
14
14
|
import { normalizeError } from "../../errors/normalize";
|
|
15
15
|
import { guardSend, logSuppressedSend } from "../../_internal/server/notifications/send-guard";
|
|
16
|
+
import { recordSendAttempt } from "../../_internal/server/notifications/send-recorder";
|
|
16
17
|
import { getProviders } from "../../contracts";
|
|
17
18
|
import { createResendProvider } from "../../providers/email-resend/provider";
|
|
18
19
|
import { siteSettingsRepository } from "../admin/repository/site-settings.repository";
|
|
@@ -94,6 +95,21 @@ export async function sendEmail(opts, guard) {
|
|
|
94
95
|
const decision = await guardSend(guard);
|
|
95
96
|
if (!decision.allow) {
|
|
96
97
|
logSuppressedSend(guard, decision);
|
|
98
|
+
/*
|
|
99
|
+
* Record the SUPPRESSION, not just the sends. Six checklist cases assert that
|
|
100
|
+
* an email should NOT arrive, and over IMAP that is unprovable — you wait and
|
|
101
|
+
* hope. Here it is a row saying which rule stopped it. Gated to the harness
|
|
102
|
+
* mailbox; a no-op for everyone else. See send-recorder.ts.
|
|
103
|
+
*/
|
|
104
|
+
void recordSendAttempt({
|
|
105
|
+
to: opts.to,
|
|
106
|
+
subject: typeof opts.subject === "string" ? opts.subject : "",
|
|
107
|
+
feature: guard.feature,
|
|
108
|
+
audience: String(guard.audience),
|
|
109
|
+
channel: String(guard.channel),
|
|
110
|
+
outcome: "suppressed",
|
|
111
|
+
reason: decision.reason,
|
|
112
|
+
});
|
|
97
113
|
return { data: null, error: null, suppressed: decision.reason };
|
|
98
114
|
}
|
|
99
115
|
try {
|
|
@@ -112,11 +128,30 @@ export async function sendEmail(opts, guard) {
|
|
|
112
128
|
? opts.headers
|
|
113
129
|
: undefined,
|
|
114
130
|
});
|
|
131
|
+
void recordSendAttempt({
|
|
132
|
+
to: opts.to,
|
|
133
|
+
subject: typeof opts.subject === "string" ? opts.subject : "",
|
|
134
|
+
feature: guard.feature,
|
|
135
|
+
audience: String(guard.audience),
|
|
136
|
+
channel: String(guard.channel),
|
|
137
|
+
outcome: "sent",
|
|
138
|
+
html: typeof opts.html === "string" ? opts.html : undefined,
|
|
139
|
+
});
|
|
115
140
|
return { data: data, error: null };
|
|
116
141
|
}
|
|
117
142
|
catch (error) {
|
|
118
143
|
void normalizeError(error);
|
|
119
|
-
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
void recordSendAttempt({
|
|
146
|
+
to: opts.to,
|
|
147
|
+
subject: typeof opts.subject === "string" ? opts.subject : "",
|
|
148
|
+
feature: guard.feature,
|
|
149
|
+
audience: String(guard.audience),
|
|
150
|
+
channel: String(guard.channel),
|
|
151
|
+
outcome: "failed",
|
|
152
|
+
reason: message,
|
|
153
|
+
});
|
|
154
|
+
return { data: null, error: message };
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
async function sendConfiguredEmail(opts, guard) {
|
|
@@ -8,7 +8,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
9
9
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
10
10
|
import { safeDisplayName } from "../../../security";
|
|
11
|
-
import { Div, Container, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
|
|
11
|
+
import { Div, Container, GatedPrice, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
|
|
12
12
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
|
|
13
13
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
|
|
14
14
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
@@ -65,7 +65,7 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
65
65
|
price: price ?? undefined,
|
|
66
66
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
67
67
|
storeName: storeName ?? undefined,
|
|
68
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
68
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
69
69
|
id: s.id,
|
|
70
70
|
label: s.title,
|
|
71
71
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"use client";
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import Link from "next/link";
|
|
12
|
-
import { Badge, Div, Grid, Heading, Row, Section, Stack, Text } from "../../../ui";
|
|
12
|
+
import { Badge, Div, GatedPrice, Grid, Heading, PricesOnly, Row, Section, Stack, Text } from "../../../ui";
|
|
13
13
|
import { MediaImage } from "../../media/MediaImage";
|
|
14
14
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
15
15
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -38,7 +38,7 @@ function FeaturedBundleCard({ bundle, showSavingsBadge = true, onBuyNow, }) {
|
|
|
38
38
|
const cover = bundle.display?.coverImage;
|
|
39
39
|
const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
|
|
40
40
|
const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
|
|
41
|
-
return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), showSavingsBadge && discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePrice
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), showSavingsBadge && discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: _jsx(GatedPrice, { label: "Sign in to see the bundle price", children: bundle.bundlePrice
|
|
42
|
+
? formatCurrency(bundle.bundlePrice, "INR")
|
|
43
|
+
: BUNDLE_COPY.featured.priceFallback }) }), discount && (_jsx(PricesOnly, { children: _jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, "INR") }) })), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }), onBuyNow && (_jsx(Div, { border: "subtle", className: `border-t ${__P.p3}`, padding: "t-xs", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
|
|
44
44
|
}
|
|
@@ -11,7 +11,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
|
11
11
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
12
12
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
13
13
|
import { safeDisplayName } from "../../../security";
|
|
14
|
-
import { Div, Container, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
|
|
14
|
+
import { Div, Container, GatedPrice, PricesOnly, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
|
|
15
15
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
|
|
16
16
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
|
|
17
17
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
@@ -70,7 +70,7 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
70
70
|
price: price ?? undefined,
|
|
71
71
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
72
72
|
storeName: storeName ?? undefined,
|
|
73
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}
|
|
73
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && (_jsx(PricesOnly, { children: ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}` })), transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
74
74
|
id: s.id,
|
|
75
75
|
label: s.title,
|
|
76
76
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
@@ -162,7 +162,27 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
162
162
|
canSort: boolean;
|
|
163
163
|
};
|
|
164
164
|
};
|
|
165
|
-
|
|
165
|
+
/**
|
|
166
|
+
* A store's orders.
|
|
167
|
+
*
|
|
168
|
+
* 🛑 SCOPED BY `storeId`, NOT BY A LIST OF PRODUCT IDS.
|
|
169
|
+
*
|
|
170
|
+
* It used to take the seller's whole product list and run
|
|
171
|
+
* `.where(productId, "in", productIds)`. **Firestore caps `in` at 30 values**
|
|
172
|
+
* and nothing bounded that array, so the query threw `INVALID_ARGUMENT` and
|
|
173
|
+
* the route 500'd for any store past its 30th listing. Measured on production
|
|
174
|
+
* 2026-09-14: `store-beyblade-arena` has **65** products, so
|
|
175
|
+
* `GET /api/store/orders` — the seller's own order list — was permanently
|
|
176
|
+
* broken for the only real seller on the site, while the two small stores
|
|
177
|
+
* worked fine. A seller with 29 listings would never have reported it.
|
|
178
|
+
*
|
|
179
|
+
* `storeId` is the right key regardless of the cap: it is the field a cart is
|
|
180
|
+
* SPLIT on (one order per store), `findFulfillmentQueue` a few lines up
|
|
181
|
+
* already queries it, it is a registered sieve field, and 64/64 production
|
|
182
|
+
* orders carry it. It also deletes an entire products query per request from
|
|
183
|
+
* all four call sites — the route already resolved the store.
|
|
184
|
+
*/
|
|
185
|
+
listForSeller(storeId: string, model: SieveModel, opts?: {
|
|
166
186
|
search?: string;
|
|
167
187
|
}): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
168
188
|
listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
@@ -269,13 +269,33 @@ class OrderRepository extends BaseRepository {
|
|
|
269
269
|
throw new DatabaseError(`Failed to delete orders for user: ${userId}`, error);
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
-
|
|
272
|
+
/**
|
|
273
|
+
* A store's orders.
|
|
274
|
+
*
|
|
275
|
+
* 🛑 SCOPED BY `storeId`, NOT BY A LIST OF PRODUCT IDS.
|
|
276
|
+
*
|
|
277
|
+
* It used to take the seller's whole product list and run
|
|
278
|
+
* `.where(productId, "in", productIds)`. **Firestore caps `in` at 30 values**
|
|
279
|
+
* and nothing bounded that array, so the query threw `INVALID_ARGUMENT` and
|
|
280
|
+
* the route 500'd for any store past its 30th listing. Measured on production
|
|
281
|
+
* 2026-09-14: `store-beyblade-arena` has **65** products, so
|
|
282
|
+
* `GET /api/store/orders` — the seller's own order list — was permanently
|
|
283
|
+
* broken for the only real seller on the site, while the two small stores
|
|
284
|
+
* worked fine. A seller with 29 listings would never have reported it.
|
|
285
|
+
*
|
|
286
|
+
* `storeId` is the right key regardless of the cap: it is the field a cart is
|
|
287
|
+
* SPLIT on (one order per store), `findFulfillmentQueue` a few lines up
|
|
288
|
+
* already queries it, it is a registered sieve field, and 64/64 production
|
|
289
|
+
* orders carry it. It also deletes an entire products query per request from
|
|
290
|
+
* all four call sites — the route already resolved the store.
|
|
291
|
+
*/
|
|
292
|
+
async listForSeller(storeId, model, opts) {
|
|
273
293
|
const plan = planSearchTxt(opts?.search);
|
|
274
294
|
// A search that yielded no usable term must return NOTHING, not the
|
|
275
295
|
// seller's whole order list.
|
|
276
296
|
if (plan.empty)
|
|
277
297
|
return emptySearchResult();
|
|
278
|
-
if (
|
|
298
|
+
if (!storeId) {
|
|
279
299
|
const page = Math.max(1, Number(model.page ?? 1));
|
|
280
300
|
const pageSize = Math.max(1, Number(model.pageSize ?? 20));
|
|
281
301
|
return {
|
|
@@ -287,14 +307,11 @@ class OrderRepository extends BaseRepository {
|
|
|
287
307
|
hasMore: false,
|
|
288
308
|
};
|
|
289
309
|
}
|
|
290
|
-
let baseQuery = this.getCollection().where(ORDER_FIELDS.
|
|
310
|
+
let baseQuery = this.getCollection().where(ORDER_FIELDS.STORE_ID, "==", storeId);
|
|
291
311
|
if (plan.head) {
|
|
292
|
-
// `array-contains` alongside
|
|
293
|
-
// limits are on `array-contains` and on the
|
|
294
|
-
// `array-contains-any` family separately
|
|
295
|
-
// Verified against production data before this shipped, and the
|
|
296
|
-
// matching composite index is declared in firestore.indexes.json;
|
|
297
|
-
// the query happened to plan without one, which is not a guarantee.
|
|
312
|
+
// One `array-contains` alongside one equality is unconstrained —
|
|
313
|
+
// Firestore's per-query limits are on `array-contains` and on the
|
|
314
|
+
// `in`/`not-in`/`array-contains-any` family separately.
|
|
298
315
|
baseQuery = baseQuery.where(ORDER_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
299
316
|
}
|
|
300
317
|
const result = await this.sieveQuery(model, OrderRepository.SELLER_SIEVE_FIELDS, {
|
|
@@ -14,7 +14,7 @@ const CLS_PREORDER_BADGE = "inline-flex items-center rounded-full bg-warning-sol
|
|
|
14
14
|
const CLS_SALE_BADGE = "rounded-full bg-success-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-success-on-solid";
|
|
15
15
|
const CLS_TRENDING_BADGE = "rounded-full bg-indigo-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
|
|
16
16
|
const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-info-on-solid";
|
|
17
|
-
import { BaseListingCard, Button, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
17
|
+
import { BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
18
18
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
19
19
|
import { PreorderBadge } from "./PreorderCard";
|
|
20
20
|
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
|
|
@@ -66,7 +66,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
66
66
|
onSelect(product.id, !isSelected);
|
|
67
67
|
}, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
|
|
68
68
|
/* ── Compact list layout ── */
|
|
69
|
-
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
69
|
+
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
70
70
|
/* ── Full grid layout ── */
|
|
71
|
-
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
71
|
+
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
72
72
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
-
import { Button, Div, LoginRequiredModal, Row, Span, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Button, Div, GatedPrice, LoginRequiredModal, PricesOnly, Row, Span, Stack, Text } from "../../../ui";
|
|
5
5
|
import { isAuthError } from "../../../utils/auth-error";
|
|
6
6
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
7
7
|
import { normalizeError } from "../../../errors/normalize";
|
|
@@ -29,7 +29,7 @@ export function PreOrderActionsClient({ productId, price, currency, depositAmoun
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
return (_jsxs(Stack, { gap: "md", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-error", size: "xs", children: error })), success && (_jsx(Text, { className: "text-success", size: "xs", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reserve this pre-order. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "start", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", className: "", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { className: "text-center", justify: "center", gap: "md", wrap: true, children: [
|
|
32
|
+
return (_jsxs(Stack, { gap: "md", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) }), depositAmount !== null && (_jsx(PricesOnly, { children: _jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }) }))] })), error && (_jsx(Text, { className: "text-error", size: "xs", children: error })), success && (_jsx(Text, { className: "text-success", size: "xs", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reserve this pre-order. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "start", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", className: "", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { className: "text-center", justify: "center", gap: "md", wrap: true, children: [
|
|
33
33
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
34
34
|
{ icon: "📅", label: "Guaranteed\nDelivery" },
|
|
35
35
|
{ icon: "↩", label: "Free\nCancellation" },
|
|
@@ -17,7 +17,7 @@ import { formatCurrency, roundRupees } from "../../../utils/number.formatter";
|
|
|
17
17
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
18
18
|
import { safeDisplayName } from "../../../security";
|
|
19
19
|
import { ProgressBarFill } from "../../../ui/components/ProgressBarFill";
|
|
20
|
-
import { Button, Container, Div, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, Dl, Dt, Dd, } from "../../../ui";
|
|
20
|
+
import { Button, Container, Div, GatedPrice, PricesOnly, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, Dl, Dt, Dd, } from "../../../ui";
|
|
21
21
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
|
|
22
22
|
import { PreOrderBottomActions } from "./PreOrderBottomActions";
|
|
23
23
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
@@ -66,7 +66,7 @@ function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate
|
|
|
66
66
|
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category })), !category && categoryName && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this product" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }));
|
|
67
67
|
}
|
|
68
68
|
function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, productId, price, currency, depositAmount, depositPercent, isCancellable, tags, onReserveNow, }) {
|
|
69
|
-
return (_jsxs(Stack, { id: "pre-order-buy-bar", className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [reserveTarget > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { size: "xs", color: "muted", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: `h-2 w-full ${__O.hidden}`, rounded: "full", surface: "subtle", children: _jsx(ProgressBarFill, { pct: progressPct, className: "h-full bg-primary transition-all rounded-full" }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: productId, price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "start", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
|
|
69
|
+
return (_jsxs(Stack, { id: "pre-order-buy-bar", className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [reserveTarget > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { size: "xs", color: "muted", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: `h-2 w-full ${__O.hidden}`, rounded: "full", surface: "subtle", children: _jsx(ProgressBarFill, { pct: progressPct, className: "h-full bg-primary transition-all rounded-full" }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: productId, price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) }), depositAmount !== null && (_jsx(PricesOnly, { children: _jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }) }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "start", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
|
|
70
70
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
71
71
|
{ icon: "📅", label: "Guaranteed\nDelivery" },
|
|
72
72
|
{ icon: "↩", label: "Free\nCancellation" },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { withoutPriceSorts } from "../../products/constants/sieve";
|
|
4
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
3
5
|
import { useState, useCallback } from "react";
|
|
4
6
|
import { Columns, Heart, ShoppingCart } from "lucide-react";
|
|
5
7
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
@@ -34,6 +36,7 @@ const DEFAULT_SORT = PREORDER_SORT_OPTIONS[2].value;
|
|
|
34
36
|
const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_PRICE, TABLE_KEYS.MAX_PRICE, TABLE_KEYS.STORE_ID, TABLE_KEYS.PREORDER_STATUS, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
|
|
35
37
|
export function PreOrdersIndexListing({ initialData, categorySlug, brandName }) {
|
|
36
38
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
39
|
+
const { canSeePrices } = useCanSeePrices();
|
|
37
40
|
const { showToast } = useToast();
|
|
38
41
|
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
39
42
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
@@ -181,7 +184,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
181
184
|
},
|
|
182
185
|
},
|
|
183
186
|
] } } : {});
|
|
184
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PREORDER_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: preOrders.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(Div, { padding: "y-sm", children: _jsx(AvailabilityTabs, { types: PRE_ORDER_LISTING_TYPES }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
187
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: withoutPriceSorts(PREORDER_SORT_OPTIONS, canSeePrices), onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: preOrders.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(Div, { padding: "y-sm", children: _jsx(AvailabilityTabs, { types: PRE_ORDER_LISTING_TYPES }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
185
188
|
{
|
|
186
189
|
id: ACTION_ID.ADD_TO_CART,
|
|
187
190
|
label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
|
|
@@ -15,6 +15,15 @@ export interface ProductListActionParams {
|
|
|
15
15
|
export type ProductListResult = FirebaseSieveResult<ProductDocument>;
|
|
16
16
|
export declare function listProducts(params?: ProductListActionParams): Promise<ProductListResult>;
|
|
17
17
|
export declare function getProductById(id: string): Promise<ProductDocument | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Featured items for the homepage's generic "Featured Products" strip.
|
|
20
|
+
*
|
|
21
|
+
* Scoped to `GENERIC_PRODUCT_LISTING_TYPES` for the same reason `/products` is:
|
|
22
|
+
* the homepage already has dedicated Live Auctions, Pre-Orders and Prize Draws
|
|
23
|
+
* sections, so leaving this unscoped put the same item in two strips on one
|
|
24
|
+
* page — and an auction rendered as a generic product card shows Buy-Now chrome
|
|
25
|
+
* instead of bid affordances.
|
|
26
|
+
*/
|
|
18
27
|
export declare function getFeaturedProducts(pageSize?: number): Promise<ProductListResult>;
|
|
19
28
|
/**
|
|
20
29
|
* The soonest-ending LIVE auctions.
|