@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
|
@@ -3,6 +3,7 @@ import { sortBy } from "@mohasinac/appkit";
|
|
|
3
3
|
import { listPublicProducts } from "../../../_internal/server/features/products/list-public";
|
|
4
4
|
import { AVAILABILITY_VALUES, PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
5
5
|
import { productRepository } from "../repository/products.repository";
|
|
6
|
+
import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs";
|
|
6
7
|
import { ProductStatusValues } from "../schemas";
|
|
7
8
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility";
|
|
8
9
|
/**
|
|
@@ -63,10 +64,20 @@ function toListResult(result, pageSize) {
|
|
|
63
64
|
hasMore: result.hasMore,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Featured items for the homepage's generic "Featured Products" strip.
|
|
69
|
+
*
|
|
70
|
+
* Scoped to `GENERIC_PRODUCT_LISTING_TYPES` for the same reason `/products` is:
|
|
71
|
+
* the homepage already has dedicated Live Auctions, Pre-Orders and Prize Draws
|
|
72
|
+
* sections, so leaving this unscoped put the same item in two strips on one
|
|
73
|
+
* page — and an auction rendered as a generic product card shows Buy-Now chrome
|
|
74
|
+
* instead of bid affordances.
|
|
75
|
+
*/
|
|
66
76
|
export async function getFeaturedProducts(pageSize = 8) {
|
|
67
77
|
return toListResult(await listPublicProducts({
|
|
68
78
|
featured: true,
|
|
69
79
|
availability: AVAILABILITY_VALUES.AVAILABLE,
|
|
80
|
+
listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
|
|
70
81
|
sorts: sortBy("createdAt", "DESC"),
|
|
71
82
|
page: 1,
|
|
72
83
|
pageSize,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Div, Row, Span } from "../../../ui";
|
|
2
|
+
import { Button, Div, GatedPrice, Row, Span } from "../../../ui";
|
|
3
3
|
import { MediaImage } from "../../media/MediaImage";
|
|
4
4
|
import { formatCurrency as defaultFormatCurrency } from "../../../utils/number.formatter";
|
|
5
5
|
const __O = {
|
|
@@ -35,7 +35,7 @@ export function getProductTableColumns({ labels, onEdit, onDelete, currencyCode,
|
|
|
35
35
|
header: labels.price,
|
|
36
36
|
sortable: true,
|
|
37
37
|
width: "10%",
|
|
38
|
-
render: (product) => (_jsx(Span, { children: formatCurrency(product.price ?? 0, product.currency ?? currencyCode, locale) })),
|
|
38
|
+
render: (product) => (_jsx(Span, { children: _jsx(GatedPrice, { children: formatCurrency(product.price ?? 0, product.currency ?? currencyCode, locale) }) })),
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
key: "stockQuantity",
|
|
@@ -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 "../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 { useProducts } from "../hooks/useProducts";
|
|
@@ -30,6 +32,7 @@ const AUCTION_LISTING_TYPES = ["auction"];
|
|
|
30
32
|
const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_BID, TABLE_KEYS.MAX_BID, TABLE_KEYS.STORE_ID, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
|
|
31
33
|
export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
32
34
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
35
|
+
const { canSeePrices } = useCanSeePrices();
|
|
33
36
|
const { showToast } = useToast();
|
|
34
37
|
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
35
38
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
@@ -134,7 +137,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
134
137
|
},
|
|
135
138
|
},
|
|
136
139
|
] } } : {});
|
|
137
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_PUBLIC_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: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(Div, { padding: "y-sm", children: _jsx(AvailabilityTabs, { types: AUCTION_LISTING_TYPES }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
140
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: withoutPriceSorts(AUCTION_PUBLIC_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: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(Div, { padding: "y-sm", children: _jsx(AvailabilityTabs, { types: AUCTION_LISTING_TYPES }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
138
141
|
{
|
|
139
142
|
id: ACTION_ID.WATCH_AUCTION,
|
|
140
143
|
label: "Add to Watchlist",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { Div, Heading, Span, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Div, GatedPrice, Heading, Span, Stack, Text } from "../../../ui";
|
|
5
5
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
6
|
function formatBidDate(raw) {
|
|
7
7
|
if (!raw)
|
|
@@ -40,5 +40,5 @@ export function BidHistory({ isLoading = false, isEmpty = false, renderList, ren
|
|
|
40
40
|
}
|
|
41
41
|
if (renderList)
|
|
42
42
|
return _jsx(Div, { className: className, children: renderList() });
|
|
43
|
-
return (_jsxs(Div, { className: className, children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-3 tracking-wide", color: "muted", size: "sm", weight: "semibold", transform: "uppercase", children: labels.title })), _jsx(Stack, { gap: "sm", children: bids.map((bid, i) => renderBid ? (_jsx(React.Fragment, { children: renderBid(bid, i) }, bid.id)) : (_jsxs(Div, { layout: "flex", align: "center", justify: "between", rounded: "lg", padding: "sm", textSize: "sm", className: "border border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", "data-section": "bidhistory-div-422", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Span, { weight: "bold", className: "text-primary-600 dark:text-primary-400", children: currency ? formatCurrency(bid.amount, currency) : bid.amount.toLocaleString() }), _jsx(Span, { size: "xs", color: "muted", children: bid.bidderName || maskBidderId(bid.bidderId) })] }), _jsx(Span, { size: "xs", color: "muted", children: formatBidDate(bid.placedAt) })] }, bid.id))) })] }));
|
|
43
|
+
return (_jsxs(Div, { className: className, children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-3 tracking-wide", color: "muted", size: "sm", weight: "semibold", transform: "uppercase", children: labels.title })), _jsx(Stack, { gap: "sm", children: bids.map((bid, i) => renderBid ? (_jsx(React.Fragment, { children: renderBid(bid, i) }, bid.id)) : (_jsxs(Div, { layout: "flex", align: "center", justify: "between", rounded: "lg", padding: "sm", textSize: "sm", className: "border border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", "data-section": "bidhistory-div-422", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Span, { weight: "bold", className: "text-primary-600 dark:text-primary-400", children: _jsx(GatedPrice, { label: "Sign in to see bids", children: currency ? formatCurrency(bid.amount, currency) : bid.amount.toLocaleString() }) }), _jsx(Span, { size: "xs", color: "muted", children: bid.bidderName || maskBidderId(bid.bidderId) })] }), _jsx(Span, { size: "xs", color: "muted", children: formatBidDate(bid.placedAt) })] }, bid.id))) })] }));
|
|
44
44
|
}
|
|
@@ -12,7 +12,7 @@ const REL_NOOPENER = "noopener noreferrer";
|
|
|
12
12
|
const CLS_REMOVE_BTN = "shrink-0 text-zinc-400 hover:text-error hover:bg-surface-hover";
|
|
13
13
|
import Link from "next/link";
|
|
14
14
|
import { X } from "lucide-react";
|
|
15
|
-
import { Button, Div, Heading, IconButton, Row, Span, Stack, Text } from "../../../ui";
|
|
15
|
+
import { Button, Div, GatedPrice, Heading, IconButton, Row, Span, Stack, Text } from "../../../ui";
|
|
16
16
|
import { MediaImage } from "../../media/MediaImage";
|
|
17
17
|
import { useSwipe } from "../../../react/hooks/useSwipe";
|
|
18
18
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -67,7 +67,7 @@ function customFieldValue(field) {
|
|
|
67
67
|
function CompareColumn({ item, productType, labels, onRemove, onClose, customFieldKeys }) {
|
|
68
68
|
const href = detailHref(item, productType);
|
|
69
69
|
const img = item.mainImage ?? item.images?.[0];
|
|
70
|
-
return (_jsxs(Stack, { gap: "md", className: COLUMN_CARD_CLASS, children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, className: "text-[length:var(--appkit-text-sm)] font-semibold text-[var(--appkit-color-text)] hover:underline line-clamp-2", children: item.title ?? item.id }), onRemove && (_jsx(IconButton, { type: "button", onClick: () => onRemove(item.id), "aria-label": `${labels.remove} ${item.title ?? item.id}`, variant: "ghost", rounded: "full", className: CLS_REMOVE_BTN, children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsx(FieldRow, { label: labels.field.image, children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, children: _jsx(Div, { className: `relative aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: img ? (_jsx(MediaImage, { src: img, alt: item.title ?? item.id, size: "card" })) : null }) }) }), _jsx(FieldRow, { label: labels.field.price, children: _jsx(Text, { size: "base", weight: "semibold", color: "primary", children: priceLabel(item) }) }), _jsx(FieldRow, { label: labels.field.condition, children: _jsx(Text, { children: item.condition ? _jsx(Span, { className: CHIP_CLASS, children: item.condition }) : "—" }) }), _jsx(FieldRow, { label: labels.field.brand, children: _jsx(Text, { children: item.brand ? _jsx(Span, { className: CHIP_CLASS, children: item.brand }) : "—" }) }), _jsx(FieldRow, { label: labels.field.category, children: _jsx(Text, { children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) || (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) ? (_jsx(Span, { className: CHIP_CLASS, children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) ?? (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) })) : ("—") }) }), _jsx(FieldRow, { label: labels.field.store, children: _jsx(Text, { size: "sm", color: "muted", children: item.storeName ?? "—" }) }), customFieldKeys.map((key) => (_jsx(FieldRow, { label: key, children: _jsx(Text, { size: "sm", children: customFieldValue(item.customFields?.find((f) => f.key === key)) }) }, key))), _jsx(Button, { asChild: true, variant: "primary", size: "sm", className: "mt-auto", children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, onClick: onClose, children: labels.view }) })] }));
|
|
70
|
+
return (_jsxs(Stack, { gap: "md", className: COLUMN_CARD_CLASS, children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, className: "text-[length:var(--appkit-text-sm)] font-semibold text-[var(--appkit-color-text)] hover:underline line-clamp-2", children: item.title ?? item.id }), onRemove && (_jsx(IconButton, { type: "button", onClick: () => onRemove(item.id), "aria-label": `${labels.remove} ${item.title ?? item.id}`, variant: "ghost", rounded: "full", className: CLS_REMOVE_BTN, children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsx(FieldRow, { label: labels.field.image, children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, children: _jsx(Div, { className: `relative aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: img ? (_jsx(MediaImage, { src: img, alt: item.title ?? item.id, size: "card" })) : null }) }) }), _jsx(FieldRow, { label: labels.field.price, children: _jsx(Text, { size: "base", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: priceLabel(item) }) }) }), _jsx(FieldRow, { label: labels.field.condition, children: _jsx(Text, { children: item.condition ? _jsx(Span, { className: CHIP_CLASS, children: item.condition }) : "—" }) }), _jsx(FieldRow, { label: labels.field.brand, children: _jsx(Text, { children: item.brand ? _jsx(Span, { className: CHIP_CLASS, children: item.brand }) : "—" }) }), _jsx(FieldRow, { label: labels.field.category, children: _jsx(Text, { children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) || (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) ? (_jsx(Span, { className: CHIP_CLASS, children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) ?? (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) })) : ("—") }) }), _jsx(FieldRow, { label: labels.field.store, children: _jsx(Text, { size: "sm", color: "muted", children: item.storeName ?? "—" }) }), customFieldKeys.map((key) => (_jsx(FieldRow, { label: key, children: _jsx(Text, { size: "sm", children: customFieldValue(item.customFields?.find((f) => f.key === key)) }) }, key))), _jsx(Button, { asChild: true, variant: "primary", size: "sm", className: "mt-auto", children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, onClick: onClose, children: labels.view }) })] }));
|
|
71
71
|
}
|
|
72
72
|
function useFetchedProducts(productIds, passedItems, isOpen) {
|
|
73
73
|
const [fetched, setFetched] = useState([]);
|
|
@@ -18,7 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* consumer pages untouched.
|
|
19
19
|
*/
|
|
20
20
|
import { useMemo, useState } from "react";
|
|
21
|
-
import { Alert, Button, Div, Row, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, } from "../../../ui";
|
|
21
|
+
import { Alert, Button, Div, GatedPrice, PricesOnly, Row, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, } from "../../../ui";
|
|
22
22
|
import { QuantityStepper } from "../../../ui/components/QuantityStepper";
|
|
23
23
|
import { MediaImage } from "../../media/MediaImage";
|
|
24
24
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -101,8 +101,8 @@ export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAu
|
|
|
101
101
|
};
|
|
102
102
|
return (_jsxs(Stack, { gap: "sm", children: [isCrossStore && (_jsx(Alert, { variant: "info", children: "These items are sold by different sellers, so they can't be combined into one cart line. Open an item to buy it on its own." })), _jsx(Div, { className: __O.xAuto, children: _jsxs(Table, { className: "text-left min-w-[440px]", children: [_jsx(Thead, { children: _jsxs(Tr, { border: "default", children: [_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Image" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Name" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Price" }), !isCrossStore && (_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Qty" })), _jsx(Th, { paddingY: "sm", paddingSide: "pb-sm", color: "muted", size: "xs", weight: "semibold" })] }) }), _jsx(Tbody, { children: members.map((m) => {
|
|
103
103
|
const blocked = blockedReason(m);
|
|
104
|
-
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: formatCurrency(m.price, m.currency ?? currency) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0, max: m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx("a", { href: memberHref(m), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
|
|
105
|
-
}) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [error && _jsx(Alert, { variant: "error", children: error }), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: _jsx(GatedPrice, { children: formatCurrency(m.price, m.currency ?? currency) }) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0, max: m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx("a", { href: memberHref(m), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
|
|
105
|
+
}) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [error && _jsx(Alert, { variant: "error", children: error }), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [_jsxs(Text, { size: "sm", color: "muted", children: [selectedUnits > 0
|
|
106
|
+
? `${selectedUnits} item${selectedUnits === 1 ? "" : "s"}`
|
|
107
|
+
: "Nothing selected yet", selectedUnits > 0 && (_jsx(PricesOnly, { children: ` · ${formatCurrency(selectedTotal, currency)}` }))] }), _jsx(Button, { action: ACTIONS.CART["add-group-to-cart"], disabled: selectedUnits === 0, isLoading: isSaving, onClick: handleAdd })] })] }))] }));
|
|
108
108
|
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useBottomActions } from "../../layout/hooks/useBottomActions";
|
|
3
3
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
4
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
4
5
|
import { ACTION_META, MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
|
|
5
6
|
export function ListingBottomActions({ listingType, anchorId, price, currency, unavailable = false, infoSuffix, }) {
|
|
6
7
|
const actionIds = MOBILE_PRIMARY_ACTIONS[listingType] ?? [];
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const { canSeePrices } = useCanSeePrices();
|
|
9
|
+
// `infoLabel` is a plain string on the bottom bar, not a slot, so it cannot
|
|
10
|
+
// be wrapped in <GatedPrice> — the gate has to happen while the string is
|
|
11
|
+
// built. Note this label is ALSO the re-publish key for `useBottomActions`
|
|
12
|
+
// (see CLAUDE.md's bottom-edge notes), so it has to change when the viewer's
|
|
13
|
+
// access changes, which it does: the two branches never produce equal text.
|
|
14
|
+
const infoLabel = price === null
|
|
15
|
+
? undefined
|
|
16
|
+
: canSeePrices
|
|
17
|
+
? `${formatCurrency(price, currency)}${infoSuffix ? ` ${infoSuffix}` : ""}`
|
|
18
|
+
: "Sign in to see price";
|
|
10
19
|
useBottomActions(unavailable
|
|
11
20
|
? {}
|
|
12
21
|
: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
4
5
|
import { Button, Div, LoginRequiredModal, Modal, Span, Stack, Text, } from "../../../ui";
|
|
5
6
|
import { Form } from "../../../ui/components/Form";
|
|
6
7
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
@@ -30,6 +31,7 @@ function isActiveOfferError(msg) {
|
|
|
30
31
|
export function MakeOfferButton({ productId, listedPrice, currency, bounds, onMakeOffer, className = "", }) {
|
|
31
32
|
const [state, setState] = useState("idle");
|
|
32
33
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
34
|
+
const { canSeePrices } = useCanSeePrices();
|
|
33
35
|
const [isPending, startTransition] = useTransition();
|
|
34
36
|
const fmt = (n) => (currency ? formatCurrency(n, currency) : `₹${n.toLocaleString()}`);
|
|
35
37
|
// The SAME bounds the server enforces — passed in, never recomputed, so the
|
|
@@ -84,7 +86,11 @@ export function MakeOfferButton({ productId, listedPrice, currency, bounds, onMa
|
|
|
84
86
|
if (state === "pending") {
|
|
85
87
|
return (_jsxs(Div, { className: `${CLS_PENDING_BOX} ${className}`, children: [_jsx(Span, { size: "lg", children: "\u23F3" }), _jsx(Text, { className: CLS_PENDING_TITLE, children: bounds.isBuyRequest ? "Request Pending" : "Offer Pending" }), _jsxs(Text, { className: CLS_PENDING_BODY, children: ["You already have ", bounds.isBuyRequest ? "a request" : "an offer", " on this item. Check My Offers for updates."] })] }));
|
|
86
88
|
}
|
|
87
|
-
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to make an offer. Please log in or create an account to continue." }), _jsx(Button, { variant: "ghost", size: "md", border: "strong", className: `w-full ${className}`,
|
|
89
|
+
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to make an offer. Please log in or create an account to continue." }), _jsx(Button, { variant: "ghost", size: "md", border: "strong", className: `w-full ${className}`,
|
|
90
|
+
// The modal's first line is "Listed at ₹X · Minimum offer: ₹Y", so
|
|
91
|
+
// opening it for a signed-out visitor both discloses the price and
|
|
92
|
+
// walks them into a form that can only fail at submit.
|
|
93
|
+
onClick: () => (canSeePrices ? setState("open") : setShowLoginModal(true)), children: bounds.isBuyRequest ? "Request to Buy" : "Make Offer" }), _jsx(Modal, { isOpen: state === "open", onClose: () => setState("idle"), size: "md", title: bounds.isBuyRequest ? "Request to buy" : "Make an offer", children: _jsx(Form, { schema: schema, onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors, markSubmitAttempted }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: bounds.isBuyRequest
|
|
88
94
|
? `Listed at ${fmt(listedPrice)} · this seller is not taking lower offers`
|
|
89
95
|
: `Listed at ${fmt(listedPrice)} · Minimum offer: ${fmt(bounds.min)}` }), _jsx(FieldInput, { name: "offerAmount", type: "number", label: bounds.isBuyRequest ? "Amount" : "Your offer amount", required: true, value: offerAmount, onChange: (value) => {
|
|
90
96
|
setOfferAmount(value);
|
|
@@ -10,7 +10,7 @@ const CLS_BUNDLE_PILL = "inline-flex items-center rounded-full bg-violet-600 px-
|
|
|
10
10
|
const CLS_STOCK_OK = "bg-success-solid text-success-on-solid";
|
|
11
11
|
// audit-content-alignment-ok: button label text, not page content — buttons are universally center-labeled
|
|
12
12
|
const CLS_VIEW_BTN = "mt-2 w-full cursor-pointer rounded-md bg-violet-600 py-[var(--appkit-space-1-5)] text-center text-[length:var(--appkit-text-xs)] font-semibold text-white transition-colors hover:bg-violet-700 active:scale-[0.98]";
|
|
13
|
-
import { BaseListingCard, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
13
|
+
import { BaseListingCard, Div, GatedPrice, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
14
14
|
import { MediaImage } from "../../media/MediaImage";
|
|
15
15
|
import { computeBundleDiscount } from "../../../_internal/shared/features/categories/bundle-pricing";
|
|
16
16
|
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
@@ -54,8 +54,8 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
54
54
|
onSelect(bundle.id, !isSelected);
|
|
55
55
|
}, className: selectable || isSelected
|
|
56
56
|
? "opacity-100"
|
|
57
|
-
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: bundle.name }) }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: price
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: bundle.name }) }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", wrap: true, children: [_jsx(Row, { gap: "xs", align: "center", children: _jsx(GatedPrice, { label: "Sign in to see the bundle price", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: price
|
|
58
|
+
? formatCurrency(price, getDefaultCurrency())
|
|
59
|
+
: "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }) }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
|
|
60
60
|
handleNavigate(); }, children: "View Bundle" })] })] }));
|
|
61
61
|
}
|
|
@@ -8,7 +8,7 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
|
8
8
|
import { formatCurrency } from "../../../utils";
|
|
9
9
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
10
10
|
const CLS_PRIZE_PILL = "inline-flex items-center rounded-full bg-fuchsia-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
|
|
11
|
-
import { BaseListingCard, Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
11
|
+
import { BaseListingCard, Button, Div, GatedPrice, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
12
12
|
import { MediaImage } from "../../media/MediaImage";
|
|
13
13
|
import { CardStoreLine } from "./CardStoreLine";
|
|
14
14
|
const DEFAULT_LABELS = {
|
|
@@ -99,5 +99,5 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
99
99
|
onSelect(product.id, !isSelected);
|
|
100
100
|
}, className: selectable || isSelected
|
|
101
101
|
? "opacity-100"
|
|
102
|
-
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [
|
|
102
|
+
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }) }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
103
103
|
}
|
|
@@ -16,7 +16,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
16
16
|
*/
|
|
17
17
|
import { Row } from "@mohasinac/appkit/client";
|
|
18
18
|
import { useState } from "react";
|
|
19
|
-
import { Button, Div, Grid, Scrim, Text } from "../../../ui";
|
|
19
|
+
import { Button, Div, Grid, PricesOnly, Scrim, Text } from "../../../ui";
|
|
20
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
20
21
|
import { MediaImage } from "../../media/MediaImage";
|
|
21
22
|
import { ImageLightbox } from "../../../ui/components/ImageLightbox";
|
|
22
23
|
const __P = {
|
|
@@ -34,23 +35,29 @@ function makePrizeItemClickHandler(it, idx, onItemClick, setLightboxIndex) {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
/** Build the lightbox images array from sorted prize items. */
|
|
37
|
-
function toGalleryImages(items) {
|
|
38
|
+
function toGalleryImages(items, canSeePrices) {
|
|
38
39
|
return items.map((it) => ({
|
|
39
40
|
src: it.images?.[0] ?? "",
|
|
40
41
|
alt: it.title || `Prize #${it.itemNumber}`,
|
|
41
42
|
badge: `#${it.itemNumber}`,
|
|
42
43
|
caption: it.title || `Prize #${it.itemNumber}`,
|
|
43
|
-
sub
|
|
44
|
+
// `sub` is a plain caption string, so the gate happens here — the tile's
|
|
45
|
+
// own estimate is gated in the JSX below and this would otherwise print it
|
|
46
|
+
// again one click away.
|
|
47
|
+
sub: canSeePrices && it.estimatedValue != null
|
|
44
48
|
? `est. ₹${it.estimatedValue.toLocaleString("en-IN")}`
|
|
45
49
|
: undefined,
|
|
46
50
|
}));
|
|
47
51
|
}
|
|
48
52
|
export function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonLabel = "Won", hideWonState = false, }) {
|
|
49
53
|
const [lightboxIndex, setLightboxIndex] = useState(null);
|
|
54
|
+
// Above the empty-state early return — a hook after a conditional return is a
|
|
55
|
+
// render-order violation, not a style preference.
|
|
56
|
+
const { canSeePrices } = useCanSeePrices();
|
|
50
57
|
if (!items.length) {
|
|
51
58
|
return (_jsx(Div, { className: `border border-dashed border-[var(--appkit-color-border)] ${__P.p6} text-left`, rounded: "default", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "No prizes configured yet." }) }));
|
|
52
59
|
}
|
|
53
|
-
const galleryImages = toGalleryImages(items);
|
|
60
|
+
const galleryImages = toGalleryImages(items, canSeePrices);
|
|
54
61
|
return (_jsxs(_Fragment, { children: [_jsx(Grid, { cols: "categoryCards", gap: "3", children: items.map((it, idx) => {
|
|
55
62
|
const cover = it.images?.[0];
|
|
56
63
|
const isHighlight = highlightItemNumber === it.itemNumber;
|
|
@@ -68,7 +75,11 @@ export function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonL
|
|
|
68
75
|
isHighlight
|
|
69
76
|
? "ring-2 ring-offset-2 ring-[var(--appkit-color-primary)] scale-[1.02]"
|
|
70
77
|
: "hover:scale-[1.01]",
|
|
71
|
-
].join(" "), "aria-label": `View ${it.title || `prize #${it.itemNumber}`} in lightbox`, children: [_jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (_jsx(MediaImage, { src: cover, alt: it.title || `Prize #${it.itemNumber}`, size: "thumbnail" })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })), _jsxs(Div, { textWeight: "semibold", textSize: "xs", surface: "overlay-xl", padding: "chip-2xs", className: "absolute left-2 top-2 text-white", rounded: "default", children: ["#", it.itemNumber] }), it.isWon && !hideWonState ? (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "diagonal", intensity: "medium", multiply: true, className: "absolute inset-0" }), _jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Text, { className: CLS_WON_STAMP, children: wonLabel }) })] })) : null] }), _jsxs(Div, { padding: "xs", children: [_jsx(Text, { className: "line-clamp-2", size: "sm", weight: "medium", children: it.title || `Prize #${it.itemNumber}` }), it.estimatedValue != null ? (
|
|
78
|
+
].join(" "), "aria-label": `View ${it.title || `prize #${it.itemNumber}`} in lightbox`, children: [_jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (_jsx(MediaImage, { src: cover, alt: it.title || `Prize #${it.itemNumber}`, size: "thumbnail" })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })), _jsxs(Div, { textWeight: "semibold", textSize: "xs", surface: "overlay-xl", padding: "chip-2xs", className: "absolute left-2 top-2 text-white", rounded: "default", children: ["#", it.itemNumber] }), it.isWon && !hideWonState ? (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "diagonal", intensity: "medium", multiply: true, className: "absolute inset-0" }), _jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Text, { className: CLS_WON_STAMP, children: wonLabel }) })] })) : null] }), _jsxs(Div, { padding: "xs", children: [_jsx(Text, { className: "line-clamp-2", size: "sm", weight: "medium", children: it.title || `Prize #${it.itemNumber}` }), it.estimatedValue != null ? (
|
|
79
|
+
// The prize's estimated value is a money amount like any
|
|
80
|
+
// other — hidden rather than prompted, because the entry
|
|
81
|
+
// price beside it already carries the sign-in gate.
|
|
82
|
+
_jsx(PricesOnly, { children: _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["est. \u20B9", it.estimatedValue.toLocaleString("en-IN")] }) })) : null] })] }, `collage-${it.itemNumber}`));
|
|
72
83
|
}) }), _jsx(ImageLightbox, { images: galleryImages, activeIndex: lightboxIndex, onClose: () => setLightboxIndex(null), onNavigate: (i) => setLightboxIndex(i), showThumbnails: true })] }));
|
|
73
84
|
}
|
|
74
85
|
export default PrizeDrawCollage;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useState } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
|
-
import { Anchor, Button, Stack, Text, LoginRequiredModal } from "../../../ui";
|
|
5
|
+
import { Anchor, Button, GatedPrice, Stack, Text, LoginRequiredModal } from "../../../ui";
|
|
6
6
|
import { ROUTES } from "../../../next";
|
|
7
7
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
8
8
|
import { NonRefundableConsentModal } from "./NonRefundableConsentModal";
|
|
@@ -31,5 +31,5 @@ export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerE
|
|
|
31
31
|
const slug = productSlug ?? productId;
|
|
32
32
|
router.push(`${String(ROUTES.USER.CHECKOUT)}?directItem=${encodeURIComponent(slug)}&type=prize-draw`);
|
|
33
33
|
}, [productSlug, productId, router]);
|
|
34
|
-
return (_jsxs(Stack, { gap: "md", children: [
|
|
34
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-[var(--appkit-color-text-muted)]", size: "xs", weight: "normal", children: "per entry" })] }) }), _jsx(Button, { action: closed ? undefined : ACTIONS.PRIZE_DRAW["buy-now"], variant: "primary", size: "md", className: "w-full", disabled: closed || isAuthResolving, onClick: handleEnter, children: closed ? "Draw closed" : isAuthResolving ? "Checking…" : undefined }), prizeGithubFileUrl ? (_jsx(Anchor, { href: prizeGithubFileUrl, tone: "none", underline: "hover", size: "xs", weight: "medium", className: "text-left text-primary-600 underline-offset-4 dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", align: "start", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
35
35
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { withoutPriceSorts } from "../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";
|
|
@@ -28,6 +30,7 @@ const FILTER_KEYS = [
|
|
|
28
30
|
];
|
|
29
31
|
export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, storeId: forcedStoreId, }) {
|
|
30
32
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
33
|
+
const { canSeePrices } = useCanSeePrices();
|
|
31
34
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
32
35
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
33
36
|
const availability = table.get(TABLE_KEYS.AVAILABILITY) ||
|
|
@@ -82,7 +85,7 @@ export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, s
|
|
|
82
85
|
if (e.key === "Enter")
|
|
83
86
|
commitSearch();
|
|
84
87
|
};
|
|
85
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRIZE_DRAW_SORT_OPTIONS, onSortChange: (v) => {
|
|
88
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: withoutPriceSorts(PRIZE_DRAW_SORT_OPTIONS, canSeePrices), onSortChange: (v) => {
|
|
86
89
|
table.set(TABLE_KEYS.SORT, v);
|
|
87
90
|
}, view: view, onViewChange: (v) => {
|
|
88
91
|
if (v === VIEW_MODE.TABLE)
|
|
@@ -9,7 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
* Firebase Function at draw expiry/sellout ("scheduled" mode). This modal is
|
|
10
10
|
* a pure display: "pending" (no result yet) or "won" (order.prizeWon is set).
|
|
11
11
|
*/
|
|
12
|
-
import { Anchor, Div, Heading, Modal, Span, Stack, Text } from "../../../ui";
|
|
12
|
+
import { Anchor, Div, Heading, Modal, PricesOnly, Span, Stack, Text } from "../../../ui";
|
|
13
13
|
import { MediaImage } from "../../media/MediaImage";
|
|
14
14
|
import { PrizeDrawCollage } from "./PrizeDrawCollage";
|
|
15
15
|
const __P = {
|
|
@@ -26,6 +26,6 @@ export function PrizeRevealModal({ open, onClose, items, initialPrizeWon, reveal
|
|
|
26
26
|
// audit-content-alignment-ok: informational reveal-status panel, not marketing content
|
|
27
27
|
_jsxs(Div, { className: `bg-[var(--appkit-color-surface-muted)] ${__P.p4} text-center`, rounded: "default", children: [_jsx(Text, { size: "lg", weight: "semibold", children: "Reveal pending" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: PENDING_COPY[revealMode] })] })) : (
|
|
28
28
|
// audit-content-alignment-ok: theatrical reveal-modal centerpiece, not marketing content
|
|
29
|
-
_jsxs(Div, { className: `border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] ${__P.p4} text-center`, rounded: "lg", children: [_jsx(Text, { className: "tracking-wider text-[var(--appkit-color-text-muted)]", size: "xs", transform: "uppercase", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (_jsx(Div, { className: "relative mx-auto h-64 w-64 max-w-full", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: winnerImg, alt: winner.title, size: "hero", objectFit: "contain" }) })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: ["Estimated value: \u20B9", winner.estimatedValue.toLocaleString("en-IN")] })) : null] })), _jsxs(Div, { textSize: "xs", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] text-[var(--appkit-color-text-muted)]", padding: "inlineSm", rounded: "default", children: [_jsx(Span, { weight: "bold", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server the moment your prize is assigned. Neither the store nor LetItRip staff can influence the outcome.", " ", rngSourceUrl ? (_jsx(Anchor, { href: rngSourceUrl, tone: "none", underline: "always", children: "View RNG source code \u2192" })) : null] })] }) }));
|
|
29
|
+
_jsxs(Div, { className: `border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] ${__P.p4} text-center`, rounded: "lg", children: [_jsx(Text, { className: "tracking-wider text-[var(--appkit-color-text-muted)]", size: "xs", transform: "uppercase", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (_jsx(Div, { className: "relative mx-auto h-64 w-64 max-w-full", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: winnerImg, alt: winner.title, size: "hero", objectFit: "contain" }) })) : null, winner.estimatedValue != null ? (_jsx(PricesOnly, { children: _jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: ["Estimated value: \u20B9", winner.estimatedValue.toLocaleString("en-IN")] }) })) : null] })), _jsxs(Div, { textSize: "xs", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] text-[var(--appkit-color-text-muted)]", padding: "inlineSm", rounded: "default", children: [_jsx(Span, { weight: "bold", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server the moment your prize is assigned. Neither the store nor LetItRip staff can influence the outcome.", " ", rngSourceUrl ? (_jsx(Anchor, { href: rngSourceUrl, tone: "none", underline: "always", children: "View RNG source code \u2192" })) : null] })] }) }));
|
|
30
30
|
}
|
|
31
31
|
export default PrizeRevealModal;
|
|
@@ -13,6 +13,7 @@ import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
|
13
13
|
import { ACTION_ID } from "../constants/action-defs";
|
|
14
14
|
import { useBottomActions } from "../../layout/hooks/useBottomActions";
|
|
15
15
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
16
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
16
17
|
import { normalizeError } from "../../../errors/normalize";
|
|
17
18
|
/**
|
|
18
19
|
* Wires the three primary product-detail CTAs (Buy Now / Add to Cart / Wishlist)
|
|
@@ -23,6 +24,7 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
|
|
|
23
24
|
const router = useRouter();
|
|
24
25
|
const { showToast } = useToast();
|
|
25
26
|
const { requireAuth, isAuthResolving, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
27
|
+
const { canSeePrices } = useCanSeePrices();
|
|
26
28
|
const [busy, setBusy] = useState(null);
|
|
27
29
|
const [wishlisted, setWishlisted] = useState(false);
|
|
28
30
|
// `isAuthResolving` covers the window where the session has not settled and a
|
|
@@ -122,9 +124,14 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
|
|
|
122
124
|
onClick: handleBuyNow,
|
|
123
125
|
},
|
|
124
126
|
],
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
// A plain string on the bottom bar, so the gate happens as it is
|
|
128
|
+
// built rather than in a slot. It also doubles as the bar's
|
|
129
|
+
// re-publish key, and the two branches never produce equal text.
|
|
130
|
+
infoLabel: price == null || !currency
|
|
131
|
+
? undefined
|
|
132
|
+
: canSeePrices
|
|
133
|
+
? formatCurrency(price, currency)
|
|
134
|
+
: "Sign in to see price",
|
|
128
135
|
// The bar itself owns responsive visibility now, so this one registrar serves
|
|
129
136
|
// both breakpoints. The desktop branch below must NOT also register —
|
|
130
137
|
// useBottomActions is last-mounted-wins and the two would clobber each other.
|
|
@@ -5,7 +5,7 @@ import { FilterFacetSection } from "../../filters/FilterFacetSection";
|
|
|
5
5
|
import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
|
|
6
6
|
import { RangeFilter } from "../../filters/RangeFilter";
|
|
7
7
|
import { SwitchFilter } from "../../filters/SwitchFilter";
|
|
8
|
-
import { Div } from "../../../ui";
|
|
8
|
+
import { Div, PricesOnly } from "../../../ui";
|
|
9
9
|
import { TABLE_KEYS } from "../../../constants/table-keys";
|
|
10
10
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
11
11
|
import { STANDARD_SORT_OPTIONS, STANDARD_PUBLIC_SORT_OPTIONS, } from "../constants/sieve";
|
|
@@ -107,5 +107,5 @@ export function ProductFilters({ table, categoryOptions = [], brandOptions = [],
|
|
|
107
107
|
table.set(TABLE_KEYS.LISTING_TYPE, listingTypeOptions
|
|
108
108
|
.filter((o) => vals.includes(o.value))
|
|
109
109
|
.map((o) => o.value)
|
|
110
|
-
.join("|")), searchable: false, defaultCollapsed: false }), listingTypeFooter] })), loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: t("condition"), options: conditionOptions, selected: selectedConditions, onChange: (vals) => table.set(TABLE_KEYS.CONDITION, vals.join("|")), searchable: false, defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get(TABLE_KEYS.MIN_PRICE), maxValue: table.get(TABLE_KEYS.MAX_PRICE), onMinChange: (v) => table.set(TABLE_KEYS.MIN_PRICE, v), onMaxChange: (v) => table.set(TABLE_KEYS.MAX_PRICE, v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), defaultCollapsed: false })) : resolvedStoreOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: resolvedStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), searchable: resolvedStoreOptions.length > 4, defaultCollapsed: resolvedStoreOptions.length > 6 })) : null, showShipping && (_jsx(SwitchFilter, { title: t("shipping"), label: t("freeShippingOnly"), checked: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (v) => table.set(TABLE_KEYS.FREE_SHIPPING, v ? "true" : ""), defaultCollapsed: false })), tagOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("tags"), options: tagOptions, selected: selectedTags, onChange: (vals) => table.set(TABLE_KEYS.TAGS, vals.join("|")), searchable: tagOptions.length > 4, defaultCollapsed: tagOptions.length > 6 })), shouldShowStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions ?? defaultStatusOptions, selected: selectedStatuses, onChange: (vals) => table.set(TABLE_KEYS.STATUS, vals.join("|")), searchable: false, defaultCollapsed: false })), sublistingCategoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Sublisting Type", options: sublistingCategoryOptions, selected: selectedSublistings, onChange: (vals) => table.set(TABLE_KEYS.SUBLISTING_CATEGORY, vals.join("|")), searchable: sublistingCategoryOptions.length > 4, defaultCollapsed: false })), showBundleFilter && (_jsx(SwitchFilter, { title: "Bundles", label: "Part of a bundle only", checked: table.get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true", onChange: (v) => table.set(TABLE_KEYS.IS_PART_OF_BUNDLE, v ? "true" : ""), defaultCollapsed: false })), featureOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Features", options: featureOptions, selected: selectedFeatures, onChange: (vals) => table.set(TABLE_KEYS.FEATURES, vals.join("|")), searchable: featureOptions.length > 4, defaultCollapsed: featureOptions.length > 4 }))] }));
|
|
110
|
+
.join("|")), searchable: false, defaultCollapsed: false }), listingTypeFooter] })), loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: t("condition"), options: conditionOptions, selected: selectedConditions, onChange: (vals) => table.set(TABLE_KEYS.CONDITION, vals.join("|")), searchable: false, defaultCollapsed: false }), _jsx(PricesOnly, { children: _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get(TABLE_KEYS.MIN_PRICE), maxValue: table.get(TABLE_KEYS.MAX_PRICE), onMinChange: (v) => table.set(TABLE_KEYS.MIN_PRICE, v), onMaxChange: (v) => table.set(TABLE_KEYS.MAX_PRICE, v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }) }), loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), defaultCollapsed: false })) : resolvedStoreOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: resolvedStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), searchable: resolvedStoreOptions.length > 4, defaultCollapsed: resolvedStoreOptions.length > 6 })) : null, showShipping && (_jsx(SwitchFilter, { title: t("shipping"), label: t("freeShippingOnly"), checked: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (v) => table.set(TABLE_KEYS.FREE_SHIPPING, v ? "true" : ""), defaultCollapsed: false })), tagOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("tags"), options: tagOptions, selected: selectedTags, onChange: (vals) => table.set(TABLE_KEYS.TAGS, vals.join("|")), searchable: tagOptions.length > 4, defaultCollapsed: tagOptions.length > 6 })), shouldShowStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions ?? defaultStatusOptions, selected: selectedStatuses, onChange: (vals) => table.set(TABLE_KEYS.STATUS, vals.join("|")), searchable: false, defaultCollapsed: false })), sublistingCategoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Sublisting Type", options: sublistingCategoryOptions, selected: selectedSublistings, onChange: (vals) => table.set(TABLE_KEYS.SUBLISTING_CATEGORY, vals.join("|")), searchable: sublistingCategoryOptions.length > 4, defaultCollapsed: false })), showBundleFilter && (_jsx(SwitchFilter, { title: "Bundles", label: "Part of a bundle only", checked: table.get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true", onChange: (v) => table.set(TABLE_KEYS.IS_PART_OF_BUNDLE, v ? "true" : ""), defaultCollapsed: false })), featureOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Features", options: featureOptions, selected: selectedFeatures, onChange: (vals) => table.set(TABLE_KEYS.FEATURES, vals.join("|")), searchable: featureOptions.length > 4, defaultCollapsed: featureOptions.length > 4 }))] }));
|
|
111
111
|
}
|
|
@@ -4,7 +4,7 @@ import React from "react";
|
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
6
|
import { ROUTES } from "../../../next";
|
|
7
|
-
import { BaseListingCard, Button, Div, Grid, PriceDisplay, Row, Span, Stack, Text } from "../../../ui";
|
|
7
|
+
import { BaseListingCard, Button, Div, GatedPrice, Grid, PriceDisplay, PricesOnly, Row, Span, Stack, Text } from "../../../ui";
|
|
8
8
|
import { MediaImage } from "../../media/MediaImage";
|
|
9
9
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
10
10
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
@@ -142,7 +142,10 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
|
142
142
|
onClick ? "cursor-pointer" : "",
|
|
143
143
|
]
|
|
144
144
|
.filter(Boolean)
|
|
145
|
-
.join(" "), children: [_jsx(Div, { className: `flex-shrink-0 w-16 h-16 sm:w-20 sm:h-20 ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "lg", children: product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "thumbnail" })) : (_jsx(Div, { className: "w-full h-full bg-neutral-200" })) }), _jsxs(Stack, { className: "flex-1 min-w-0 gap-[2px]", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }), (product.categoryName || product.brand) && (_jsx(Span, { className: "text-[11px] truncate", color: "faint", children: [product.categoryName, product.brand].filter(Boolean).join(" · ") })), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId, className: "text-[11px]" }), _jsxs(Row, { className: "mt-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "sm", weight: "semibold", className: "text-primary", children: formatCurrency(product.price, getDefaultCurrency()) })
|
|
145
|
+
.join(" "), children: [_jsx(Div, { className: `flex-shrink-0 w-16 h-16 sm:w-20 sm:h-20 ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "lg", children: product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "thumbnail" })) : (_jsx(Div, { className: "w-full h-full bg-neutral-200" })) }), _jsxs(Stack, { className: "flex-1 min-w-0 gap-[2px]", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }), (product.categoryName || product.brand) && (_jsx(Span, { className: "text-[11px] truncate", color: "faint", children: [product.categoryName, product.brand].filter(Boolean).join(" · ") })), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId, className: "text-[11px]" }), _jsxs(Row, { className: "mt-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "sm", weight: "semibold", className: "text-primary", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), discount && (
|
|
146
|
+
// The discount % is derived from price vs originalPrice, so it is a
|
|
147
|
+
// money disclosure even though it carries no currency symbol.
|
|
148
|
+
_jsx(PricesOnly, { children: _jsxs(Span, { weight: "bold", className: CLS_DISCOUNT_TEXT_BARE, children: ["-", discount, "%"] }) })), product.rating !== undefined && (_jsxs(Span, { layout: "flex", gap: "2xs", className: "text-[11px]", color: "faint", children: [_jsx(Span, { className: CLS_STAR_BARE, children: "\u2605" }), product.rating.toFixed(1)] })), (() => {
|
|
146
149
|
const stock = product.stockCount ?? product.stockQuantity ?? product.availableQuantity;
|
|
147
150
|
if (stock === undefined)
|
|
148
151
|
return null;
|
|
@@ -8,6 +8,8 @@ import { useProducts } from "../hooks/useProducts";
|
|
|
8
8
|
import { BulkActionBar, Div, FilterDrawer, Grid, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
|
|
9
9
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
10
10
|
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
11
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
12
|
+
import { withoutPriceSorts } from "../constants/sieve";
|
|
11
13
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-defs";
|
|
12
14
|
import { CompareOverlay } from "./CompareOverlay";
|
|
13
15
|
import { ROUTES } from "../../../next";
|
|
@@ -56,6 +58,10 @@ function dedicatedPageFor(types) {
|
|
|
56
58
|
const plugin = pluginFor(types[0]);
|
|
57
59
|
if (!plugin.browseRoute)
|
|
58
60
|
return null;
|
|
61
|
+
// `standard`'s browseRoute IS /products, so without this the page offers
|
|
62
|
+
// "Full Products filters →" pointing at itself.
|
|
63
|
+
if (plugin.browseRoute === String(ROUTES.PUBLIC.PRODUCTS))
|
|
64
|
+
return null;
|
|
59
65
|
return {
|
|
60
66
|
href: plugin.browseRoute,
|
|
61
67
|
label: `Full ${plugin.pluralLabel} filters →`,
|
|
@@ -66,6 +72,7 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
66
72
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
67
73
|
const { showToast } = useToast();
|
|
68
74
|
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
75
|
+
const { canSeePrices } = useCanSeePrices();
|
|
69
76
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
70
77
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
71
78
|
const [compareIds, setCompareIds] = useState([]);
|
|
@@ -81,7 +88,11 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
81
88
|
// "Ending Soon"; pick Auctions + Products and you get only what both
|
|
82
89
|
// support, because a sort on `auctionEndDate` would order the products
|
|
83
90
|
// arbitrarily (and needs an index nobody declares for that shape).
|
|
84
|
-
|
|
91
|
+
// Price sorts are dropped for signed-out visitors for the same reason the
|
|
92
|
+
// price-range facet is hidden: ordering by price discloses the relative
|
|
93
|
+
// ranking of every hidden amount, and combined with paging it recovers a
|
|
94
|
+
// usable price list. `withoutPriceSorts` is a no-op once signed in.
|
|
95
|
+
const sortOptions = withoutPriceSorts(commonSortOptionsFor(effectiveTypes, "public"), canSeePrices);
|
|
85
96
|
const sortValue = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
|
|
86
97
|
// Guard against a sort carried over from a previous selection that the new
|
|
87
98
|
// one can't satisfy — that combination throws FAILED_PRECONDITION in
|
|
@@ -7,6 +7,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
|
7
7
|
import { TABLE_KEYS } from "../../../constants/table-keys";
|
|
8
8
|
import { sortBy } from "../../../constants/sort";
|
|
9
9
|
import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs";
|
|
10
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
10
11
|
import { listPublicProducts, parsePublicProductParams, defaultAvailabilityForListingTypes, } from "../../../_internal/server/features/products/list-public";
|
|
11
12
|
import { parseSelectedListingTypes } from "../utils/listing-type";
|
|
12
13
|
const DEFAULT_PAGE_SIZE = 24;
|
|
@@ -18,6 +19,31 @@ export async function ProductsIndexPageView({ searchParams = {} }) {
|
|
|
18
19
|
// (Root Cause #30).
|
|
19
20
|
const raw = searchParams[TABLE_KEYS.LISTING_TYPE];
|
|
20
21
|
const selectedTypes = parseSelectedListingTypes(Array.isArray(raw) ? raw[0] : raw);
|
|
22
|
+
// A selection naming ONLY types outside the general catalogue (a bookmark or
|
|
23
|
+
// an inbound link from before /products was narrowed) must be sent to the
|
|
24
|
+
// dedicated page that owns them. Without this, `narrowed = selected ∩ span`
|
|
25
|
+
// is empty and listPublicProducts falls back to the FULL span — so
|
|
26
|
+
// /products?listingType=auction would silently render the general catalogue
|
|
27
|
+
// and look like the auctions had vanished.
|
|
28
|
+
//
|
|
29
|
+
// Only redirect when the out-of-span types agree on one destination: `art`
|
|
30
|
+
// and `stickers` both point at /art, so that pair still resolves, whereas
|
|
31
|
+
// auction+pre-order genuinely has no single home and is better served by
|
|
32
|
+
// dropping the filter.
|
|
33
|
+
const outOfSpan = selectedTypes.filter((t) => !GENERIC_PRODUCT_LISTING_TYPES.includes(t));
|
|
34
|
+
if (outOfSpan.length > 0 && outOfSpan.length === selectedTypes.length) {
|
|
35
|
+
const destinations = new Set(outOfSpan.map((t) => pluginFor(t).browseRoute).filter((r) => Boolean(r)));
|
|
36
|
+
if (destinations.size === 1) {
|
|
37
|
+
const [destination] = [...destinations];
|
|
38
|
+
// Carry the search term only. Sort keys are per-type (Root Cause #63) —
|
|
39
|
+
// forwarding a /products sort to /auctions would name a field that page's
|
|
40
|
+
// config never offers, which drops the sort silently rather than loudly.
|
|
41
|
+
const q = searchParams[TABLE_KEYS.QUERY];
|
|
42
|
+
const term = (Array.isArray(q) ? q[0] : q)?.trim();
|
|
43
|
+
const { redirect } = await import("next/navigation");
|
|
44
|
+
redirect(term ? `${destination}?${TABLE_KEYS.QUERY}=${encodeURIComponent(term)}` : destination);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
const effectiveTypes = selectedTypes.length > 0 ? selectedTypes : GENERIC_PRODUCT_LISTING_TYPES;
|
|
22
48
|
// See ArtStickersListView — one shared filter implementation with
|
|
23
49
|
// /api/products, so SSR and the client refetch agree by construction.
|