@mohasinac/appkit 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/auth/actions.d.ts +0 -1
- package/dist/_internal/server/features/auth/actions.js +0 -1
- package/dist/_internal/server/features/bundles/data.d.ts +22 -4
- package/dist/_internal/server/features/bundles/data.js +41 -4
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +7 -78
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
- package/dist/_internal/server/features/orders/adapters.js +1 -14
- package/dist/_internal/server/features/products/actions.js +8 -3
- package/dist/_internal/server/features/products/service.d.ts +15 -0
- package/dist/_internal/server/features/products/service.js +41 -0
- package/dist/_internal/server/functions/firestore.d.ts +3 -1
- package/dist/_internal/server/functions/firestore.js +17 -1
- package/dist/_internal/server/functions/scheduled.d.ts +2 -5
- package/dist/_internal/server/functions/scheduled.js +6 -30
- package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
- package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
- package/dist/_internal/server/jobs/core/index.d.ts +4 -4
- package/dist/_internal/server/jobs/core/index.js +4 -4
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +2 -2
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -4
- package/dist/_internal/server/jobs/handlers/index.js +8 -6
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
- package/dist/_internal/shared/actions/action-registry.js +40 -10
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
- package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
- package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
- package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
- package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +5 -5
- package/dist/constants/field-names.d.ts +17 -1
- package/dist/constants/field-names.js +17 -1
- package/dist/features/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.js +1 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
- package/dist/features/admin/hooks/useAdminListing.js +4 -2
- package/dist/features/admin/schemas/firestore.d.ts +13 -5
- package/dist/features/admin/schemas/firestore.js +1 -2
- package/dist/features/auctions/actions/bid-actions.js +3 -1
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
- package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
- package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +21 -3
- package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
- package/dist/features/auctions/components/LiveBidPrice.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useBids.d.ts +33 -0
- package/dist/features/auctions/hooks/useBids.js +32 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
- package/dist/features/auth/checkout-value-otp.d.ts +7 -10
- package/dist/features/auth/checkout-value-otp.js +14 -10
- package/dist/features/auth/server.d.ts +0 -1
- package/dist/features/auth/server.js +0 -1
- package/dist/features/blog/api/[slug]/route.d.ts +5 -0
- package/dist/features/blog/api/[slug]/route.js +12 -3
- package/dist/features/blog/components/BlogPostView.d.ts +2 -0
- package/dist/features/blog/components/BlogPostView.js +5 -4
- package/dist/features/blog/hooks/useBlog.d.ts +2 -0
- package/dist/features/blog/hooks/useBlog.js +2 -0
- package/dist/features/blog/repository/blog.repository.d.ts +7 -0
- package/dist/features/blog/repository/blog.repository.js +44 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/index.d.ts +0 -2
- package/dist/features/cart/components/index.js +0 -1
- package/dist/features/cart/hooks/useCartCount.js +22 -5
- package/dist/features/cart/hooks/useGuestCart.js +11 -1
- package/dist/features/cart/utils/guest-cart.d.ts +4 -0
- package/dist/features/cart/utils/guest-cart.js +10 -0
- package/dist/features/cart/utils/pending-ops.d.ts +17 -0
- package/dist/features/cart/utils/pending-ops.js +46 -5
- package/dist/features/categories/components/BrandDetailPageView.js +6 -2
- package/dist/features/categories/components/BundleDetailView.js +4 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
- package/dist/features/categories/schemas/firestore.d.ts +7 -0
- package/dist/features/checkout/actions/index.d.ts +0 -1
- package/dist/features/checkout/actions/index.js +0 -1
- package/dist/features/checkout/schemas/firestore.d.ts +2 -2
- package/dist/features/events/actions/event-actions.d.ts +2 -0
- package/dist/features/events/actions/event-actions.js +5 -0
- package/dist/features/events/components/EventCard.js +1 -2
- package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
- package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/repository/events.repository.d.ts +6 -0
- package/dist/features/events/repository/events.repository.js +21 -0
- package/dist/features/history/repository/user-history.repository.d.ts +1 -1
- package/dist/features/history/utils/guest-history.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.d.ts +7 -0
- package/dist/features/media/MediaVideo.js +36 -5
- package/dist/features/orders/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +2 -8
- package/dist/features/orders/schemas/index.d.ts +3 -32
- package/dist/features/orders/schemas/index.js +1 -4
- package/dist/features/orders/types/index.d.ts +6 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/actions/product-actions.js +13 -10
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
- package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
- package/dist/features/products/components/PrizeRevealModal.js +18 -147
- package/dist/features/products/components/ProductDetailPageView.js +72 -30
- package/dist/features/products/components/ProductForm.js +7 -14
- package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
- package/dist/features/products/components/ProductGalleryClient.js +29 -10
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +1 -1
- package/dist/features/products/components/index.d.ts +4 -2
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/constants/action-defs.d.ts +0 -1
- package/dist/features/products/constants/action-defs.js +0 -2
- package/dist/features/products/repository/products.repository.d.ts +6 -0
- package/dist/features/products/repository/products.repository.js +20 -1
- package/dist/features/products/schemas/firestore.d.ts +26 -2
- package/dist/features/products/schemas/index.d.ts +11 -3
- package/dist/features/products/schemas/index.js +10 -2
- package/dist/features/products/types/index.d.ts +2 -1
- package/dist/features/reviews/actions/review-actions.js +1 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -1
- package/dist/features/reviews/schemas/firestore.d.ts +3 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -0
- package/dist/features/reviews/schemas/index.js +2 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.js +3 -2
- package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
- package/dist/features/scams/repository/scammer.repository.js +24 -0
- package/dist/features/seller/actions/seller-actions.js +8 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +21 -9
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
- package/dist/features/tester/actions/leaderboard-actions.js +5 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
- package/dist/features/tester/components/index.d.ts +2 -0
- package/dist/features/tester/components/index.js +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
- package/dist/features/tester/schemas/firestore.d.ts +22 -1
- package/dist/features/tester/schemas/firestore.js +8 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
- package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
- package/dist/features/tester/seed-data/products-tester-seed-data.js +152 -8
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
- package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
- package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
- package/dist/index.d.ts +19 -22
- package/dist/index.js +25 -58
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/schemas/registry.d.ts +6 -26
- package/dist/seed/bids-seed-data.js +14 -2
- package/dist/seed/blog-posts-seed-data.js +2 -1
- package/dist/seed/categories-seed-data.js +11 -2
- package/dist/seed/events-seed-data.js +11 -11
- package/dist/seed/faq-seed-data.js +5 -1
- package/dist/seed/products-auctions-seed-data.js +2 -2
- package/dist/seed/products-preorders-seed-data.d.ts +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +56 -10
- package/dist/seed/products-standard-seed-data.js +82 -3
- package/dist/seed/scammers-seed-data.js +75 -2
- package/dist/seed/site-settings-seed-data.js +5 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +4 -20
- package/dist/server.js +10 -57
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +1 -1
- package/dist/ui/components/ImageLightbox.d.ts +9 -0
- package/dist/ui/components/ImageLightbox.js +6 -3
- package/dist/ui/components/ListingToolbar.d.ts +18 -1
- package/dist/ui/components/ListingToolbar.js +5 -2
- package/dist/ui/components/StickyToolbar.js +13 -8
- package/package.json +1 -1
|
@@ -31,6 +31,7 @@ import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid";
|
|
|
31
31
|
import { listReviewsBySeller } from "../../reviews/actions/review-actions";
|
|
32
32
|
import { PlaceBidModalButton } from "./PlaceBidFormClient";
|
|
33
33
|
import { CollapsibleBidHistory } from "./CollapsibleBidHistory";
|
|
34
|
+
import { LiveBidPrice } from "./LiveBidPrice";
|
|
34
35
|
import { SublistingCarouselSection } from "../../products/components/SublistingCarouselSection";
|
|
35
36
|
function toDescriptionHtml(raw) {
|
|
36
37
|
if (!raw)
|
|
@@ -39,8 +40,8 @@ function toDescriptionHtml(raw) {
|
|
|
39
40
|
return normalizeRichTextHtml(s);
|
|
40
41
|
}
|
|
41
42
|
function renderAuctionInfoPanel(props) {
|
|
42
|
-
const { title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
43
|
-
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" }),
|
|
43
|
+
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
44
|
+
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 && _jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: [isEnded ? "Ended" : "Ends", " ", _jsx(Span, { weight: "medium", color: "muted", children: 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" })] }) }))] }));
|
|
44
45
|
}
|
|
45
46
|
function renderAuctionStoreReviews(storeReviews) {
|
|
46
47
|
if (storeReviews.length === 0)
|
|
@@ -53,7 +54,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
53
54
|
? (initialAuction ?? undefined)
|
|
54
55
|
: await productRepository.findByIdOrSlug(id).catch(() => undefined);
|
|
55
56
|
const bidsResult = product
|
|
56
|
-
? await listBidsByProduct(String(product.id), { pageSize:
|
|
57
|
+
? await listBidsByProduct(String(product.id), { pageSize: 5 }).catch(() => null)
|
|
57
58
|
: null;
|
|
58
59
|
const storeId = product?.storeId;
|
|
59
60
|
const storeReviews = storeId
|
|
@@ -83,6 +84,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
83
84
|
: typeof p.mainImage === "string"
|
|
84
85
|
? [p.mainImage]
|
|
85
86
|
: [];
|
|
87
|
+
const productVideo = p.video;
|
|
86
88
|
const endDate = p.auctionEndDate ? new Date(p.auctionEndDate) : null;
|
|
87
89
|
const isEnded = endDate ? endDate < new Date() : false;
|
|
88
90
|
const bidCount = typeof p.bidCount === "number" ? p.bidCount : 0;
|
|
@@ -125,23 +127,18 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
125
127
|
price: typeof p.price === "number" ? p.price : undefined,
|
|
126
128
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
127
129
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
128
|
-
} }), _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.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), 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(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => renderAuctionInfoPanel({ title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Stack, { id: "auction-bid-form", gap: "3", children: _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(
|
|
130
|
+
} }), _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.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), 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(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => renderAuctionInfoPanel({ productId: String(product.id), title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Stack, { id: "auction-bid-form", gap: "3", children: _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "xl", badgeSize: "xs" }), !isEnded && (_jsxs(Span, { size: "xs", color: "muted", children: ["min increment ", formatCurrency(minBidIncrement, currency)] }))] }), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] }) })) : (
|
|
129
131
|
/* Read-only bid panel — shown when no bid action is wired (preview/demo) */
|
|
130
|
-
_jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children: _jsxs(Text, { size: "xs", color: "muted", children: ["Starting bid: ", formatCurrency(startingBid, currency), !isEnded && _jsxs(_Fragment, { children: [" \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] })] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + minBidIncrement, currency)}`, min: currentBid + minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && !bidsHaveStarted && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [
|
|
132
|
+
_jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children: _jsxs(Text, { size: "xs", color: "muted", children: ["Starting bid: ", formatCurrency(startingBid, currency), !isEnded && _jsxs(_Fragment, { children: [" \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] })] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + minBidIncrement, currency)}`, min: currentBid + minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && !bidsHaveStarted && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
|
|
131
133
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
132
134
|
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
133
135
|
id: s.id,
|
|
134
136
|
label: s.title,
|
|
135
137
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
136
138
|
})) })), renderBidHistory: () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
bidderName: (b.bidderName ?? b.userName),
|
|
141
|
-
amount: (typeof b.bidAmount === "number" ? b.bidAmount : typeof b.amount === "number" ? b.amount : 0),
|
|
142
|
-
placedAt: (b.bidDate ?? b.createdAt ?? b.bidAt ?? ""),
|
|
143
|
-
}));
|
|
144
|
-
return _jsx(CollapsibleBidHistory, { bids: bids, currency: currency });
|
|
139
|
+
if (!bidsResult)
|
|
140
|
+
return null;
|
|
141
|
+
return (_jsx(CollapsibleBidHistory, { productId: String(product.id), initialData: bidsResult, currency: currency }));
|
|
145
142
|
}, renderRelated: () => {
|
|
146
143
|
const now = new Date();
|
|
147
144
|
const related = relatedDocs
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { BidHistoryEntry } from "../../products/components/BidHistory";
|
|
3
|
+
import { type BidListResponse } from "../hooks/useBids";
|
|
4
|
+
interface RawBidLike {
|
|
5
|
+
id?: unknown;
|
|
6
|
+
userId?: unknown;
|
|
7
|
+
bidderId?: unknown;
|
|
8
|
+
userName?: unknown;
|
|
9
|
+
bidderName?: unknown;
|
|
10
|
+
bidAmount?: unknown;
|
|
11
|
+
amount?: unknown;
|
|
12
|
+
bidDate?: unknown;
|
|
13
|
+
createdAt?: unknown;
|
|
14
|
+
bidAt?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Shared shape mapper — SSR's raw bid docs and useBids()'s client-fetched
|
|
17
|
+
* pages both go through this so every page (SSR page 1, client pages 2+)
|
|
18
|
+
* renders identically. */
|
|
19
|
+
export declare function toBidHistoryEntry(b: RawBidLike): BidHistoryEntry;
|
|
3
20
|
interface CollapsibleBidHistoryProps {
|
|
4
|
-
|
|
21
|
+
productId: string;
|
|
22
|
+
/** SSR page-1 result — seeds `useBids()`'s cache so opening the section
|
|
23
|
+
* doesn't trigger a redundant fetch for the page already rendered. */
|
|
24
|
+
initialData: BidListResponse;
|
|
5
25
|
currency: string;
|
|
26
|
+
pageSize?: number;
|
|
6
27
|
}
|
|
7
|
-
export declare function CollapsibleBidHistory({
|
|
28
|
+
export declare function CollapsibleBidHistory({ productId, initialData, currency, pageSize }: CollapsibleBidHistoryProps): React.JSX.Element;
|
|
8
29
|
export {};
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { Button, Div, Span } from "../../../ui";
|
|
4
|
+
import { Button, Div, Row, Span } from "../../../ui";
|
|
5
|
+
import { Pagination } from "../../../ui/components/Pagination";
|
|
5
6
|
import { BidHistory } from "../../products/components/BidHistory";
|
|
7
|
+
import { useBids } from "../hooks/useBids";
|
|
6
8
|
const __O = {
|
|
7
9
|
hidden: "overflow-hidden",
|
|
8
10
|
};
|
|
9
|
-
|
|
11
|
+
/** Shared shape mapper — SSR's raw bid docs and useBids()'s client-fetched
|
|
12
|
+
* pages both go through this so every page (SSR page 1, client pages 2+)
|
|
13
|
+
* renders identically. */
|
|
14
|
+
export function toBidHistoryEntry(b) {
|
|
15
|
+
return {
|
|
16
|
+
id: String(b.id ?? ""),
|
|
17
|
+
bidderId: String(b.userId ?? b.bidderId ?? ""),
|
|
18
|
+
bidderName: (b.bidderName ?? b.userName),
|
|
19
|
+
amount: typeof b.bidAmount === "number" ? b.bidAmount : typeof b.amount === "number" ? b.amount : 0,
|
|
20
|
+
placedAt: (b.bidDate ?? b.createdAt ?? b.bidAt ?? ""),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function CollapsibleBidHistory({ productId, initialData, currency, pageSize = 5 }) {
|
|
10
24
|
const [open, setOpen] = useState(false);
|
|
11
|
-
|
|
25
|
+
const [page, setPage] = useState(1);
|
|
26
|
+
const { bids: rawBids, totalPages, isLoading } = useBids({ productId, page, pageSize }, { initialData, enabled: open });
|
|
27
|
+
const bids = rawBids.map(toBidHistoryEntry);
|
|
28
|
+
const total = initialData.total;
|
|
29
|
+
return (_jsxs(Div, { className: `mt-6 ${__O.hidden}`, border: "subtle", rounded: "xl", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setOpen((v) => !v), paddingX: "md", paddingY: "md", rounded: "none", className: "w-full justify-between bg-[var(--appkit-color-surface)]/60 hover:bg-[var(--appkit-color-surface-elevated)]", "aria-expanded": open, children: [_jsxs(Span, { size: "sm", weight: "semibold", className: "tracking-wide", color: "muted", transform: "uppercase", children: ["Bid History", total > 0 && (_jsx(Span, { size: "xs", weight: "medium", className: "ml-2 normal-case tracking-normal", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: total }))] }), _jsx("svg", { className: `h-4 w-4 text-zinc-400 transition-transform duration-200 ${open ? "rotate-180" : ""}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), open && (_jsxs(Div, { surface: "default", padding: "md", children: [_jsx(BidHistory, { bids: bids, isLoading: isLoading && bids.length === 0, isEmpty: !isLoading && bids.length === 0, currency: currency }), totalPages > 1 && (_jsx(Row, { justify: "center", className: "mt-4", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: setPage, size: "sm" }) }))] }))] }));
|
|
12
30
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LiveBidPriceProps {
|
|
2
|
+
productId: string;
|
|
3
|
+
currentBid: number;
|
|
4
|
+
bidCount: number;
|
|
5
|
+
currency: string;
|
|
6
|
+
isEnded: boolean;
|
|
7
|
+
/** Matches the appkit `<Span size>` scale — pick to match the call site. */
|
|
8
|
+
priceSize?: "base" | "lg" | "xl" | "2xl";
|
|
9
|
+
badgeSize?: "xs" | "sm";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* LiveBidPrice — the current-bid + bid-count pill, kept live via the
|
|
13
|
+
* `/auction-bids/{id}` SSE channel. Replaces 3 near-identical static
|
|
14
|
+
* SSR-only displays on the auction detail page (info panel, desktop and
|
|
15
|
+
* mobile compact bid-summary cards) so every viewer watching the page sees
|
|
16
|
+
* a new bid the instant it's placed, not just after their own bid or a
|
|
17
|
+
* page reload.
|
|
18
|
+
*/
|
|
19
|
+
export declare function LiveBidPrice({ productId, currentBid, bidCount, currency, isEnded, priceSize, badgeSize, }: LiveBidPriceProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Row, Span } from "../../../ui";
|
|
4
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
5
|
+
import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
|
|
6
|
+
/**
|
|
7
|
+
* LiveBidPrice — the current-bid + bid-count pill, kept live via the
|
|
8
|
+
* `/auction-bids/{id}` SSE channel. Replaces 3 near-identical static
|
|
9
|
+
* SSR-only displays on the auction detail page (info panel, desktop and
|
|
10
|
+
* mobile compact bid-summary cards) so every viewer watching the page sees
|
|
11
|
+
* a new bid the instant it's placed, not just after their own bid or a
|
|
12
|
+
* page reload.
|
|
13
|
+
*/
|
|
14
|
+
export function LiveBidPrice({ productId, currentBid, bidCount, currency, isEnded, priceSize = "xl", badgeSize = "xs", }) {
|
|
15
|
+
const live = useLiveAuctionBid(productId, currentBid, bidCount, { enabled: !isEnded });
|
|
16
|
+
return (_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { weight: "bold", className: "text-primary-600 dark:text-primary-400", size: priceSize, children: formatCurrency(live.currentBid, currency) }), _jsxs(Span, { size: badgeSize, weight: "medium", rounded: "full", padding: "pill-md", surface: "subtle", color: "muted", children: [live.bidCount, " ", live.bidCount === 1 ? "bid" : "bids"] })] }));
|
|
17
|
+
}
|
|
@@ -18,7 +18,7 @@ export interface PlaceBidFormClientProps {
|
|
|
18
18
|
onPlaceBid: (input: PlaceBidInput) => Promise<unknown>;
|
|
19
19
|
onBuyNow?: () => Promise<unknown>;
|
|
20
20
|
}
|
|
21
|
-
export declare function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted, bidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
|
|
21
|
+
export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
|
|
22
22
|
/**
|
|
23
23
|
* `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
|
|
24
24
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
4
5
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
5
6
|
import { isAuthError } from "../../../utils/auth-error";
|
|
6
7
|
import { Button, Div, LoginRequiredModal, Modal, Row, Span, Stack, Text } from "../../../ui";
|
|
@@ -8,6 +9,7 @@ import { Form } from "../../../ui/components/Form";
|
|
|
8
9
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
9
10
|
import { applyZodIssues } from "../../../ui/forms/FormShell";
|
|
10
11
|
import { placeBidSchema } from "../schemas/bid-input";
|
|
12
|
+
import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
|
|
11
13
|
import { normalizeError } from "../../../errors/normalize";
|
|
12
14
|
const __P = {
|
|
13
15
|
p5: "p-[var(--appkit-space-5)]",
|
|
@@ -19,7 +21,13 @@ const BID_ERROR_DISPLAY = {
|
|
|
19
21
|
BID_SELF_BID: "You cannot bid on your own listing.",
|
|
20
22
|
BID_USER_BANNED: "Your account is currently restricted from bidding. Contact support.",
|
|
21
23
|
};
|
|
22
|
-
export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted = false, bidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
24
|
+
export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted = false, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
// Live-updates while the bid card is open — reflects other bidders'
|
|
27
|
+
// activity on this auction, not just the bidder's own submission.
|
|
28
|
+
const live = useLiveAuctionBid(productId, ssrCurrentBid, ssrBidCount, { enabled: !isEnded });
|
|
29
|
+
const currentBid = live.currentBid;
|
|
30
|
+
const bidCount = live.bidCount;
|
|
23
31
|
const minBid = currentBid + minBidIncrement;
|
|
24
32
|
const [bidAmount, setBidAmount] = useState(String(minBid));
|
|
25
33
|
const [stepMul, setStepMul] = useState(1);
|
|
@@ -44,6 +52,12 @@ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidI
|
|
|
44
52
|
setSuccess(true);
|
|
45
53
|
setBidAmount(String(amount + minBidIncrement));
|
|
46
54
|
setStepMul(1);
|
|
55
|
+
// The bid history list and any duplicated static current-bid/bid-count
|
|
56
|
+
// text elsewhere on this SSR'd page (info panel, page-level summary
|
|
57
|
+
// cards) are frozen server props — refresh so they reflect this bid
|
|
58
|
+
// immediately instead of waiting for the page's ISR window to roll
|
|
59
|
+
// over. The live price shown above already updated via SSE regardless.
|
|
60
|
+
router.refresh();
|
|
47
61
|
}
|
|
48
62
|
catch (err) {
|
|
49
63
|
void normalizeError(err);
|
|
@@ -78,6 +92,7 @@ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidI
|
|
|
78
92
|
return;
|
|
79
93
|
}
|
|
80
94
|
setSuccess(true);
|
|
95
|
+
router.refresh();
|
|
81
96
|
}
|
|
82
97
|
catch (err) {
|
|
83
98
|
void normalizeError(err);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BidDocument } from "../schemas/firestore";
|
|
2
|
+
export interface BidListResponse {
|
|
3
|
+
items: Omit<BidDocument, "userEmail">[];
|
|
4
|
+
total: number;
|
|
5
|
+
page: number;
|
|
6
|
+
pageSize: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
hasMore: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UseBidsParams {
|
|
11
|
+
productId: string;
|
|
12
|
+
page?: number;
|
|
13
|
+
pageSize?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface UseBidsOptions {
|
|
16
|
+
initialData?: BidListResponse;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* useBids
|
|
21
|
+
*
|
|
22
|
+
* Paginated, newest-first bid history for a single auction — backs the
|
|
23
|
+
* auction detail page's "Bid History" section. `/api/bids` already sorts
|
|
24
|
+
* `bidDate DESC` server-side (see `listBidsByProduct`), so no client sort
|
|
25
|
+
* is needed here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useBids(params: UseBidsParams, opts?: UseBidsOptions): {
|
|
28
|
+
bids: Omit<BidDocument, "userEmail">[];
|
|
29
|
+
total: number;
|
|
30
|
+
totalPages: number;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
error: Error | null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { apiClient } from "../../../http";
|
|
3
|
+
import { BID_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
4
|
+
/**
|
|
5
|
+
* useBids
|
|
6
|
+
*
|
|
7
|
+
* Paginated, newest-first bid history for a single auction — backs the
|
|
8
|
+
* auction detail page's "Bid History" section. `/api/bids` already sorts
|
|
9
|
+
* `bidDate DESC` server-side (see `listBidsByProduct`), so no client sort
|
|
10
|
+
* is needed here.
|
|
11
|
+
*/
|
|
12
|
+
export function useBids(params, opts) {
|
|
13
|
+
const { productId, page = 1, pageSize = 5 } = params;
|
|
14
|
+
const sp = new URLSearchParams();
|
|
15
|
+
sp.set("productId", productId);
|
|
16
|
+
sp.set("page", String(page));
|
|
17
|
+
sp.set("pageSize", String(pageSize));
|
|
18
|
+
const endpoint = `${BID_ENDPOINTS.LIST}?${sp.toString()}`;
|
|
19
|
+
const query = useQuery({
|
|
20
|
+
queryKey: ["bids", productId, page, pageSize],
|
|
21
|
+
queryFn: () => apiClient.get(endpoint),
|
|
22
|
+
initialData: page === 1 ? opts?.initialData : undefined,
|
|
23
|
+
enabled: opts?.enabled !== false && !!productId,
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
bids: query.data?.items ?? [],
|
|
27
|
+
total: query.data?.total ?? 0,
|
|
28
|
+
totalPages: query.data?.totalPages ?? 1,
|
|
29
|
+
isLoading: query.isLoading,
|
|
30
|
+
error: query.error,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface UseLiveAuctionBidResult {
|
|
2
|
+
currentBid: number;
|
|
3
|
+
bidCount: number;
|
|
4
|
+
/** True once the SSE channel has delivered at least one live update. */
|
|
5
|
+
isLive: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* useLiveAuctionBid
|
|
9
|
+
*
|
|
10
|
+
* Layers the SSE-driven `useRealtimeBids` channel on top of the auction
|
|
11
|
+
* detail page's SSR-rendered `currentBid`/`bidCount` (server props, frozen
|
|
12
|
+
* at request time / the page's ISR window). Falls back to those SSR values
|
|
13
|
+
* until the live channel connects and pushes its first update — this is
|
|
14
|
+
* what lets any viewer watching an auction see a new bid the instant
|
|
15
|
+
* another user places it, mirroring how the cart/wishlist badges layer a
|
|
16
|
+
* live delta on top of the last known server count.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useLiveAuctionBid(productId: string, ssrCurrentBid: number, ssrBidCount: number, opts?: {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
}): UseLiveAuctionBidResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRealtimeBids } from "./useRealtimeBids";
|
|
3
|
+
/**
|
|
4
|
+
* useLiveAuctionBid
|
|
5
|
+
*
|
|
6
|
+
* Layers the SSE-driven `useRealtimeBids` channel on top of the auction
|
|
7
|
+
* detail page's SSR-rendered `currentBid`/`bidCount` (server props, frozen
|
|
8
|
+
* at request time / the page's ISR window). Falls back to those SSR values
|
|
9
|
+
* until the live channel connects and pushes its first update — this is
|
|
10
|
+
* what lets any viewer watching an auction see a new bid the instant
|
|
11
|
+
* another user places it, mirroring how the cart/wishlist badges layer a
|
|
12
|
+
* live delta on top of the last known server count.
|
|
13
|
+
*/
|
|
14
|
+
export function useLiveAuctionBid(productId, ssrCurrentBid, ssrBidCount, opts) {
|
|
15
|
+
const enabled = opts?.enabled !== false;
|
|
16
|
+
const live = useRealtimeBids(enabled ? productId : null);
|
|
17
|
+
const isLive = live.connected && live.currentBid !== null;
|
|
18
|
+
return {
|
|
19
|
+
currentBid: isLive ? live.currentBid : ssrCurrentBid,
|
|
20
|
+
bidCount: isLive && live.bidCount !== null ? live.bidCount : ssrBidCount,
|
|
21
|
+
isLive,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checkout Value OTP — Tier PP.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `payment.otpCheckoutThreshold` (skipped for COD — see
|
|
7
|
-
* `createCheckoutOrderAction`). Deliberately a SEPARATE Firestore namespace
|
|
8
|
-
* (`checkoutValueOtps` vs `consentOtps`) rather than reusing the shipping-
|
|
9
|
-
* consent one — different purpose, different copy, independently tunable
|
|
10
|
-
* rate limits. Reuses `generateOtpCode()`/`hashOtp()` from `consent-otp.ts`
|
|
11
|
-
* rather than duplicating the crypto.
|
|
4
|
+
* Gates placing a checkout ≥ the site's `payment.otpCheckoutThreshold`
|
|
5
|
+
* (skipped for COD — see `createCheckoutOrderAction`).
|
|
12
6
|
*/
|
|
13
|
-
|
|
14
|
-
export
|
|
7
|
+
/** HMAC-SHA256 hash of an OTP code. */
|
|
8
|
+
export declare function hashOtp(code: string): string;
|
|
9
|
+
/** Generate a cryptographically-secure random 6-digit OTP string. */
|
|
10
|
+
export declare function generateOtpCode(): string;
|
|
15
11
|
/** OTP validity window (10 minutes) — matches consent-OTP's window; independently tunable if policy diverges later. */
|
|
16
12
|
export declare const CHECKOUT_VALUE_OTP_EXPIRY_MS: number;
|
|
17
13
|
export declare const CHECKOUT_VALUE_OTP_EXPIRY_MINUTES = 10;
|
|
@@ -44,3 +40,4 @@ export interface CheckoutValueOtpDoc {
|
|
|
44
40
|
export declare function enforceCheckoutValueOtpRateLimit(db: Db, uid: string): Promise<void>;
|
|
45
41
|
/** Build the checkout-value OTP WhatsApp/SMS message body. */
|
|
46
42
|
export declare function buildCheckoutValueOtpMessage(code: string, siteName: string): string;
|
|
43
|
+
export {};
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checkout Value OTP — Tier PP.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `payment.otpCheckoutThreshold` (skipped for COD — see
|
|
7
|
-
* `createCheckoutOrderAction`). Deliberately a SEPARATE Firestore namespace
|
|
8
|
-
* (`checkoutValueOtps` vs `consentOtps`) rather than reusing the shipping-
|
|
9
|
-
* consent one — different purpose, different copy, independently tunable
|
|
10
|
-
* rate limits. Reuses `generateOtpCode()`/`hashOtp()` from `consent-otp.ts`
|
|
11
|
-
* rather than duplicating the crypto.
|
|
4
|
+
* Gates placing a checkout ≥ the site's `payment.otpCheckoutThreshold`
|
|
5
|
+
* (skipped for COD — see `createCheckoutOrderAction`).
|
|
12
6
|
*/
|
|
7
|
+
import { createHmac, randomInt } from "crypto";
|
|
13
8
|
import { AuthorizationError } from "../../errors";
|
|
14
9
|
import { resolveDate } from "../../utils";
|
|
15
10
|
import { USER_COLLECTION } from "./schemas";
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const HMAC_KEY = process.env.CONSENT_OTP_HMAC_KEY ||
|
|
12
|
+
process.env.HMAC_SECRET ||
|
|
13
|
+
"consent-otp-secret";
|
|
14
|
+
/** HMAC-SHA256 hash of an OTP code. */
|
|
15
|
+
export function hashOtp(code) {
|
|
16
|
+
return createHmac("sha256", HMAC_KEY).update(code).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
/** Generate a cryptographically-secure random 6-digit OTP string. */
|
|
19
|
+
export function generateOtpCode() {
|
|
20
|
+
return String(randomInt(100000, 1000000));
|
|
21
|
+
}
|
|
18
22
|
/** OTP validity window (10 minutes) — matches consent-OTP's window; independently tunable if policy diverges later. */
|
|
19
23
|
export const CHECKOUT_VALUE_OTP_EXPIRY_MS = 10 * 60 * 1000;
|
|
20
24
|
export const CHECKOUT_VALUE_OTP_EXPIRY_MINUTES = 10;
|
|
@@ -17,7 +17,12 @@ type RouteContext = {
|
|
|
17
17
|
};
|
|
18
18
|
export interface BlogPostDetailResponse {
|
|
19
19
|
post: BlogPost;
|
|
20
|
+
/** Same category — the original single "related" signal. */
|
|
20
21
|
related: BlogPost[];
|
|
22
|
+
/** Posts sharing at least one tag. */
|
|
23
|
+
relatedByTags: BlogPost[];
|
|
24
|
+
/** Other posts by the same author. */
|
|
25
|
+
relatedByAuthor: BlogPost[];
|
|
21
26
|
}
|
|
22
27
|
export declare function GET(_request: Request, context: RouteContext): Promise<NextResponse>;
|
|
23
28
|
export {};
|
|
@@ -30,9 +30,18 @@ export async function GET(_request, context) {
|
|
|
30
30
|
}
|
|
31
31
|
// Increment view count fire-and-forget — must not block response
|
|
32
32
|
safeFireAndForget(blogRepository.incrementViews(post.id), "blog: incrementViews");
|
|
33
|
-
// Related posts: same category
|
|
34
|
-
const related =
|
|
35
|
-
|
|
33
|
+
// Related posts: same category / tag overlap / same author — latest 3 each, excluding current
|
|
34
|
+
const [related, relatedByTags, relatedByAuthor] = await Promise.all([
|
|
35
|
+
blogRepository.findRelated(post.category, post.id, 3),
|
|
36
|
+
blogRepository.findByTagsOverlap(post.tags ?? [], post.id, 3),
|
|
37
|
+
blogRepository.findByAuthor(post.authorId, post.id, 3),
|
|
38
|
+
]);
|
|
39
|
+
const body = {
|
|
40
|
+
post: toBlogPost(post),
|
|
41
|
+
related: related.map(toBlogPost),
|
|
42
|
+
relatedByTags: relatedByTags.map(toBlogPost),
|
|
43
|
+
relatedByAuthor: relatedByAuthor.map(toBlogPost),
|
|
44
|
+
};
|
|
36
45
|
return NextResponse.json({ success: true, data: body });
|
|
37
46
|
}
|
|
38
47
|
catch (error) {
|
|
@@ -14,6 +14,8 @@ export interface BlogPostViewProps {
|
|
|
14
14
|
viewsLabel?: string;
|
|
15
15
|
featured?: string;
|
|
16
16
|
relatedTitle?: string;
|
|
17
|
+
relatedByTagsTitle?: string;
|
|
18
|
+
relatedByAuthorTitle?: string;
|
|
17
19
|
};
|
|
18
20
|
/** Render the cover image */
|
|
19
21
|
renderImage?: (post: BlogPost) => React.ReactNode;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { Div, Heading, RichText, Row, Scrim, Span, Stack, Text } from "../../../ui";
|
|
6
|
+
import { Iframe } from "../../../ui/components/Iframe";
|
|
6
7
|
import { MediaImage } from "../../media/MediaImage";
|
|
7
8
|
import { useBlogPost } from "../hooks/useBlog";
|
|
8
9
|
import { BlogCard } from "./BlogListView";
|
|
@@ -27,13 +28,13 @@ const CLS_FEATURED_BADGE = "bg-warning-surface text-warning dark:bg-warning-surf
|
|
|
27
28
|
function renderBlogPostHeader(post, date, labels) {
|
|
28
29
|
return (_jsxs(Div, { className: "mb-8", children: [_jsxs(Row, { className: "mb-4", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? ""}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && _jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels?.featured ?? "Featured" })] }), _jsx(Heading, { level: 1, className: "mb-4", size: "3xl", weight: "bold", children: post.title }), post.excerpt && _jsx(Text, { className: "mb-6", color: "muted", size: "lg", children: post.excerpt }), _jsxs(Row, { color: "muted", textSize: "sm", wrap: true, gap: "md", children: [post.authorName && _jsxs(Span, { children: [labels?.author ?? "By", " ", _jsx(Span, { weight: "medium", color: "muted", children: post.authorName })] }), post.readTimeMinutes != null && _jsxs(Span, { children: [post.readTimeMinutes, " ", labels?.readTime ?? "min read"] }), date && _jsxs(Span, { children: [labels?.publishedOn ?? "Published", " ", date] }), post.views != null && _jsxs(Span, { children: [post.views, " ", labels?.viewsLabel ?? "views"] })] })] }));
|
|
29
30
|
}
|
|
30
|
-
function renderBlogPostRelated(related,
|
|
31
|
+
function renderBlogPostRelated(related, title, renderRelatedCard) {
|
|
31
32
|
if (related.length === 0)
|
|
32
33
|
return null;
|
|
33
|
-
return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children:
|
|
34
|
+
return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children: title }), _jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-3", children: related.map((rel, i) => renderRelatedCard ? (_jsx(React.Fragment, { children: renderRelatedCard(rel, i) }, rel.id)) : (_jsx(BlogCard, { post: rel, href: String(ROUTES.BLOG.ARTICLE(rel.slug)) }, rel.id))) })] }));
|
|
34
35
|
}
|
|
35
36
|
export function BlogPostView({ slug, initialData, labels = {}, renderImage, renderContent, renderBackButton, renderLoading, renderError, renderRelatedCard, renderAuthorBio, className = "", }) {
|
|
36
|
-
const { post, related, isLoading, error } = useBlogPost(slug, {
|
|
37
|
+
const { post, related, relatedByTags, relatedByAuthor, isLoading, error } = useBlogPost(slug, {
|
|
37
38
|
initialData,
|
|
38
39
|
});
|
|
39
40
|
if (isLoading) {
|
|
@@ -54,5 +55,5 @@ export function BlogPostView({ slug, initialData, labels = {}, renderImage, rend
|
|
|
54
55
|
})
|
|
55
56
|
: "";
|
|
56
57
|
const coverImageUrl = getMediaUrl(post.coverImage);
|
|
57
|
-
return (_jsxs(Div, { className: `min-h-screen ${className}`, children: [coverImageUrl && (_jsxs(Div, { className: `relative h-72 md:h-96 ${__O.hidden}`, children: [renderImage ? (renderImage(post)) : (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "hero" })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-0" })] })), _jsxs(Div, { className: "max-w-3xl mx-auto", paddingY: "y-3xl", paddingX: "x-md", children: [renderBlogPostHeader(post, date, labels), post.tags && post.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-8", children: post.tags.map((tag) => (_jsxs(Link, { href: `${String(ROUTES.PUBLIC.BLOG)}?tags=${encodeURIComponent(tag)}`, className: "inline-block px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] hover:bg-primary/10 text-[var(--appkit-color-text-muted)] hover:text-primary text-[length:var(--appkit-text-xs)] font-medium transition-colors", children: ["#", tag] }, tag))) })), renderAuthorBio && _jsx(Div, { className: "mb-6", children: renderAuthorBio(post) }), _jsx(Div, { surface: "card", padding: "xl", className: "mb-12", children: renderContent ? renderContent(post) : (_jsx(RichText, { html: normalizeRichTextHtml(post.content ?? ""), proseClass: "prose max-w-none dark:prose-invert" })) }), renderBlogPostRelated(related, labels, renderRelatedCard), renderBackButton && _jsx(Div, { border: "default", className: "mt-10 border-t", padding: "t-xl", children: renderBackButton() })] })] }));
|
|
58
|
+
return (_jsxs(Div, { className: `min-h-screen ${className}`, children: [coverImageUrl && (_jsxs(Div, { className: `relative h-72 md:h-96 ${__O.hidden}`, children: [renderImage ? (renderImage(post)) : (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "hero" })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-0" })] })), _jsxs(Div, { className: "max-w-3xl mx-auto", paddingY: "y-3xl", paddingX: "x-md", children: [renderBlogPostHeader(post, date, labels), post.youtubeId && /^[A-Za-z0-9_-]{6,20}$/.test(post.youtubeId) && (_jsx(Div, { className: "mb-8", children: _jsx(Iframe, { src: `https://www.youtube-nocookie.com/embed/${post.youtubeId}`, title: `${post.title} — video`, aspect: "video", rounded: "lg" }) })), post.tags && post.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-8", children: post.tags.map((tag) => (_jsxs(Link, { href: `${String(ROUTES.PUBLIC.BLOG)}?tags=${encodeURIComponent(tag)}`, className: "inline-block px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] hover:bg-primary/10 text-[var(--appkit-color-text-muted)] hover:text-primary text-[length:var(--appkit-text-xs)] font-medium transition-colors", children: ["#", tag] }, tag))) })), renderAuthorBio && _jsx(Div, { className: "mb-6", children: renderAuthorBio(post) }), _jsx(Div, { surface: "card", padding: "xl", className: "mb-12", children: renderContent ? renderContent(post) : (_jsx(RichText, { html: normalizeRichTextHtml(post.content ?? ""), proseClass: "prose max-w-none dark:prose-invert" })) }), _jsxs(Stack, { gap: "xl", children: [renderBlogPostRelated(related, labels?.relatedTitle ?? "Related Posts", renderRelatedCard), renderBlogPostRelated(relatedByTags, labels?.relatedByTagsTitle ?? "You might also like", renderRelatedCard), renderBlogPostRelated(relatedByAuthor, labels?.relatedByAuthorTitle ?? `More from ${post.authorName}`, renderRelatedCard)] }), renderBackButton && _jsx(Div, { border: "default", className: "mt-10 border-t", padding: "t-xl", children: renderBackButton() })] })] }));
|
|
58
59
|
}
|
|
@@ -24,6 +24,8 @@ interface UseBlogPostOptions {
|
|
|
24
24
|
export declare function useBlogPost(slug: string, opts?: UseBlogPostOptions): {
|
|
25
25
|
post: import("..").BlogPost | null;
|
|
26
26
|
related: import("..").BlogPost[];
|
|
27
|
+
relatedByTags: import("..").BlogPost[];
|
|
28
|
+
relatedByAuthor: import("..").BlogPost[];
|
|
27
29
|
isLoading: boolean;
|
|
28
30
|
error: Error | null;
|
|
29
31
|
};
|
|
@@ -46,6 +46,8 @@ export function useBlogPost(slug, opts) {
|
|
|
46
46
|
return {
|
|
47
47
|
post: query.data?.post ?? null,
|
|
48
48
|
related: query.data?.related ?? [],
|
|
49
|
+
relatedByTags: query.data?.relatedByTags ?? [],
|
|
50
|
+
relatedByAuthor: query.data?.relatedByAuthor ?? [],
|
|
49
51
|
isLoading: query.isLoading,
|
|
50
52
|
error: query.error,
|
|
51
53
|
};
|
|
@@ -10,6 +10,13 @@ declare class BlogRepository extends BaseRepository<BlogPostDocument> {
|
|
|
10
10
|
delete(id: string): Promise<void>;
|
|
11
11
|
incrementViews(id: string): Promise<void>;
|
|
12
12
|
findRelated(category: BlogPostCategory, excludeId: string, limit?: number): Promise<BlogPostDocument[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Published posts sharing at least one tag with the given list.
|
|
15
|
+
* `array-contains-any` caps at 10 values, so only the first 10 tags are used.
|
|
16
|
+
*/
|
|
17
|
+
findByTagsOverlap(tags: string[], excludeId: string, limit?: number): Promise<BlogPostDocument[]>;
|
|
18
|
+
/** Other published posts by the same author. */
|
|
19
|
+
findByAuthor(authorId: string, excludeId: string, limit?: number): Promise<BlogPostDocument[]>;
|
|
13
20
|
static readonly SIEVE_FIELDS: {
|
|
14
21
|
id: {
|
|
15
22
|
canFilter: boolean;
|