@mohasinac/appkit 4.11.1 → 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 +138 -27
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +116 -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 +2 -1
- package/dist/_internal/server/features/products/index.js +2 -1
- package/dist/_internal/server/features/products/list-public.d.ts +130 -0
- package/dist/_internal/server/features/products/list-public.js +375 -0
- 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/auctionSettlement.js +89 -16
- package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
- 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/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- 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/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- 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 +7 -2
- package/dist/client.js +8 -2
- 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/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/actions/notification-actions.js +1 -1
- 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/admin/schemas/firestore.d.ts +2 -1
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
- package/dist/features/auctions/actions/bid-actions.js +29 -15
- package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/auctions/components/AuctionsListView.js +12 -53
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
- package/dist/features/auctions/repository/bid.repository.js +19 -0
- package/dist/features/auctions/schemas/firestore.d.ts +11 -1
- package/dist/features/auctions/schemas/firestore.js +1 -0
- package/dist/features/cart/actions/cart-actions.d.ts +11 -0
- package/dist/features/cart/actions/cart-actions.js +24 -0
- package/dist/features/cart/repository/cart.repository.d.ts +24 -1
- package/dist/features/cart/repository/cart.repository.js +71 -6
- package/dist/features/cart/schemas/firestore.d.ts +23 -4
- 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 +56 -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 -15
- package/dist/features/orders/repository/orders.repository.js +4 -27
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- 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/components/ProductsIndexPageView.js +9 -62
- 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 +67 -8
- 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/actions/offer-actions.d.ts +6 -0
- package/dist/features/seller/actions/offer-actions.js +8 -9
- package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
- package/dist/features/seller/components/SellerOffersView.js +59 -8
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/seller/repository/offer.repository.d.ts +18 -2
- package/dist/features/seller/repository/offer.repository.js +38 -2
- package/dist/features/seller/schemas/firestore.d.ts +7 -2
- package/dist/features/seller/schemas/firestore.js +3 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
- package/dist/features/seller/schemas/offer-forms.js +28 -0
- 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 +11 -4
- package/dist/index.js +15 -3
- 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/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- 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
|
@@ -9,8 +9,6 @@ const __P = {
|
|
|
9
9
|
};
|
|
10
10
|
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
|
|
11
11
|
const CLS_LIVE_BADGE = "inline-block rounded-full bg-warning-surface dark:bg-warning-surface px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-warning dark:text-warning";
|
|
12
|
-
const CLS_STAR_ON = "text-warning";
|
|
13
|
-
const CLS_STAR_OFF = "text-zinc-200 dark:text-zinc-700";
|
|
14
12
|
import { ROUTES } from "../../../next";
|
|
15
13
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
16
14
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -30,7 +28,7 @@ import { FeatureBadgeList } from "../../products/components/FeatureBadge";
|
|
|
30
28
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
31
29
|
import { ShareButton } from "../../products/components/ShareButton";
|
|
32
30
|
import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid";
|
|
33
|
-
import {
|
|
31
|
+
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
34
32
|
import { PlaceBidModalButton } from "./PlaceBidFormClient";
|
|
35
33
|
import { CollapsibleBidHistory } from "./CollapsibleBidHistory";
|
|
36
34
|
import { LiveBidPrice } from "./LiveBidPrice";
|
|
@@ -50,11 +48,18 @@ function renderAuctionInfoPanel(props) {
|
|
|
50
48
|
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
51
49
|
return (_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_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowPrice !== null && !isEnded && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed 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" })] }) }))] }));
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Store reviews for the auction's seller.
|
|
53
|
+
*
|
|
54
|
+
* Was a bespoke `slice(0, 10)` block over `listReviewsBySeller()` — which concatenates
|
|
55
|
+
* per-product batches, so the "latest 10" it showed were in product order, not date
|
|
56
|
+
* order, and reviews 11+ were unreachable. `ReviewsListingPanel` pages the real
|
|
57
|
+
* store-reviews endpoint instead.
|
|
58
|
+
*/
|
|
59
|
+
function renderAuctionStoreReviews(storeSlug) {
|
|
60
|
+
if (!storeSlug)
|
|
55
61
|
return null;
|
|
56
|
-
|
|
57
|
-
return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsxs(Row, { className: "mb-4", align: "center", gap: "3", children: [_jsx(Span, { weight: "bold", size: "3xl", color: "primary", children: avg.toFixed(1) }), _jsxs(Div, { children: [_jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { className: star <= Math.round(avg) ? CLS_STAR_ON : CLS_STAR_OFF, children: "\u2605" }, star)) }), _jsxs(Text, { size: "xs", color: "muted", children: [storeReviews.length, " review", storeReviews.length !== 1 ? "s" : ""] })] })] }), _jsx(Stack, { gap: "sm", children: storeReviews.slice(0, 10).map((review) => (_jsxs(Stack, { gap: "xs", surface: "card", padding: "sm", className: "", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { size: "sm", weight: "medium", color: "primary", children: review.userName }), _jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { size: "xs", className: star <= review.rating ? CLS_STAR_ON : CLS_STAR_OFF, children: "\u2605" }, star)) })] }) }), review.title && _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: review.title }), _jsx(Text, { className: "leading-relaxed", color: "muted", size: "sm", children: review.comment }), _jsx(Text, { size: "xs", color: "faint", children: review.productTitle })] }, review.id))) })] }));
|
|
62
|
+
return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsx(ReviewsListingPanel, { source: { kind: "store", storeSlug }, stateMode: "local", context: "store", emptyLabel: "This store has no reviews yet." })] }));
|
|
58
63
|
}
|
|
59
64
|
export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }) {
|
|
60
65
|
const product = initialAuction !== undefined
|
|
@@ -64,9 +69,6 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
64
69
|
? await listBidsByProduct(String(product.id), { pageSize: 5 }).catch(() => null)
|
|
65
70
|
: null;
|
|
66
71
|
const storeId = product?.storeId;
|
|
67
|
-
const storeReviews = storeId
|
|
68
|
-
? await listReviewsBySeller(storeId).catch(() => [])
|
|
69
|
-
: [];
|
|
70
72
|
if (!product) {
|
|
71
73
|
return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Auction Not Found" }), _jsx(Text, { color: "muted", children: "This auction may have ended or the link is incorrect." }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Auctions" })] }) }) }) }));
|
|
72
74
|
}
|
|
@@ -191,5 +193,5 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
191
193
|
slug: typeof r.slug === "string" ? r.slug : undefined,
|
|
192
194
|
}));
|
|
193
195
|
return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related, gridClassName: "grid grid-cols-2 gap-[var(--appkit-space-4)] sm:grid-cols-3 lg:grid-cols-4" })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
|
|
194
|
-
} }), renderAuctionStoreReviews(
|
|
196
|
+
} }), renderAuctionStoreReviews(storeId), _jsx(AuctionBottomActions, { currentBid: currentBid, currency: currency, bidCount: bidCount, isEnded: isEnded, auctionEndDate: endDate })] })] }));
|
|
195
197
|
}
|
|
@@ -1,62 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
3
|
-
import { productRepository } from "../../../repositories";
|
|
4
2
|
import { Container, Heading, Main, Section } from "../../../ui";
|
|
5
3
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
6
|
-
import { parseListingSearchParams } from "../../../utils/listing-params";
|
|
7
4
|
import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing";
|
|
8
|
-
|
|
5
|
+
import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
|
|
6
|
+
const AUCTION_LISTING_TYPES = ["auction"];
|
|
9
7
|
const DEFAULT_PAGE_SIZE = 24;
|
|
10
8
|
const DEFAULT_SORT = "auctionEndDate";
|
|
11
|
-
function sp(params, key) {
|
|
12
|
-
const v = params[key];
|
|
13
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
14
|
-
}
|
|
15
|
-
function buildAuctionFilters(params) {
|
|
16
|
-
const parts = ["status==published", "listingType==auction"];
|
|
17
|
-
const minBid = sp(params, "minBid");
|
|
18
|
-
const maxBid = sp(params, "maxBid");
|
|
19
|
-
if (minBid)
|
|
20
|
-
parts.push(sieveFilter("currentBid", SIEVE_OP.GTE, minBid));
|
|
21
|
-
if (maxBid)
|
|
22
|
-
parts.push(sieveFilter("currentBid", SIEVE_OP.LTE, maxBid));
|
|
23
|
-
const store = sp(params, "store");
|
|
24
|
-
if (store) {
|
|
25
|
-
const values = store.split("|").filter(Boolean);
|
|
26
|
-
if (values.length === 1)
|
|
27
|
-
parts.push(sieveFilter("storeId", SIEVE_OP.EQ, values[0]));
|
|
28
|
-
else if (values.length > 1)
|
|
29
|
-
parts.push(sieveFilter("storeId", SIEVE_OP.EQ, values.join("|")));
|
|
30
|
-
}
|
|
31
|
-
// Mirror AuctionsIndexListing's client-side default (Root Cause pattern —
|
|
32
|
-
// SSR initialData is seeded into React Query with staleTime:Infinity, so if
|
|
33
|
-
// this SSR filter doesn't already exclude ended auctions, the client never
|
|
34
|
-
// refetches and ended auctions leak into the default "Show ended" off view).
|
|
35
|
-
const showEnded = sp(params, "showEnded") === "true";
|
|
36
|
-
const dateFrom = showEnded
|
|
37
|
-
? sp(params, "dateFrom")
|
|
38
|
-
: sp(params, "dateFrom") || new Date().toISOString();
|
|
39
|
-
const dateTo = sp(params, "dateTo");
|
|
40
|
-
if (dateFrom)
|
|
41
|
-
parts.push(sieveFilter("auctionEndDate", SIEVE_OP.GTE, dateFrom));
|
|
42
|
-
if (dateTo)
|
|
43
|
-
parts.push(sieveFilter("auctionEndDate", SIEVE_OP.LTE, dateTo));
|
|
44
|
-
return parts.join(",");
|
|
45
|
-
}
|
|
46
9
|
export async function AuctionsListView({ searchParams = {} }) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
pageSize,
|
|
58
|
-
})
|
|
59
|
-
.catch(() => null);
|
|
60
|
-
const initial = result ?? null;
|
|
10
|
+
// Shared with /api/products. `hideEndedByDefault` mirrors
|
|
11
|
+
// AuctionsIndexListing's `showEnded ? … : dateFrom=now` default; because the
|
|
12
|
+
// default sort IS auctionEndDate, listPublicProducts pushes that range into
|
|
13
|
+
// Firestore rather than filtering in memory.
|
|
14
|
+
const initial = await listPublicProducts(parsePublicProductParams(searchParams, {
|
|
15
|
+
listingTypes: AUCTION_LISTING_TYPES,
|
|
16
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
17
|
+
sorts: DEFAULT_SORT,
|
|
18
|
+
hideEndedByDefault: true,
|
|
19
|
+
}));
|
|
61
20
|
return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Live Auctions" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(AuctionsIndexListing, { initialData: initial }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
|
|
62
21
|
}
|
|
@@ -96,7 +96,16 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
setSuccess(true);
|
|
99
|
-
|
|
99
|
+
// Buy-Now writes a locked cart line, so send the buyer straight to the
|
|
100
|
+
// auction checkout lane to pay for it. This used to just call
|
|
101
|
+
// `router.refresh()` and discard the action's result entirely, leaving
|
|
102
|
+
// the buyer on the product page with no indication that anything was
|
|
103
|
+
// owed — the action returned an orderId nobody ever used.
|
|
104
|
+
const checkoutUrl = result?.checkoutUrl;
|
|
105
|
+
if (checkoutUrl)
|
|
106
|
+
router.push(checkoutUrl);
|
|
107
|
+
else
|
|
108
|
+
router.refresh();
|
|
100
109
|
}
|
|
101
110
|
catch (err) {
|
|
102
111
|
void normalizeError(err);
|
|
@@ -50,6 +50,21 @@ export declare class BidRepository extends BaseRepository<BidDocument> {
|
|
|
50
50
|
data: BidDocument;
|
|
51
51
|
} | null>;
|
|
52
52
|
markWon(batch: WriteBatch, ref: DocumentReference): void;
|
|
53
|
+
/**
|
|
54
|
+
* Link a won bid to the order it eventually became.
|
|
55
|
+
*
|
|
56
|
+
* `BidDocument` had no `orderId` at all before 2026-08-21, so a won bid and
|
|
57
|
+
* the thing the buyer paid for were two unrelated records — the "Won" row in
|
|
58
|
+
* /user/bids could not link anywhere, and nothing could tell a settled win
|
|
59
|
+
* from an unpaid one.
|
|
60
|
+
*/
|
|
61
|
+
attachOrder(bidId: string, orderId: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* The winner never paid within their checkout window. Distinct from "lost"
|
|
64
|
+
* (outbid) — this bidder won and then defaulted, which a seller may want to
|
|
65
|
+
* act on.
|
|
66
|
+
*/
|
|
67
|
+
markForfeited(bidId: string): Promise<void>;
|
|
53
68
|
markLost(batch: WriteBatch, ref: DocumentReference): void;
|
|
54
69
|
markOutbid(batch: WriteBatch, ref: DocumentReference): void;
|
|
55
70
|
markWinning(batch: WriteBatch, ref: DocumentReference): void;
|
|
@@ -122,6 +122,25 @@ export class BidRepository extends BaseRepository {
|
|
|
122
122
|
updatedAt: new Date(),
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Link a won bid to the order it eventually became.
|
|
127
|
+
*
|
|
128
|
+
* `BidDocument` had no `orderId` at all before 2026-08-21, so a won bid and
|
|
129
|
+
* the thing the buyer paid for were two unrelated records — the "Won" row in
|
|
130
|
+
* /user/bids could not link anywhere, and nothing could tell a settled win
|
|
131
|
+
* from an unpaid one.
|
|
132
|
+
*/
|
|
133
|
+
async attachOrder(bidId, orderId) {
|
|
134
|
+
await this.update(bidId, { orderId, updatedAt: new Date() });
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The winner never paid within their checkout window. Distinct from "lost"
|
|
138
|
+
* (outbid) — this bidder won and then defaulted, which a seller may want to
|
|
139
|
+
* act on.
|
|
140
|
+
*/
|
|
141
|
+
async markForfeited(bidId) {
|
|
142
|
+
await this.update(bidId, { status: "forfeited", isWinning: false, updatedAt: new Date() });
|
|
143
|
+
}
|
|
125
144
|
markLost(batch, ref) {
|
|
126
145
|
batch.update(ref, {
|
|
127
146
|
status: "lost",
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type GenerateBidIdInput } from "../../../utils/id-generators";
|
|
5
5
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
6
|
-
export type BidStatus = "active" | "outbid" | "won" | "lost" | "cancelled"
|
|
6
|
+
export type BidStatus = "active" | "outbid" | "won" | "lost" | "cancelled"
|
|
7
|
+
/** Won, then the buyer let the 48h payment window lapse. */
|
|
8
|
+
| "forfeited";
|
|
7
9
|
/** Runtime-accessible bid status values — use instead of bare string literals. */
|
|
8
10
|
export declare const BidStatusValues: {
|
|
9
11
|
readonly ACTIVE: "active";
|
|
@@ -11,6 +13,7 @@ export declare const BidStatusValues: {
|
|
|
11
13
|
readonly WON: "won";
|
|
12
14
|
readonly LOST: "lost";
|
|
13
15
|
readonly CANCELLED: "cancelled";
|
|
16
|
+
readonly FORFEITED: "forfeited";
|
|
14
17
|
};
|
|
15
18
|
export interface BidDocument extends BaseDocument {
|
|
16
19
|
productId: string;
|
|
@@ -25,6 +28,13 @@ export interface BidDocument extends BaseDocument {
|
|
|
25
28
|
previousBidAmount?: number;
|
|
26
29
|
bidDate: Date;
|
|
27
30
|
autoMaxBid?: number;
|
|
31
|
+
/**
|
|
32
|
+
* The order this winning bid was settled into. Written by
|
|
33
|
+
* `finalizeLockedLines` once the winner actually pays; absent on a won-but-
|
|
34
|
+
* unpaid bid, which is what lets /user/bids show a "Pay now" CTA rather than
|
|
35
|
+
* a dead status badge.
|
|
36
|
+
*/
|
|
37
|
+
orderId?: string;
|
|
28
38
|
}
|
|
29
39
|
export declare const BID_COLLECTION: "bids";
|
|
30
40
|
export declare const BID_INDEXED_FIELDS: readonly ["productId", "userId", "status", "isWinning", "bidDate", "createdAt"];
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AddToCartInput, CartDocument, UpdateCartItemInput } from "../schemas";
|
|
8
8
|
export declare function addItemToCart(userId: string, input: AddToCartInput): Promise<CartDocument>;
|
|
9
|
+
/** Error code the client maps to a lane-aware message. */
|
|
10
|
+
export declare const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
|
|
11
|
+
/**
|
|
12
|
+
* Won-auction lines are non-removable and fixed-quantity — the buyer committed
|
|
13
|
+
* to the purchase by bidding, and letting them delete it would turn every
|
|
14
|
+
* auction into a free option. That's already enforced one layer down:
|
|
15
|
+
* `cartRepository.updateItem` / `removeItem` both reject a `locked` line, and
|
|
16
|
+
* auction settlement is the only writer that sets `locked: true`. Accepted
|
|
17
|
+
* offers deliberately do NOT set it — declining to buy is the buyer's right,
|
|
18
|
+
* and the offer simply lapses at its `checkoutDeadline`.
|
|
19
|
+
*/
|
|
9
20
|
export declare function updateCartItem(userId: string, itemId: string, input: UpdateCartItemInput): Promise<CartDocument>;
|
|
10
21
|
export declare function removeCartItem(userId: string, itemId: string): Promise<CartDocument>;
|
|
11
22
|
export declare function clearCart(userId: string): Promise<CartDocument>;
|
|
@@ -11,6 +11,7 @@ import { productRepository } from "../../products/repository/products.repository
|
|
|
11
11
|
import { storeRepository } from "../../stores/repository/store.repository";
|
|
12
12
|
import { normalizeListingType } from "../../products/utils/listing-type";
|
|
13
13
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
14
|
+
import { CART_LANE, activeLane, canAddNewItems, } from "../../../_internal/shared/checkout/lanes";
|
|
14
15
|
/**
|
|
15
16
|
* Product documents don't always carry a denormalized `storeName` — the
|
|
16
17
|
* create routes only set it when the caller explicitly passes one, so
|
|
@@ -35,9 +36,32 @@ export async function addItemToCart(userId, input) {
|
|
|
35
36
|
if (!canAddToCart(input.listingType)) {
|
|
36
37
|
throw new ValidationError(`Listings of type "${input.listingType}" cannot be added to the cart.`);
|
|
37
38
|
}
|
|
39
|
+
// Lane gate. While a higher-obligation lane (a won auction, then an accepted
|
|
40
|
+
// offer) is outstanding, no NEW shopping may be added — otherwise the buyer
|
|
41
|
+
// can keep deferring something they already committed to by piling more into
|
|
42
|
+
// the cart. Enforced here, on the server, not only in the UI: the UI hint is
|
|
43
|
+
// a courtesy, this is the rule.
|
|
44
|
+
const cart = await cartRepository.getOrCreate(userId);
|
|
45
|
+
if (!canAddNewItems(cart.items ?? [])) {
|
|
46
|
+
const blocking = activeLane(cart.items ?? []);
|
|
47
|
+
throw new ValidationError(blocking === CART_LANE.AUCTION
|
|
48
|
+
? "Settle your won auction first — you can add other items once it's paid for."
|
|
49
|
+
: "Complete your accepted offer first — you can add other items once it's paid for.", { code: CART_LANE_BLOCKED });
|
|
50
|
+
}
|
|
38
51
|
const storeName = await resolveStoreName(input.storeId, input.storeName);
|
|
39
52
|
return cartRepository.addItem(userId, { ...input, storeName });
|
|
40
53
|
}
|
|
54
|
+
/** Error code the client maps to a lane-aware message. */
|
|
55
|
+
export const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
|
|
56
|
+
/**
|
|
57
|
+
* Won-auction lines are non-removable and fixed-quantity — the buyer committed
|
|
58
|
+
* to the purchase by bidding, and letting them delete it would turn every
|
|
59
|
+
* auction into a free option. That's already enforced one layer down:
|
|
60
|
+
* `cartRepository.updateItem` / `removeItem` both reject a `locked` line, and
|
|
61
|
+
* auction settlement is the only writer that sets `locked: true`. Accepted
|
|
62
|
+
* offers deliberately do NOT set it — declining to buy is the buyer's right,
|
|
63
|
+
* and the offer simply lapses at its `checkoutDeadline`.
|
|
64
|
+
*/
|
|
41
65
|
export async function updateCartItem(userId, itemId, input) {
|
|
42
66
|
return cartRepository.updateItem(userId, itemId, input);
|
|
43
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DocumentReference } from "firebase-admin/firestore";
|
|
2
2
|
import type { DocumentSnapshot } from "../../../providers/db-firebase";
|
|
3
3
|
import { BaseRepository, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
|
|
4
|
-
import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type UpdateCartItemInput } from "../schemas";
|
|
4
|
+
import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type CartItemDocument, type UpdateCartItemInput } from "../schemas";
|
|
5
5
|
export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
6
6
|
private static readonly SIEVE_FIELDS;
|
|
7
7
|
constructor();
|
|
@@ -19,6 +19,29 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
|
19
19
|
getSubtotal(cart: CartDocument): number;
|
|
20
20
|
addCoupon(userId: string, coupon: CartAppliedCoupon): Promise<void>;
|
|
21
21
|
removeCoupon(userId: string, code: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Drop every locked line matching a predicate, bypassing the `locked` guard
|
|
24
|
+
* that blocks user-initiated removal.
|
|
25
|
+
*
|
|
26
|
+
* Only the expiry sweeps call this: a locked line whose claim has lapsed must
|
|
27
|
+
* be cleared, or it keeps the buyer's auction/offer lane non-empty forever
|
|
28
|
+
* and the lane gate blocks their ordinary checkout on something they can no
|
|
29
|
+
* longer act on.
|
|
30
|
+
*/
|
|
31
|
+
private removeLockedLines;
|
|
32
|
+
/** Clear the cart line created for a specific accepted offer. */
|
|
33
|
+
removeItemsByOfferId(userId: string, offerId: string): Promise<number>;
|
|
34
|
+
/** Clear the cart line created for a specific won auction bid. */
|
|
35
|
+
removeItemsByBidId(userId: string, bidId: string): Promise<number>;
|
|
36
|
+
/**
|
|
37
|
+
* Every locked line across all carts whose `checkoutDeadline` has passed.
|
|
38
|
+
* Bounded per Rule #6 — the sweep runs on a schedule, so a backlog drains
|
|
39
|
+
* over successive runs rather than in one unbounded scan.
|
|
40
|
+
*/
|
|
41
|
+
findExpiredLockedLines(now: Date, limit?: number): Promise<Array<{
|
|
42
|
+
userId: string;
|
|
43
|
+
item: CartItemDocument;
|
|
44
|
+
}>>;
|
|
22
45
|
updateItemShipping(userId: string, itemId: string, providerId: string, fee: number): Promise<CartDocument>;
|
|
23
46
|
clearAllCoupons(userId: string): Promise<void>;
|
|
24
47
|
setSelectedItems(userId: string, itemIds: string[] | null): Promise<void>;
|
|
@@ -61,14 +61,20 @@ export class CartRepository extends BaseRepository {
|
|
|
61
61
|
try {
|
|
62
62
|
const cart = await this.getOrCreate(userId);
|
|
63
63
|
const items = [...cart.items];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// A locked line (accepted offer / won auction) is identified by its
|
|
65
|
+
// offerId or bidId, never merged by productId — two separate wins on the
|
|
66
|
+
// same listing are two separate obligations, and re-running the
|
|
67
|
+
// settlement sweep must not duplicate a line it already wrote.
|
|
68
|
+
if (input.offerId && items.some((item) => item.offerId === input.offerId)) {
|
|
69
|
+
return cart;
|
|
68
70
|
}
|
|
69
|
-
|
|
71
|
+
if (input.bidId && items.some((item) => item.bidId === input.bidId)) {
|
|
72
|
+
return cart;
|
|
73
|
+
}
|
|
74
|
+
const isLockedLine = Boolean(input.offerId || input.bidId);
|
|
75
|
+
const existingIndex = isLockedLine
|
|
70
76
|
? -1
|
|
71
|
-
: items.findIndex((item) => item.productId === input.productId && !item.offerId);
|
|
77
|
+
: items.findIndex((item) => item.productId === input.productId && !item.offerId && !item.bidId);
|
|
72
78
|
if (existingIndex >= 0) {
|
|
73
79
|
items[existingIndex] = {
|
|
74
80
|
...items[existingIndex],
|
|
@@ -90,9 +96,15 @@ export class CartRepository extends BaseRepository {
|
|
|
90
96
|
listingType: input.listingType,
|
|
91
97
|
...(input.isOffer !== undefined && { isOffer: input.isOffer }),
|
|
92
98
|
...(input.offerId !== undefined && { offerId: input.offerId }),
|
|
99
|
+
...(input.isAuctionWin !== undefined && { isAuctionWin: input.isAuctionWin }),
|
|
100
|
+
...(input.auctionId !== undefined && { auctionId: input.auctionId }),
|
|
101
|
+
...(input.bidId !== undefined && { bidId: input.bidId }),
|
|
93
102
|
...(input.lockedPrice !== undefined && {
|
|
94
103
|
lockedPrice: input.lockedPrice,
|
|
95
104
|
}),
|
|
105
|
+
...(input.checkoutDeadline !== undefined && {
|
|
106
|
+
checkoutDeadline: input.checkoutDeadline,
|
|
107
|
+
}),
|
|
96
108
|
...(input.locked !== undefined && { locked: input.locked }),
|
|
97
109
|
// SB-UNI-4 2026-05-13 — propagate bundle identifiers when present.
|
|
98
110
|
...(input.bundleCategorySlug !== undefined && {
|
|
@@ -231,6 +243,59 @@ export class CartRepository extends BaseRepository {
|
|
|
231
243
|
.doc(userId)
|
|
232
244
|
.set({ appliedCoupons: updated, updatedAt: new Date() }, { merge: true });
|
|
233
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Drop every locked line matching a predicate, bypassing the `locked` guard
|
|
248
|
+
* that blocks user-initiated removal.
|
|
249
|
+
*
|
|
250
|
+
* Only the expiry sweeps call this: a locked line whose claim has lapsed must
|
|
251
|
+
* be cleared, or it keeps the buyer's auction/offer lane non-empty forever
|
|
252
|
+
* and the lane gate blocks their ordinary checkout on something they can no
|
|
253
|
+
* longer act on.
|
|
254
|
+
*/
|
|
255
|
+
async removeLockedLines(userId, matches) {
|
|
256
|
+
const cart = await this.findByUserId(userId);
|
|
257
|
+
if (!cart)
|
|
258
|
+
return 0;
|
|
259
|
+
const items = cart.items.filter((item) => !matches(item));
|
|
260
|
+
const removed = cart.items.length - items.length;
|
|
261
|
+
if (removed === 0)
|
|
262
|
+
return 0;
|
|
263
|
+
await this.db
|
|
264
|
+
.collection(this.collection)
|
|
265
|
+
.doc(userId)
|
|
266
|
+
.set(prepareForFirestore({ ...cart, items, updatedAt: new Date() }));
|
|
267
|
+
return removed;
|
|
268
|
+
}
|
|
269
|
+
/** Clear the cart line created for a specific accepted offer. */
|
|
270
|
+
async removeItemsByOfferId(userId, offerId) {
|
|
271
|
+
return this.removeLockedLines(userId, (item) => item.offerId === offerId);
|
|
272
|
+
}
|
|
273
|
+
/** Clear the cart line created for a specific won auction bid. */
|
|
274
|
+
async removeItemsByBidId(userId, bidId) {
|
|
275
|
+
return this.removeLockedLines(userId, (item) => item.bidId === bidId);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Every locked line across all carts whose `checkoutDeadline` has passed.
|
|
279
|
+
* Bounded per Rule #6 — the sweep runs on a schedule, so a backlog drains
|
|
280
|
+
* over successive runs rather than in one unbounded scan.
|
|
281
|
+
*/
|
|
282
|
+
async findExpiredLockedLines(now, limit = 200) {
|
|
283
|
+
const snapshot = await this.getCollection().limit(limit).get();
|
|
284
|
+
const out = [];
|
|
285
|
+
for (const doc of snapshot.docs) {
|
|
286
|
+
const cart = this.mapDoc(doc);
|
|
287
|
+
for (const item of cart.items ?? []) {
|
|
288
|
+
if (!item.checkoutDeadline)
|
|
289
|
+
continue;
|
|
290
|
+
const deadline = item.checkoutDeadline instanceof Date
|
|
291
|
+
? item.checkoutDeadline
|
|
292
|
+
: new Date(item.checkoutDeadline);
|
|
293
|
+
if (deadline <= now)
|
|
294
|
+
out.push({ userId: cart.userId ?? doc.id, item });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
234
299
|
async updateItemShipping(userId, itemId, providerId, fee) {
|
|
235
300
|
try {
|
|
236
301
|
const cart = await this.findByUserId(userId);
|
|
@@ -20,15 +20,32 @@ export interface CartItemDocument {
|
|
|
20
20
|
/**
|
|
21
21
|
* Snapshot of the product's listing-kind at add-to-cart time (SB1-G Phase 4).
|
|
22
22
|
* Drives order-grouping (auctions/pre-orders settle separately from standard
|
|
23
|
-
* carts) and cart-side UI badges. Replaces the legacy `
|
|
23
|
+
* carts) and cart-side UI badges. Replaces the legacy `isAuctionWin`/`isPreOrder`
|
|
24
24
|
* pair on the cart item.
|
|
25
25
|
*/
|
|
26
26
|
listingType: ListingType;
|
|
27
27
|
/** True when item was added from an accepted Make-an-Offer */
|
|
28
28
|
isOffer?: boolean;
|
|
29
29
|
offerId?: string;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* True when the line was created by auction settlement for the winning
|
|
32
|
+
* bidder. Auctions are `canAddToCart: false` for user-initiated adds — this
|
|
33
|
+
* line is written by the settlement job through the repository directly, and
|
|
34
|
+
* is the ONLY way an auction reaches the cart.
|
|
35
|
+
*/
|
|
36
|
+
isAuctionWin?: boolean;
|
|
37
|
+
/** The auction product this win refers to (= productId, kept explicit). */
|
|
38
|
+
auctionId?: string;
|
|
39
|
+
/** The winning bid, so the resulting order can be linked back to it. */
|
|
40
|
+
bidId?: string;
|
|
41
|
+
/** Locked offer/winning-bid price — overrides normal product price at checkout */
|
|
31
42
|
lockedPrice?: number;
|
|
43
|
+
/**
|
|
44
|
+
* When the claim on this locked price lapses. Mirrors the offer's own
|
|
45
|
+
* `checkoutDeadline`; set to now + 48h for a won auction. The expiry sweep
|
|
46
|
+
* clears lines past this point.
|
|
47
|
+
*/
|
|
48
|
+
checkoutDeadline?: Date;
|
|
32
49
|
/** When true the item cannot be removed or have its quantity changed. Set on won-auction and accepted-offer items that require mandatory payment. */
|
|
33
50
|
locked?: boolean;
|
|
34
51
|
/**
|
|
@@ -65,8 +82,6 @@ export interface CartAppliedCoupon {
|
|
|
65
82
|
storeId?: string;
|
|
66
83
|
/** Item IDs (CartItemDocument.itemId) this coupon was calculated against */
|
|
67
84
|
applicableItemIds?: string[];
|
|
68
|
-
/** Mirrors CouponDocument.restrictions.combineWithSellerCoupons — used for conflict detection when adding future coupons */
|
|
69
|
-
combineWithSellerCoupons?: boolean;
|
|
70
85
|
}
|
|
71
86
|
export interface CartDocument extends BaseDocument {
|
|
72
87
|
userId: string;
|
|
@@ -114,7 +129,11 @@ export type AddToCartInput = {
|
|
|
114
129
|
listingType: ListingType;
|
|
115
130
|
isOffer?: boolean;
|
|
116
131
|
offerId?: string;
|
|
132
|
+
isAuctionWin?: boolean;
|
|
133
|
+
auctionId?: string;
|
|
134
|
+
bidId?: string;
|
|
117
135
|
lockedPrice?: number;
|
|
136
|
+
checkoutDeadline?: Date;
|
|
118
137
|
locked?: boolean;
|
|
119
138
|
/** SB-UNI-4 2026-05-13 — bundle identifier when the line is a bundle. */
|
|
120
139
|
bundleCategorySlug?: string;
|
|
@@ -7,14 +7,35 @@ import { AuctionsIndexListing } from "../../products/components/AuctionsIndexLis
|
|
|
7
7
|
import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing";
|
|
8
8
|
import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsIndexListing";
|
|
9
9
|
import { CategoryBundlesListing } from "./CategoryBundlesListing";
|
|
10
|
-
import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
|
|
10
|
+
import { CATEGORY_PAGE_TABS, ART_STICKERS_LISTING_TYPES, } from "../../products/constants/listing-tabs";
|
|
11
|
+
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
12
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
13
|
+
/**
|
|
14
|
+
* Tab slug → listing/category type, derived (2026-08-21). The hand-written
|
|
15
|
+
* version covered 5 of 10 tabs, and this view's filter did `if (!mapping)
|
|
16
|
+
* return false` — so classifieds, digital codes, live items and art were
|
|
17
|
+
* silently DROPPED from every brand page rather than rendering blank (the
|
|
18
|
+
* failure mode its category-page twin had). Same root gap, opposite symptom.
|
|
19
|
+
*
|
|
20
|
+
* `stores` is intentionally absent: a brand has no store roster, which is the
|
|
21
|
+
* one real difference between this component and CategoryDetailTabs (see
|
|
22
|
+
* CLAUDE.md's Categories & Brands Reference on why the duplication stands).
|
|
23
|
+
*/
|
|
11
24
|
const TAB_TYPE_MAP = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
...Object.fromEntries(ALL_LISTING_TYPES.map((type) => [
|
|
26
|
+
pluginFor(type).tabSlug,
|
|
27
|
+
{ kind: "listing", type },
|
|
28
|
+
])),
|
|
16
29
|
bundles: { kind: "category", type: "bundle" },
|
|
17
30
|
};
|
|
31
|
+
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
32
|
+
const GENERIC_TAB_LISTING_TYPES = {
|
|
33
|
+
[pluginFor("standard").tabSlug]: ["standard"],
|
|
34
|
+
[pluginFor("classified").tabSlug]: ["classified"],
|
|
35
|
+
[pluginFor("digital-code").tabSlug]: ["digital-code"],
|
|
36
|
+
[pluginFor("live").tabSlug]: ["live"],
|
|
37
|
+
[pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
|
|
38
|
+
};
|
|
18
39
|
export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
19
40
|
const countFor = (id) => {
|
|
20
41
|
switch (id) {
|
|
@@ -28,11 +49,10 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
|
|
|
28
49
|
};
|
|
29
50
|
const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
|
|
30
51
|
const mapping = TAB_TYPE_MAP[t.id];
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// here rather than rely on downstream JSX to silently render nothing.
|
|
52
|
+
// A tab with no mapping has no render branch below and would show a blank
|
|
53
|
+
// panel, so drop it here rather than let downstream JSX render nothing.
|
|
54
|
+
// Today that's only `stores` — a brand has no store roster. Every listing
|
|
55
|
+
// type IS mapped now (it used to also silently drop four real types).
|
|
36
56
|
if (!mapping)
|
|
37
57
|
return false;
|
|
38
58
|
if (mapping.kind === "listing" && enabledListingTypes) {
|
|
@@ -49,5 +69,5 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
|
|
|
49
69
|
});
|
|
50
70
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
51
71
|
const [activeTab, setActiveTab] = useState(firstTabId);
|
|
52
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab
|
|
72
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
|
|
53
73
|
}
|