@mohasinac/appkit 4.37.1 → 4.38.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/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/classified/og.d.ts +0 -1
- package/dist/_internal/server/features/classified/og.js +1 -9
- 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/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/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 +4 -0
- package/dist/client.js +4 -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/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/CategoryProductsListing.js +4 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
- 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/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/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__product-detail.js +53 -15
- 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/wishlist/components/WishlistPage.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- 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/styles.css +41 -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 +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
|
@@ -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.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { Div, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
|
|
5
|
+
import { Div, PricesOnly, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
|
|
6
6
|
import { MediaImage } from "../../media/MediaImage";
|
|
7
7
|
import { GroupMemberPicker } from "./GroupMemberPicker";
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -23,7 +23,7 @@ function MemberThumb({ member, isCurrent }) {
|
|
|
23
23
|
const price = formatCurrency(member.price, member.currency ?? "INR");
|
|
24
24
|
return (_jsxs(Link, { href: href, "aria-label": member.title, className: "flex flex-col items-center gap-[var(--appkit-space-1-5)] flex-shrink-0 w-16 group", children: [_jsxs(Div, { rounded: "full", className: `relative w-14 h-14 overflow-hidden border-2 transition-all ${isCurrent
|
|
25
25
|
? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
|
|
26
|
-
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
|
|
26
|
+
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: _jsx(PricesOnly, { children: price }) })] }));
|
|
27
27
|
}
|
|
28
28
|
export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle }) {
|
|
29
29
|
const [open, setOpen] = useState(false);
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { ROUTES } from "../../../next";
|
|
6
|
-
import { Div, Row, Span, Text } from "../../../ui";
|
|
6
|
+
import { Div, PricesOnly, Row, Span, Text } from "../../../ui";
|
|
7
7
|
import { MediaImage } from "../../media/MediaImage";
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
9
9
|
import { normalizeListingType } from "../utils/listing-type";
|
|
@@ -23,7 +23,7 @@ function ListingThumb({ listing, isCurrent, }) {
|
|
|
23
23
|
const price = formatCurrency(listing.price, listing.currency ?? "INR");
|
|
24
24
|
return (_jsxs(Link, { href: href, "aria-label": listing.title, className: "flex flex-col items-center gap-[var(--appkit-space-1-5)] flex-shrink-0 w-16 group", children: [_jsx(Div, { rounded: "full", className: `relative w-14 h-14 overflow-hidden border-2 transition-all ${isCurrent
|
|
25
25
|
? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
|
|
26
|
-
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: image ? (_jsx(MediaImage, { src: image, alt: listing.title, size: "avatar" })) : (_jsx(Row, { textSize: "xs", color: "faint", className: "w-full h-full", surface: "subtle", align: "center", justify: "center", children: "\u00E2\u2014\u00AF" })) }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: listing.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
|
|
26
|
+
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: image ? (_jsx(MediaImage, { src: image, alt: listing.title, size: "avatar" })) : (_jsx(Row, { textSize: "xs", color: "faint", className: "w-full h-full", surface: "subtle", align: "center", justify: "center", children: "\u00E2\u2014\u00AF" })) }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: listing.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: _jsx(PricesOnly, { children: price }) })] }));
|
|
27
27
|
}
|
|
28
28
|
export function SublistingCarouselSection({ sublistingCategoryId, currentListingId }) {
|
|
29
29
|
const [open, setOpen] = useState(false);
|
|
@@ -33,16 +33,33 @@ export interface ListingTab {
|
|
|
33
33
|
entity?: "stores";
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* The generic `/products` browse page spans
|
|
36
|
+
* The types the generic `/products` browse page spans — the GENERAL CATALOGUE,
|
|
37
|
+
* not an index of everything.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* DERIVED from `ListingTypePlugin.inGeneralCatalogue`, never hand-written: a
|
|
40
|
+
* literal array here is how ten enumerations of one union drifted apart in
|
|
41
|
+
* Root Cause #61, and `audit-listing-type-tab-coverage`'s STAYS_DERIVED rule
|
|
42
|
+
* blocks one.
|
|
43
|
+
*
|
|
44
|
+
* ## History — this spanned all nine types from 2026-08-21 until now
|
|
45
|
+
*
|
|
46
|
+
* The widening was a fix for Root Cause #61 and cited three reasons. Two have
|
|
47
|
+
* since been solved elsewhere, and the third is an accepted cost:
|
|
48
|
+
*
|
|
49
|
+
* 1. "Five of nine types unreachable from the main catalogue." Still true for
|
|
50
|
+
* `classified`/`digital-code`/`live`, which have dedicated pages but no
|
|
51
|
+
* `MAIN_NAV_ITEMS` entry — which is exactly why `inGeneralCatalogue` keeps
|
|
52
|
+
* those three here. The other five are all in the main nav.
|
|
53
|
+
* 2. "Seven legacy `/search/.../tab/<type>` redirects all point at /products."
|
|
54
|
+
* No longer true: that route derives `tabSlug -> browseRoute` from this
|
|
55
|
+
* same registry, and `/search` maps every resource type to its own page.
|
|
56
|
+
* Neither falls through to `/products` for a typed request.
|
|
57
|
+
* 3. "Cross-type Compare + bulk-cart only exist on this page." Still true, and
|
|
58
|
+
* accepted: you can no longer compare an auction against a standard product.
|
|
59
|
+
*
|
|
60
|
+
* A type appearing both here and in the main nav is the bug this fixes — the
|
|
61
|
+
* same item reachable from two places with two different chromes (an auction
|
|
62
|
+
* card in the general grid renders Buy-Now chrome, not bid affordances).
|
|
46
63
|
*/
|
|
47
64
|
export declare const GENERIC_PRODUCT_LISTING_TYPES: readonly ListingType[];
|
|
48
65
|
/** The listing types the combined `/art` page spans. */
|
|
@@ -60,9 +77,16 @@ export type SellerListingTabId = string;
|
|
|
60
77
|
* In-page type-filter chips on the generic `/products` browse page.
|
|
61
78
|
*
|
|
62
79
|
* Multi-select (checkbox semantics) — no "All" sentinel chip, because "none
|
|
63
|
-
* checked" already means "every type". A sentinel
|
|
64
|
-
* express the same state and would have to be cleared
|
|
65
|
-
* was ticked.
|
|
80
|
+
* checked" already means "every type IN THE GENERAL CATALOGUE". A sentinel
|
|
81
|
+
* would be a second way to express the same state and would have to be cleared
|
|
82
|
+
* whenever a real chip was ticked.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 Derived from `GENERIC_PRODUCT_LISTING_TYPES`, NOT `ALL_LISTING_TYPES`. The
|
|
85
|
+
* chips must offer exactly the types the page can actually return: a chip for a
|
|
86
|
+
* type outside the span intersects to empty, and `listPublicProducts` then falls
|
|
87
|
+
* back to the full span — i.e. ticking it would silently widen the results
|
|
88
|
+
* instead of narrowing them. `ADMIN_PRODUCT_LISTING_TYPE_TABS` is a separate
|
|
89
|
+
* constant and correctly stays all-nine.
|
|
66
90
|
*/
|
|
67
91
|
export declare const PRODUCT_TYPE_FILTER_TABS: readonly ListingTab[];
|
|
68
92
|
/** In-page type-filter chips on the combined `/art` (Art & Stickers) page. */
|