@mohasinac/appkit 4.11.2 → 4.11.3
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/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +111 -4
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +19 -0
- package/dist/_internal/server/features/products/list-public.js +39 -6
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +5 -1
- package/dist/client.js +4 -1
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/cart/schemas/firestore.d.ts +0 -2
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +5 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -0
- package/dist/features/orders/repository/orders.repository.js +4 -0
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +26 -3
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +11 -2
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -16,10 +16,11 @@ import { CustomSectionTabContent } from "../../products/components/CustomSection
|
|
|
16
16
|
import { ShareButton } from "../../products/components/ShareButton";
|
|
17
17
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
18
18
|
import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
|
|
19
|
-
import { computeRelatedItems,
|
|
19
|
+
import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
|
|
20
20
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
21
21
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
22
|
-
import {
|
|
22
|
+
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
23
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
23
24
|
function toDescriptionHtml(raw) {
|
|
24
25
|
if (!raw)
|
|
25
26
|
return "";
|
|
@@ -50,10 +51,10 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
50
51
|
const specs = Array.isArray(p.specifications) ? p.specifications : [];
|
|
51
52
|
const customSections = Array.isArray(p.customSections) ? p.customSections : [];
|
|
52
53
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
53
|
-
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups,
|
|
54
|
+
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
|
|
54
55
|
computeRelatedItems(product),
|
|
55
56
|
getGroupsWithItemsForProduct(product.id),
|
|
56
|
-
|
|
57
|
+
getReviewPageForProduct(product.id),
|
|
57
58
|
]);
|
|
58
59
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
|
|
59
60
|
title,
|
|
@@ -61,19 +62,19 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
61
62
|
price: price ?? undefined,
|
|
62
63
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
63
64
|
storeName: storeName ?? undefined,
|
|
64
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(
|
|
65
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
65
66
|
id: s.id,
|
|
66
67
|
label: s.title,
|
|
67
68
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
68
|
-
})) })), renderBuyBar: () => renderPrimaryActions?.({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
})) })), renderBuyBar: () => (_jsx(Div, { id: "digital-code-buy-bar", children: renderPrimaryActions?.({
|
|
70
|
+
productId: String(product.id),
|
|
71
|
+
productSlug: String(p.slug ?? product.id),
|
|
72
|
+
productTitle: title,
|
|
73
|
+
productImage: images[0],
|
|
74
|
+
price,
|
|
75
|
+
currency,
|
|
76
|
+
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
77
|
+
storeName: storeName ?? undefined,
|
|
78
|
+
inStock: codesLeft > 0,
|
|
79
|
+
}) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "digital-code", anchorId: "digital-code-buy-bar", price: price, currency: currency, unavailable: codesLeft <= 0 })] })] }));
|
|
79
80
|
}
|
|
@@ -13,5 +13,5 @@ export function FeaturedResultsSection({ title, subtitle, pillLabel, ornamentLab
|
|
|
13
13
|
afterImage: item.afterImage,
|
|
14
14
|
caption: item.caption ?? "",
|
|
15
15
|
sortOrder: item.sortOrder ?? i,
|
|
16
|
-
} }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: CAROUSEL_PER_VIEW.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true,
|
|
16
|
+
} }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: CAROUSEL_PER_VIEW.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true, loop: true }) })] }));
|
|
17
17
|
}
|
|
@@ -48,5 +48,5 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
|
|
|
48
48
|
// Double-row grids (e.g. the homepage Auctions section's
|
|
49
49
|
// rows:2 config) get the full-height sliver arrows — much
|
|
50
50
|
// easier to hit than a small circle when the track is tall.
|
|
51
|
-
arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true,
|
|
51
|
+
arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: hand === "left" ? "end" : "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, shadow: useLightText ? "lg" : "none", className: "inline-flex items-[center] gap-[0.375rem] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] })] }));
|
|
52
52
|
}
|
|
@@ -69,5 +69,5 @@ export function ShopByCategorySection({ title = "Shop by Category", subtitle, li
|
|
|
69
69
|
if (!cat)
|
|
70
70
|
return null;
|
|
71
71
|
return (_jsx(FilterChip, { label: cat.name, active: activeFilter === id, onClick: () => setActiveFilter(activeFilter === id ? "all" : id) }, id));
|
|
72
|
-
})] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true,
|
|
72
|
+
})] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
|
|
73
73
|
}
|
|
@@ -170,9 +170,16 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
170
170
|
const handleBeforeDashboardNavToggle = useCallback(() => {
|
|
171
171
|
setSidebarOpen(false);
|
|
172
172
|
}, []);
|
|
173
|
+
// Must stay in sync with BottomActions' own `isVisible` — `secondaryLabel` was missing
|
|
174
|
+
// here, so an auction bar shown by its countdown alone got no bottom clearance.
|
|
173
175
|
const hasBottomActions = bottomActionsState.actions.length > 0 ||
|
|
174
176
|
!!(bottomActionsState.bulk && bottomActionsState.bulk.selectedCount > 0) ||
|
|
175
|
-
!!bottomActionsState.infoLabel
|
|
177
|
+
!!bottomActionsState.infoLabel ||
|
|
178
|
+
!!bottomActionsState.secondaryLabel;
|
|
179
|
+
/** The bar overlays desktop content too once a page opts in, so reserve space there. */
|
|
180
|
+
const hasDesktopBottomActions = hasBottomActions &&
|
|
181
|
+
(bottomActionsState.desktop === "always" ||
|
|
182
|
+
bottomActionsState.desktop === "after-scroll");
|
|
176
183
|
const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
|
|
177
184
|
const normalizedLightBackground = {
|
|
178
185
|
type: lightBackground.type,
|
|
@@ -192,5 +199,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
192
199
|
opacity: darkBackground.overlay?.opacity ?? 0,
|
|
193
200
|
},
|
|
194
201
|
};
|
|
195
|
-
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} lg:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-[var(--appkit-space-5)] md:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)]"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, userId: userId, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, wishlistHref: wishlistHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
|
|
202
|
+
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} ${hasDesktopBottomActions ? "lg:mb-24" : "lg:mb-0"}`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-[var(--appkit-space-5)] md:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)]"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, userId: userId, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, wishlistHref: wishlistHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
|
|
196
203
|
}
|
|
@@ -18,7 +18,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* styled with the selected action's variant (danger = red, etc.).
|
|
19
19
|
*
|
|
20
20
|
* Layout rules:
|
|
21
|
-
* - Hidden on lg+ screens
|
|
21
|
+
* - Hidden on lg+ screens by default — desktop shows inline action panels. A page can
|
|
22
|
+
* opt in by passing `desktop: "after-scroll" | "always"` to `useBottomActions`;
|
|
23
|
+
* "after-scroll" brings the bar back once the primary CTA has scrolled out of view.
|
|
22
24
|
* - The bar slides up with a 300 ms ease-out transition; `pointer-events-none`
|
|
23
25
|
* while off-screen.
|
|
24
26
|
*
|
|
@@ -38,6 +40,8 @@ const Z_BOTTOM_ACTIONS = "z-[var(--appkit-z-bottom-nav)]";
|
|
|
38
40
|
const BOTTOM_NAV_HEIGHT = "h-14";
|
|
39
41
|
const FLEX_CENTER = "flex items-center justify-center";
|
|
40
42
|
const CLS_COUNT_BADGE = "bg-error-surface text-white";
|
|
43
|
+
/** How far the user must scroll before the desktop bar reveals itself. */
|
|
44
|
+
const DESKTOP_REVEAL_SCROLL_PX = 400;
|
|
41
45
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
42
46
|
function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect, }) {
|
|
43
47
|
return (_jsx(Div, { role: "listbox", "aria-label": "Bulk actions", className: [
|
|
@@ -85,7 +89,23 @@ export default function BottomActions() {
|
|
|
85
89
|
const [selectedActionId, setSelectedActionId] = useState(null);
|
|
86
90
|
const containerRef = useRef(null);
|
|
87
91
|
useClickOutside(containerRef, () => setPickerOpen(false));
|
|
88
|
-
const { actions, bulk, infoLabel, secondaryLabel } = state;
|
|
92
|
+
const { actions, bulk, infoLabel, secondaryLabel, desktop } = state;
|
|
93
|
+
// Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
|
|
94
|
+
// there is no shared scroll-position hook in the codebase and one consumer doesn't
|
|
95
|
+
// justify inventing one. Only armed when a page actually opts into "after-scroll".
|
|
96
|
+
const [scrolledPastFold, setScrolledPastFold] = useState(false);
|
|
97
|
+
const wantsScrollGate = desktop === "after-scroll";
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!wantsScrollGate) {
|
|
100
|
+
setScrolledPastFold(false);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const onScroll = () => setScrolledPastFold(window.scrollY > DESKTOP_REVEAL_SCROLL_PX);
|
|
104
|
+
onScroll();
|
|
105
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
106
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
107
|
+
}, [wantsScrollGate]);
|
|
108
|
+
const showOnDesktop = desktop === "always" || (wantsScrollGate && scrolledPastFold);
|
|
89
109
|
const isBulkMode = !!(bulk && bulk.selectedCount > 0);
|
|
90
110
|
const bulkActions = bulk?.actions ?? [];
|
|
91
111
|
const pageActions = actions;
|
|
@@ -120,7 +140,10 @@ export default function BottomActions() {
|
|
|
120
140
|
setPickerOpen(false);
|
|
121
141
|
};
|
|
122
142
|
return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", "aria-hidden": !isVisible, className: [
|
|
123
|
-
"fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0
|
|
143
|
+
"fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0",
|
|
144
|
+
// `--bottom-nav-height` resolves to 0px above lg, so the same offset lands the
|
|
145
|
+
// bar on the viewport bottom on desktop with no extra positioning.
|
|
146
|
+
showOnDesktop ? "" : "lg:hidden",
|
|
124
147
|
Z_BOTTOM_ACTIONS,
|
|
125
148
|
BOTTOM_NAV_BG,
|
|
126
149
|
"shadow-[0_-4px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_-4px_20px_rgba(0,0,0,0.30)]",
|
|
@@ -69,9 +69,21 @@ export interface BottomBulkConfig {
|
|
|
69
69
|
/** Bulk action buttons, rendered right of the selection pill. */
|
|
70
70
|
actions: BottomAction[];
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the bar is allowed to show on desktop (>= lg), where it has always been
|
|
74
|
+
* `lg:hidden`.
|
|
75
|
+
*
|
|
76
|
+
* - `"hidden"` (default) — mobile/tablet only, the historical behaviour.
|
|
77
|
+
* - `"after-scroll"` — appears once the user has scrolled past the top of the page,
|
|
78
|
+
* so a detail page's primary CTA comes back after the hero scrolls away.
|
|
79
|
+
* - `"always"` — pinned on desktop from first paint.
|
|
80
|
+
*/
|
|
81
|
+
export type BottomActionsDesktopMode = "hidden" | "after-scroll" | "always";
|
|
72
82
|
export interface BottomActionsState {
|
|
73
83
|
/** Page-level primary actions. */
|
|
74
84
|
actions: BottomAction[];
|
|
85
|
+
/** Desktop visibility policy. Defaults to "hidden" — opt in per page. */
|
|
86
|
+
desktop?: BottomActionsDesktopMode;
|
|
75
87
|
/** Bulk-selection config. Bulk mode activates when selectedCount > 0. */
|
|
76
88
|
bulk?: BottomBulkConfig;
|
|
77
89
|
/**
|
|
@@ -107,6 +119,8 @@ interface BottomActionsContextValue {
|
|
|
107
119
|
setInfoLabel: (label: string | undefined) => void;
|
|
108
120
|
/** Set or clear the secondary label (rendered above infoLabel). */
|
|
109
121
|
setSecondaryLabel: (label: string | undefined) => void;
|
|
122
|
+
/** Set the desktop visibility policy. */
|
|
123
|
+
setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
|
|
110
124
|
/** Clear all state (called on feature unmount). */
|
|
111
125
|
clearAll: () => void;
|
|
112
126
|
}
|
|
@@ -87,6 +87,9 @@ export function BottomActionsProvider({ children, }) {
|
|
|
87
87
|
const setSecondaryLabel = useCallback((secondaryLabel) => {
|
|
88
88
|
setState((prev) => ({ ...prev, secondaryLabel }));
|
|
89
89
|
}, []);
|
|
90
|
+
const setDesktopMode = useCallback((desktop) => {
|
|
91
|
+
setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
|
|
92
|
+
}, []);
|
|
90
93
|
const clearAll = useCallback(() => {
|
|
91
94
|
actionCallbacksRef.current = new Map();
|
|
92
95
|
bulkCallbacksRef.current = new Map();
|
|
@@ -102,8 +105,17 @@ export function BottomActionsProvider({ children, }) {
|
|
|
102
105
|
bulkClearRef,
|
|
103
106
|
setInfoLabel,
|
|
104
107
|
setSecondaryLabel,
|
|
108
|
+
setDesktopMode,
|
|
109
|
+
clearAll,
|
|
110
|
+
}), [
|
|
111
|
+
state,
|
|
112
|
+
setActions,
|
|
113
|
+
setBulkConfig,
|
|
114
|
+
setInfoLabel,
|
|
115
|
+
setSecondaryLabel,
|
|
116
|
+
setDesktopMode,
|
|
105
117
|
clearAll,
|
|
106
|
-
|
|
118
|
+
]);
|
|
107
119
|
return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
|
|
108
120
|
}
|
|
109
121
|
// --- Hook ---------------------------------------------------------------------
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { type BottomAction, type BottomBulkConfig } from "../BottomActionsContext";
|
|
1
|
+
import { type BottomAction, type BottomActionsDesktopMode, type BottomBulkConfig } from "../BottomActionsContext";
|
|
2
2
|
export interface UseBottomActionsOptions {
|
|
3
3
|
actions?: BottomAction[];
|
|
4
4
|
bulk?: BottomBulkConfig;
|
|
5
5
|
infoLabel?: string;
|
|
6
6
|
/** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
|
|
7
7
|
secondaryLabel?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
|
|
10
|
+
* so pages that don't opt in behave exactly as before.
|
|
11
|
+
*/
|
|
12
|
+
desktop?: BottomActionsDesktopMode;
|
|
8
13
|
}
|
|
9
14
|
export declare function useBottomActions(options?: UseBottomActionsOptions): void;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useBottomActionsContext, } from "../BottomActionsContext";
|
|
4
4
|
export function useBottomActions(options = {}) {
|
|
5
|
-
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll } = useBottomActionsContext();
|
|
5
|
+
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setDesktopMode, clearAll, } = useBottomActionsContext();
|
|
6
6
|
const actionsRef = useRef(options.actions ?? []);
|
|
7
7
|
actionsRef.current = options.actions ?? [];
|
|
8
8
|
const bulkRef = useRef(options.bulk);
|
|
@@ -25,6 +25,9 @@ export function useBottomActions(options = {}) {
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
setSecondaryLabel(options.secondaryLabel);
|
|
27
27
|
}, [setSecondaryLabel, options.secondaryLabel]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setDesktopMode(options.desktop);
|
|
30
|
+
}, [setDesktopMode, options.desktop]);
|
|
28
31
|
useEffect(() => {
|
|
29
32
|
return () => clearAll();
|
|
30
33
|
}, [clearAll]);
|
|
@@ -27,7 +27,7 @@ export type { LayoutClientProps, LayoutProvider } from "./LayoutClient";
|
|
|
27
27
|
export { AppLayoutShell } from "./AppLayoutShell";
|
|
28
28
|
export type { AppLayoutShellProps, AppLayoutShellSidebarLink, AppLayoutShellSidebarSection } from "./AppLayoutShell";
|
|
29
29
|
export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
|
|
30
|
-
export type { BottomAction, BottomBulkConfig, BottomActionsState, } from "./BottomActionsContext";
|
|
30
|
+
export type { BottomAction, BottomBulkConfig, BottomActionsState, BottomActionsDesktopMode, } from "./BottomActionsContext";
|
|
31
31
|
export { useBottomActions } from "./hooks/useBottomActions";
|
|
32
32
|
export type { UseBottomActionsOptions } from "./hooks/useBottomActions";
|
|
33
33
|
export { default as BottomActions } from "./BottomActions";
|
|
@@ -19,10 +19,11 @@ import { CustomSectionTabContent } from "../../products/components/CustomSection
|
|
|
19
19
|
import { ShareButton } from "../../products/components/ShareButton";
|
|
20
20
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
21
21
|
import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
|
|
22
|
-
import { computeRelatedItems,
|
|
22
|
+
import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
|
|
23
23
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
24
24
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
25
|
-
import {
|
|
25
|
+
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
26
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
26
27
|
function toDescriptionHtml(raw) {
|
|
27
28
|
if (!raw)
|
|
28
29
|
return "";
|
|
@@ -55,10 +56,10 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
55
56
|
const specs = Array.isArray(p.specifications) ? p.specifications : [];
|
|
56
57
|
const customSections = Array.isArray(p.customSections) ? p.customSections : [];
|
|
57
58
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
58
|
-
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups,
|
|
59
|
+
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
|
|
59
60
|
computeRelatedItems(product),
|
|
60
61
|
getGroupsWithItemsForProduct(product.id),
|
|
61
|
-
|
|
62
|
+
getReviewPageForProduct(product.id),
|
|
62
63
|
]);
|
|
63
64
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
|
|
64
65
|
title,
|
|
@@ -66,9 +67,9 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
66
67
|
price: price ?? undefined,
|
|
67
68
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
68
69
|
storeName: storeName ?? undefined,
|
|
69
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(
|
|
70
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
70
71
|
id: s.id,
|
|
71
72
|
label: s.title,
|
|
72
73
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
73
|
-
})) })), renderBuyBar: () => renderActions?.(product), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
|
|
74
|
+
})) })), renderBuyBar: () => _jsx(Div, { id: "live-item-buy-bar", children: renderActions?.(product) }), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
|
|
74
75
|
}
|
|
@@ -175,6 +175,10 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
175
175
|
canFilter: boolean;
|
|
176
176
|
canSort: boolean;
|
|
177
177
|
};
|
|
178
|
+
orderType: {
|
|
179
|
+
canFilter: boolean;
|
|
180
|
+
canSort: boolean;
|
|
181
|
+
};
|
|
178
182
|
};
|
|
179
183
|
listAll(model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
180
184
|
/**
|
|
@@ -516,6 +516,10 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
|
|
|
516
516
|
// S-SBUNI-RULES 2026-05-13 — refund + batch fields
|
|
517
517
|
paymentBatchId: { canFilter: true, canSort: false },
|
|
518
518
|
contestable: { canFilter: true, canSort: false },
|
|
519
|
+
// Lane segregation on /user/orders — "standard"/"auction"/"offer" etc.
|
|
520
|
+
// Never filtered as "standard" via Firestore == (see listForUser callers):
|
|
521
|
+
// orders written before this field existed have no value to match.
|
|
522
|
+
orderType: { canFilter: true, canSort: false },
|
|
519
523
|
};
|
|
520
524
|
const orderRepository = new OrderRepository();
|
|
521
525
|
export { OrderRepository, orderRepository };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type PaymentGateway, PaymentGatewayValues } from "../../payments/schemas";
|
|
2
2
|
export { type PaymentGateway, PaymentGatewayValues };
|
|
3
3
|
import type { ListingType } from "../../products/types/index";
|
|
4
|
+
import type { OrderType } from "../utils/order-splitter";
|
|
5
|
+
import type { AppliedOrderDiscount } from "../schemas/firestore";
|
|
4
6
|
export type PaymentStatus = "pending" | "processing" | "paid" | "failed" | "refunded" | "partial_refund";
|
|
5
7
|
export interface UserAddress {
|
|
6
8
|
id: string;
|
|
@@ -57,6 +59,13 @@ export interface Order {
|
|
|
57
59
|
address: UserAddress;
|
|
58
60
|
orderStatus: OrderStatus;
|
|
59
61
|
paymentStatus: PaymentStatus;
|
|
62
|
+
/**
|
|
63
|
+
* Which checkout lane produced this order. Absent on orders written before
|
|
64
|
+
* the field existed — treat missing as `"standard"`, never filter it out.
|
|
65
|
+
*/
|
|
66
|
+
orderType?: OrderType;
|
|
67
|
+
/** Set when this order settled an accepted Make-an-Offer. */
|
|
68
|
+
offerId?: string;
|
|
60
69
|
paymentGateway?: PaymentGateway;
|
|
61
70
|
subtotal: number;
|
|
62
71
|
shippingCost?: number;
|
|
@@ -64,7 +73,14 @@ export interface Order {
|
|
|
64
73
|
tax?: number;
|
|
65
74
|
total: number;
|
|
66
75
|
currency: string;
|
|
76
|
+
/**
|
|
77
|
+
* First applied coupon's code only. Kept for back-compat; prefer
|
|
78
|
+
* `appliedDiscounts` when rendering, since a cart may stack one coupon per
|
|
79
|
+
* store plus one platform-wide coupon and this scalar shows only one of them.
|
|
80
|
+
*/
|
|
67
81
|
couponCode?: string;
|
|
82
|
+
/** Every discount applied to this order — the full stack, not just the first. */
|
|
83
|
+
appliedDiscounts?: AppliedOrderDiscount[];
|
|
68
84
|
trackingNumber?: string;
|
|
69
85
|
shippingCarrier?: string;
|
|
70
86
|
/** Carrier tracking page URL — rendered as an external link that opens in a new tab. No live-tracking API integration; this is just the passthrough URL a seller/admin entered. */
|
|
@@ -2,4 +2,4 @@ export interface PreOrderBottomActionsProps {
|
|
|
2
2
|
price: number | null;
|
|
3
3
|
currency: string;
|
|
4
4
|
}
|
|
5
|
-
export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps):
|
|
5
|
+
export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ACTION_ID } from "../../products/constants/action-defs";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
5
4
|
export function PreOrderBottomActions({ price, currency, }) {
|
|
6
|
-
|
|
7
|
-
actions: [
|
|
8
|
-
{
|
|
9
|
-
id: ACTION_ID.RESERVE_NOW,
|
|
10
|
-
label: "Reserve Now",
|
|
11
|
-
variant: "primary",
|
|
12
|
-
onClick: () => {
|
|
13
|
-
document
|
|
14
|
-
.getElementById("pre-order-buy-bar")
|
|
15
|
-
?.scrollIntoView({ behavior: "smooth" });
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
infoLabel: price !== null ? formatCurrency(price, currency) : undefined,
|
|
20
|
-
});
|
|
21
|
-
return null;
|
|
5
|
+
return (_jsx(ListingBottomActions, { listingType: "preorder", anchorId: "pre-order-buy-bar", price: price, currency: currency }));
|
|
22
6
|
}
|
|
@@ -24,7 +24,6 @@ const __O = {
|
|
|
24
24
|
hidden: "overflow-hidden",
|
|
25
25
|
};
|
|
26
26
|
const DEFAULT_SORT = AUCTION_PUBLIC_SORT_OPTIONS[0].value;
|
|
27
|
-
const AUCTION_SORT_OPTIONS = AUCTION_PUBLIC_SORT_OPTIONS;
|
|
28
27
|
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];
|
|
29
28
|
export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
30
29
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
@@ -132,7 +131,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
132
131
|
},
|
|
133
132
|
},
|
|
134
133
|
] } } : {});
|
|
135
|
-
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:
|
|
134
|
+
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, toggles: [
|
|
136
135
|
{ label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") },
|
|
137
136
|
] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
138
137
|
{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
|
|
2
|
+
/**
|
|
3
|
+
* The sticky-CTA registrar shared by every listing type whose bottom bar is just
|
|
4
|
+
* "primary CTA that scrolls to the buy/contact panel, plus a price label".
|
|
5
|
+
*
|
|
6
|
+
* Pre-order, prize-draw, classified, digital-code and live all fit that shape, which put
|
|
7
|
+
* it well past the Rule of Three for a per-type copy. Auction and standard product keep
|
|
8
|
+
* their own registrars — the auction ticks a live countdown into `secondaryLabel`, and
|
|
9
|
+
* the product bar carries three buttons whose labels track cart/wishlist state.
|
|
10
|
+
*
|
|
11
|
+
* Renders nothing; it exists purely to register into `BottomActionsContext`.
|
|
12
|
+
*/
|
|
13
|
+
export interface ListingBottomActionsProps {
|
|
14
|
+
/** Selects the CTA set from the ACTIONS registry (Rule #7 — never inline actions). */
|
|
15
|
+
listingType: keyof typeof MOBILE_PRIMARY_ACTIONS;
|
|
16
|
+
/** `id` of the element the CTA scrolls to. */
|
|
17
|
+
anchorId: string;
|
|
18
|
+
price: number | null;
|
|
19
|
+
currency: string;
|
|
20
|
+
/**
|
|
21
|
+
* Register nothing at all — for ended/closed/sold-out listings, matching how the
|
|
22
|
+
* auction and prize-draw registrars bail out.
|
|
23
|
+
*/
|
|
24
|
+
unavailable?: boolean;
|
|
25
|
+
/** Appended after the formatted price, e.g. "per entry". */
|
|
26
|
+
infoSuffix?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function ListingBottomActions({ listingType, anchorId, price, currency, unavailable, infoSuffix, }: ListingBottomActionsProps): null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useBottomActions } from "../../layout/hooks/useBottomActions";
|
|
3
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
4
|
+
import { ACTION_META, MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
|
|
5
|
+
export function ListingBottomActions({ listingType, anchorId, price, currency, unavailable = false, infoSuffix, }) {
|
|
6
|
+
const actionIds = MOBILE_PRIMARY_ACTIONS[listingType] ?? [];
|
|
7
|
+
const infoLabel = price !== null
|
|
8
|
+
? `${formatCurrency(price, currency)}${infoSuffix ? ` ${infoSuffix}` : ""}`
|
|
9
|
+
: undefined;
|
|
10
|
+
useBottomActions(unavailable
|
|
11
|
+
? {}
|
|
12
|
+
: {
|
|
13
|
+
actions: actionIds.map((id) => ({
|
|
14
|
+
...ACTION_META[id],
|
|
15
|
+
onClick: () => {
|
|
16
|
+
document
|
|
17
|
+
.getElementById(anchorId)
|
|
18
|
+
?.scrollIntoView({ behavior: "smooth" });
|
|
19
|
+
},
|
|
20
|
+
})),
|
|
21
|
+
infoLabel,
|
|
22
|
+
desktop: "after-scroll",
|
|
23
|
+
});
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -3,4 +3,4 @@ export interface PrizeDrawBottomActionsProps {
|
|
|
3
3
|
currency: string;
|
|
4
4
|
closed: boolean;
|
|
5
5
|
}
|
|
6
|
-
export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps):
|
|
6
|
+
export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): import("react").JSX.Element;
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ACTION_ID } from "../constants/action-defs";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ListingBottomActions } from "./ListingBottomActions";
|
|
5
4
|
export function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }) {
|
|
6
|
-
|
|
7
|
-
? {}
|
|
8
|
-
: {
|
|
9
|
-
actions: [
|
|
10
|
-
{
|
|
11
|
-
id: ACTION_ID.BUY_NOW,
|
|
12
|
-
label: "Buy Now",
|
|
13
|
-
variant: "primary",
|
|
14
|
-
onClick: () => {
|
|
15
|
-
document
|
|
16
|
-
.getElementById("prize-draw-buy-bar")
|
|
17
|
-
?.scrollIntoView({ behavior: "smooth" });
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
infoLabel: `${formatCurrency(pricePerEntry, currency)} per entry`,
|
|
22
|
-
});
|
|
23
|
-
return null;
|
|
5
|
+
return (_jsx(ListingBottomActions, { listingType: "prize-draw", anchorId: "prize-draw-buy-bar", price: pricePerEntry, currency: currency, unavailable: closed, infoSuffix: "per entry" }));
|
|
24
6
|
}
|
|
@@ -122,6 +122,10 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
|
|
|
122
122
|
infoLabel: price != null && currency
|
|
123
123
|
? formatCurrency(price, currency)
|
|
124
124
|
: undefined,
|
|
125
|
+
// The bar itself owns responsive visibility now, so this one registrar serves
|
|
126
|
+
// both breakpoints. The desktop branch below must NOT also register —
|
|
127
|
+
// useBottomActions is last-mounted-wins and the two would clobber each other.
|
|
128
|
+
desktop: "after-scroll",
|
|
125
129
|
}
|
|
126
130
|
: {});
|
|
127
131
|
const authModal = (_jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage }));
|