@mohasinac/appkit 4.53.1 → 4.54.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/.build-stamp.json +3 -3
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/auth/capabilities.js +7 -5
- package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +22 -6
- package/dist/_internal/server/features/checkout/actions.js +96 -51
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
- package/dist/_internal/server/features/orders/adapters.js +1 -3
- package/dist/_internal/server/features/price-index/market.d.ts +57 -0
- package/dist/_internal/server/features/price-index/market.js +120 -0
- package/dist/_internal/server/features/products/list-public.d.ts +12 -0
- package/dist/_internal/server/features/products/list-public.js +10 -1
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
- package/dist/_internal/server/features/products/listing-tab-counts.js +184 -10
- package/dist/_internal/server/features/search/index-build/sources.js +43 -2
- package/dist/_internal/server/features/site-settings/adapters.js +1 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +0 -1
- package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
- package/dist/_internal/server/features/stores/tabs.js +5 -3
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +2 -5
- package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
- package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +0 -2
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +0 -1
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +0 -1
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +0 -1
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +0 -1
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +0 -1
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +0 -1
- package/dist/_internal/server/notifications/channel-health.d.ts +7 -3
- package/dist/_internal/server/notifications/channel-health.js +2 -2
- package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
- package/dist/_internal/server/notifications/send-guard.js +2 -4
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +128 -28
- package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
- package/dist/_internal/shared/checkout/order-math.js +1 -1
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.d.ts +0 -2
- package/dist/_internal/shared/features/checkout/config.js +0 -2
- package/dist/_internal/shared/features/messaging/config.d.ts +7 -5
- package/dist/_internal/shared/features/messaging/config.js +1 -4
- package/dist/_internal/shared/features/site-settings/fees.d.ts +0 -2
- package/dist/_internal/shared/features/site-settings/fees.js +0 -2
- package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
- package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
- package/dist/_internal/shared/fees/calculator.d.ts +0 -8
- package/dist/_internal/shared/fees/calculator.js +0 -7
- package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
- package/dist/_internal/shared/listing-types/tiers.js +150 -0
- package/dist/_internal/shared/search/match.js +28 -2
- package/dist/cli/index.js +0 -2
- package/dist/client.d.ts +25 -4
- package/dist/client.js +36 -5
- package/dist/constants/api-endpoints.d.ts +28 -15
- package/dist/constants/api-endpoints.js +14 -8
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +1 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/constants/notification-types.js +1 -1
- package/dist/core/integration-keys.d.ts +5 -5
- package/dist/core/integration-keys.js +21 -10
- package/dist/features/about/components/FeesView.js +0 -6
- package/dist/features/about/components/GuidesHubView.js +15 -2
- package/dist/features/account/components/NotificationPreferencesPanel.js +3 -4
- package/dist/features/account/components/UserOrdersView.js +26 -7
- package/dist/features/account/types/index.d.ts +0 -2
- package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
- package/dist/features/addresses/utils/postal-autofill.js +54 -0
- package/dist/features/admin/actions/notification-actions.d.ts +2 -8
- package/dist/features/admin/actions/notification-actions.js +9 -104
- package/dist/features/admin/components/AdminGuideHubView.js +1 -8
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
- package/dist/features/admin/components/AdminOrdersView.js +0 -2
- package/dist/features/admin/components/AdminSectionsView.js +3 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +22 -68
- package/dist/features/admin/components/AdminStoreEditorView.js +72 -34
- package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
- package/dist/features/admin/components/OwnerQuickActions.js +142 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.js +13 -4
- package/dist/features/admin/components/sections/icon-options.d.ts +21 -0
- package/dist/features/admin/components/sections/icon-options.js +23 -0
- package/dist/features/admin/components/sections/sectionTemplates.js +4 -1
- package/dist/features/admin/constants/site-settings-tabs.d.ts +7 -12
- package/dist/features/admin/constants/site-settings-tabs.js +6 -12
- package/dist/features/admin/repository/site-settings.repository.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +0 -61
- package/dist/features/admin/schemas/admin-user-form.js +3 -44
- package/dist/features/admin/schemas/firestore.d.ts +28 -54
- package/dist/features/admin/schemas/firestore.js +40 -18
- package/dist/features/auctions/components/AuctionDetailPageView.js +12 -10
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
- package/dist/features/auth/checkout-value-otp.d.ts +1 -1
- package/dist/features/auth/checkout-value-otp.js +1 -1
- package/dist/features/auth/permissions/constants.d.ts +29 -2
- package/dist/features/auth/permissions/constants.js +67 -1
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
- package/dist/features/cart/components/CartDrawer.js +3 -2
- package/dist/features/cart/components/CartGroupLineRow.js +2 -1
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +0 -2
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +2 -5
- package/dist/features/cart/components/StoreAddonsPicker.js +4 -5
- package/dist/features/cart/hooks/useAddToCart.js +10 -5
- package/dist/features/cart/repository/cart.repository.js +1 -2
- package/dist/features/cart/schemas/firestore.d.ts +1 -2
- package/dist/features/cart/utils/pending-ops.d.ts +11 -1
- package/dist/features/cart/utils/pending-ops.js +14 -2
- package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
- package/dist/features/categories/components/BrandDetailPageView.js +15 -6
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +18 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleDetailView.js +2 -1
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +19 -8
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
- package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
- package/dist/features/categories/components/CategoryGrid.js +9 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
- package/dist/features/classified/components/ClassifiedDetailPageView.js +12 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +12 -2
- package/dist/features/events/components/EventCard.d.ts +11 -1
- package/dist/features/events/components/EventCard.js +9 -2
- package/dist/features/grouped/components/GroupedListingDetailView.js +3 -1
- package/dist/features/homepage/components/CTABannerSection.d.ts +3 -1
- package/dist/features/homepage/components/CTABannerSection.js +6 -3
- package/dist/features/homepage/components/HeroCarousel.js +8 -6
- package/dist/features/homepage/components/MarketplaceHomepageView.js +33 -7
- package/dist/features/homepage/components/SectionCarousel.js +13 -5
- package/dist/features/homepage/components/SecurityHighlightsSection.d.ts +8 -3
- package/dist/features/homepage/components/SecurityHighlightsSection.js +2 -2
- package/dist/features/homepage/components/StatsCounterSection.d.ts +9 -4
- package/dist/features/homepage/components/StatsCounterSection.js +2 -2
- package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
- package/dist/features/homepage/components/TrustFeaturesSection.d.ts +18 -4
- package/dist/features/homepage/components/TrustFeaturesSection.js +5 -5
- package/dist/features/homepage/components/WelcomeSection.d.ts +13 -2
- package/dist/features/homepage/components/WelcomeSection.js +3 -3
- package/dist/features/homepage/components/WhatsAppCommunitySection.d.ts +28 -0
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +42 -6
- package/dist/features/homepage/components/WhatsAppGlyph.d.ts +20 -0
- package/dist/features/homepage/components/WhatsAppGlyph.js +20 -0
- package/dist/features/homepage/constants/hero-carousel-styles.d.ts +8 -6
- package/dist/features/homepage/constants/hero-carousel-styles.js +8 -6
- package/dist/features/homepage/constants/section-copy.d.ts +12 -10
- package/dist/features/homepage/constants/section-copy.js +28 -6
- package/dist/features/homepage/constants/stat-icons.d.ts +12 -0
- package/dist/features/homepage/constants/stat-icons.js +17 -0
- package/dist/features/homepage/constants/trust-icon-aliases.d.ts +8 -0
- package/dist/features/homepage/constants/trust-icon-aliases.js +75 -0
- package/dist/features/homepage/lib/section-defaults.d.ts +16 -1
- package/dist/features/homepage/lib/section-defaults.js +24 -4
- package/dist/features/homepage/lib/section-renderer.js +48 -8
- package/dist/features/homepage/schemas/firestore.d.ts +18 -1
- package/dist/features/layout/AppLayoutShell.js +2 -1
- package/dist/features/layout/BottomActions.d.ts +1 -0
- package/dist/features/layout/BottomActions.js +330 -0
- package/dist/features/layout/BottomChrome.d.ts +45 -0
- package/dist/features/layout/BottomChrome.js +5 -17
- package/dist/features/layout/SideActionDock.js +36 -13
- package/dist/features/layout/index.d.ts +2 -7
- package/dist/features/layout/index.js +7 -7
- package/dist/features/layout/useIsDesktop.d.ts +36 -0
- package/dist/features/layout/useIsDesktop.js +48 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +12 -2
- package/dist/features/orders/actions/refund-actions.js +0 -1
- package/dist/features/orders/components/OrderAddonBadges.d.ts +1 -3
- package/dist/features/orders/components/OrderAddonBadges.js +3 -4
- package/dist/features/orders/components/OrderPaymentSummary.js +15 -2
- package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
- package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
- package/dist/features/orders/components/index.d.ts +2 -1
- package/dist/features/orders/components/index.js +1 -1
- package/dist/features/orders/repository/orders.repository.d.ts +0 -8
- package/dist/features/orders/repository/orders.repository.js +0 -2
- package/dist/features/orders/schemas/firestore.d.ts +17 -9
- package/dist/features/orders/types/index.d.ts +2 -5
- package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
- package/dist/features/orders/utils/order-announcement-message.js +16 -0
- package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
- package/dist/features/orders/utils/payment-proof-message.js +17 -0
- package/dist/features/parts/components/PartDetailPageView.js +96 -1
- package/dist/features/parts/schemas/firestore.d.ts +247 -0
- package/dist/features/parts/schemas/firestore.js +6 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +12 -10
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
- package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
- package/dist/features/price-index/lookup-target.d.ts +48 -0
- package/dist/features/price-index/lookup-target.js +107 -0
- package/dist/features/products/api/[id]/route.js +8 -1
- package/dist/features/products/api/route.js +16 -4
- package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +2 -1
- package/dist/features/products/components/CompareOverlay.js +2 -1
- package/dist/features/products/components/GroupSettingsPanel.js +2 -1
- package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
- package/dist/features/products/components/InteractiveProductCard.js +16 -4
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +34 -4
- package/dist/features/products/components/ProductGrid.d.ts +14 -1
- package/dist/features/products/components/ProductGrid.js +22 -5
- package/dist/features/products/components/ProductsIndexListing.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -0
- package/dist/features/products/repository/products.repository.js +5 -1
- package/dist/features/products/utils/group-member-image.d.ts +9 -2
- package/dist/features/products/utils/group-member-image.js +2 -1
- package/dist/features/reviews/api/[id]/route.js +83 -23
- package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
- package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
- package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
- package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
- package/dist/features/reviews/components/index.d.ts +4 -0
- package/dist/features/reviews/components/index.js +2 -0
- package/dist/features/scams/schemas/index.d.ts +10 -10
- package/dist/features/search/action-index/settings-entries.js +0 -9
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +49 -3
- package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
- package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +1 -6
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +1 -10
- package/dist/features/store-extensions/schemas/firestore.d.ts +1 -17
- package/dist/features/store-extensions/schemas/firestore.js +0 -10
- package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +0 -1
- package/dist/features/stores/components/StoreGuideHubView.js +2 -8
- package/dist/features/stores/components/StoreSettingsGuideView.js +2 -6
- package/dist/features/stores/components/index.d.ts +0 -2
- package/dist/features/stores/components/index.js +0 -1
- package/dist/features/stores/repository/store.repository.d.ts +1 -26
- package/dist/features/stores/repository/store.repository.js +5 -60
- package/dist/features/stores/schemas/firestore.d.ts +6 -25
- package/dist/features/stores/schemas/firestore.js +6 -3
- package/dist/features/stores/schemas/index.d.ts +1 -87
- package/dist/features/stores/schemas/index.js +10 -15
- package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -48
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +0 -40
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
- package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +0 -16
- package/dist/features/tester/seed-data/authored/selling__seller-guide.js +1 -1
- package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +0 -79
- package/dist/features/tester/seed-data/authored/selling__seller-orders.js +1 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +427 -69
- package/dist/index.d.ts +20 -32
- package/dist/index.js +34 -43
- package/dist/next/routing/route-map.d.ts +2 -8
- package/dist/next/routing/route-map.js +21 -4
- package/dist/providers/db-firebase/base.repository.d.ts +1 -1
- package/dist/providers/db-firebase/base.repository.js +1 -1
- package/dist/providers/payment-razorpay/index.d.ts +125 -0
- package/dist/providers/payment-razorpay/index.js +232 -0
- package/dist/providers/payment-razorpay/signature.d.ts +53 -0
- package/dist/providers/payment-razorpay/signature.js +71 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
- package/dist/providers/shipping-shiprocket/index.js +356 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +3 -0
- package/dist/react/hooks/useCanSeePrices.d.ts +0 -27
- package/dist/react/hooks/useCanSeePrices.js +25 -0
- package/dist/repositories/index.d.ts +1 -1
- package/dist/repositories/index.js +1 -1
- package/dist/schemas/registry.d.ts +10 -115
- package/dist/schemas/registry.js +1 -2
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/seed/_helpers/category-forest.js +27 -0
- package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
- package/dist/seed/_helpers/category-parts.d.ts +1 -1
- package/dist/seed/_helpers/category-parts.js +13412 -3020
- package/dist/seed/carousel-slides-seed-data.js +47 -10
- package/dist/seed/categories-seed-data.js +48 -6
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/homepage-sections-seed-data.js +286 -54
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +1 -1
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +1 -2
- package/dist/seed/model-composition-seed-data.js +6 -8
- package/dist/seed/part-glossary/chunk0.d.ts +9 -0
- package/dist/seed/part-glossary/chunk0.js +35448 -0
- package/dist/seed/part-glossary/chunk1.d.ts +9 -0
- package/dist/seed/part-glossary/chunk1.js +40494 -0
- package/dist/seed/part-glossary/chunk2.d.ts +9 -0
- package/dist/seed/part-glossary/chunk2.js +51242 -0
- package/dist/seed/part-glossary/chunk3.d.ts +9 -0
- package/dist/seed/part-glossary/chunk3.js +53337 -0
- package/dist/seed/part-glossary/chunk4.d.ts +9 -0
- package/dist/seed/part-glossary/chunk4.js +51454 -0
- package/dist/seed/part-glossary-seed-data.d.ts +1 -1
- package/dist/seed/part-glossary-seed-data.js +23 -47483
- package/dist/seed/products-beyblade-x-seed-data.js +36 -1
- package/dist/seed/site-settings-seed-data.js +34 -35
- package/dist/seed/store-extensions-seed-data.d.ts +1 -2
- package/dist/seed/store-extensions-seed-data.js +1 -23
- package/dist/seed/stores-seed-data.js +0 -1
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +1 -1
- package/dist/server.d.ts +10 -10
- package/dist/server.js +24 -22
- package/dist/styles.css +392 -43
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -1
- package/dist/tokens/themes/default-light.js +2 -1
- package/dist/tokens/tokens.css +6 -2
- package/dist/ui/components/Button.d.ts +11 -0
- package/dist/ui/components/Button.js +11 -0
- package/dist/ui/components/Button.style.css +29 -0
- package/dist/ui/components/CarouselNav.d.ts +57 -0
- package/dist/ui/components/CarouselNav.js +25 -0
- package/dist/ui/components/CarouselNav.style.css +265 -0
- package/dist/ui/components/GatedPrice.js +30 -3
- package/dist/ui/components/GatedPrice.style.css +43 -40
- package/dist/ui/components/HorizontalScroller.d.ts +12 -1
- package/dist/ui/components/HorizontalScroller.js +43 -3
- package/dist/ui/components/ImageMosaic.d.ts +8 -1
- package/dist/ui/components/ImageMosaic.js +3 -2
- package/dist/ui/components/ImageMosaic.style.css +22 -0
- package/dist/ui/components/Layout.style.css +6 -0
- package/dist/ui/components/Semantic.d.ts +3 -2
- package/dist/ui/components/Semantic.js +1 -0
- package/dist/ui/components/Typography.d.ts +2 -0
- package/dist/ui/components/Typography.js +2 -0
- package/dist/ui/components/Typography.style.css +19 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.js +4 -0
- package/package.json +3 -2
- package/scripts/seed-cli.mjs +133 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
- package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
- package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
- package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
- package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
- package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
- package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
- package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
- package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/components/index.js +0 -2
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
- package/dist/features/whatsapp-bot/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/index.js +0 -3
- package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
- package/dist/features/whatsapp-bot/manifest.js +0 -13
- package/dist/features/whatsapp-bot/messages/en.json +0 -7
- package/dist/features/whatsapp-bot/server.d.ts +0 -7
- package/dist/features/whatsapp-bot/server.js +0 -7
- package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
- package/dist/features/whatsapp-bot/types/index.js +0 -1
|
@@ -5,6 +5,7 @@ import { categoryCrumbs, withTrailHead } from "../utils/breadcrumb-chain.js";
|
|
|
5
5
|
import { ownTotalFor, descendantTotalFor } from "../schemas/category-counts.js";
|
|
6
6
|
import { categoriesRepository, productRepository, storeRepository, } from "../../../repositories/index.js";
|
|
7
7
|
import { Container, Div, Heading, ImageMosaic, Main, MOSAIC_MIN, Section, Span, SplitTile, Stack, Text } from "../../../ui/index.js";
|
|
8
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
8
9
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
10
|
import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
|
|
10
11
|
import { resolveTaxonomyVisual } from "../utils/taxonomy-visual.js";
|
|
@@ -27,6 +28,7 @@ import { safeRead } from "../../../errors/safe-read.js";
|
|
|
27
28
|
import { breadcrumbJsonLd, collectionPageJsonLd, faqJsonLd, } from "../../../seo/json-ld.js";
|
|
28
29
|
import { toStoreListItem } from "../../../_internal/server/features/stores/adapters.js";
|
|
29
30
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
31
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
30
32
|
const __O = {
|
|
31
33
|
hidden: "overflow-hidden",
|
|
32
34
|
};
|
|
@@ -170,7 +172,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
170
172
|
// Four of the ten tabs used to have no count at all and could never hide.
|
|
171
173
|
categoriesIn
|
|
172
174
|
? listingTabCounts(CATEGORY_PAGE_TABS, { categoriesIn })
|
|
173
|
-
: Promise.resolve({}),
|
|
175
|
+
: Promise.resolve({ available: {}, total: {} }),
|
|
174
176
|
// SB-UNI-D — bundles fetched from the categories collection. We pull
|
|
175
177
|
// all active bundle rows; the carousel filters by category affinity.
|
|
176
178
|
categoriesIn
|
|
@@ -419,13 +421,22 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
419
421
|
// `tabCounts` is keyed by tabSlug and already covers every listing type plus
|
|
420
422
|
// bundles; `stores` is added here because this page resolves its store list
|
|
421
423
|
// eagerly to render the tab body anyway.
|
|
422
|
-
|
|
424
|
+
// Two maps, not one: the BADGE is what the default (available) scope will
|
|
425
|
+
// render, while HIDING must stay keyed on the total — a tab whose rows have
|
|
426
|
+
// all ended still has a browsable archive behind the Ended/All scopes.
|
|
427
|
+
// `stores` is added to both because this page resolves its store list
|
|
428
|
+
// eagerly to render the tab body anyway, and a store has no availability
|
|
429
|
+
// scope of its own.
|
|
430
|
+
const counts = { ...tabCounts.available, stores: storeCount };
|
|
431
|
+
const totals = { ...tabCounts.total, stores: storeCount };
|
|
423
432
|
// Header pills read the same numbers the tabs do. `?? 0` is safe here (a pill
|
|
424
433
|
// simply doesn't render) — unlike the tab bar, where an unknown count must
|
|
425
434
|
// stay visible rather than collapse to "hide me".
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
435
|
+
// Header pills read TOTALS, deliberately: "47 products" is a statement about
|
|
436
|
+
// what this category holds, not about what one scope is currently showing.
|
|
437
|
+
const productCount = totals.products ?? category?.metrics?.totalProductCount ?? category?.metrics?.productCount ?? 0;
|
|
438
|
+
const auctionCount = totals.auctions ?? category?.metrics?.totalAuctionCount ?? category?.metrics?.auctionCount ?? 0;
|
|
439
|
+
const preOrderCount = totals["pre-orders"] ?? 0;
|
|
429
440
|
/*
|
|
430
441
|
* G3b — OWN vs CHILDREN, so one number stops answering two questions.
|
|
431
442
|
*
|
|
@@ -465,7 +476,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
465
476
|
const mosaicImages = hasCover
|
|
466
477
|
? []
|
|
467
478
|
: (productsResult?.items ?? [])
|
|
468
|
-
.map((row) => row.mainImage
|
|
479
|
+
.map((row) => row.mainImage || productImageUrls(row.images)[0] || null)
|
|
469
480
|
.filter((src) => !!src)
|
|
470
481
|
.slice(0, 5);
|
|
471
482
|
const hasMosaic = mosaicImages.length >= MOSAIC_MIN;
|
|
@@ -562,7 +573,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
562
573
|
const ldFaq = category?.faqs && category.faqs.length > 0
|
|
563
574
|
? faqJsonLd(category.faqs.map((f) => ({ question: f.question, answer: f.answer })))
|
|
564
575
|
: null;
|
|
565
|
-
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
|
|
576
|
+
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), category?.id && (_jsx(OwnerQuickActions, { resource: "CATEGORY", placement: "detail", className: "mb-3", recordLabel: category.name ?? slug, editHref: String(ROUTES.ADMIN.CATEGORIES_EDIT(category.id)) })), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
|
|
566
577
|
if (category?.tier !== 0)
|
|
567
578
|
return null;
|
|
568
579
|
const branches = childCategories.filter((c) => c.tier === 1);
|
|
@@ -587,5 +598,5 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
587
598
|
imageUrl: coverOf(b),
|
|
588
599
|
countLabel: countOf(b),
|
|
589
600
|
} }) }) }));
|
|
590
|
-
})(), directChildrenWithListings.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: _jsxs(Div, { padding: "b-2xs", children: [parentLadder.length > 0 && (_jsx(Div, { padding: "b-2xs", children: _jsx(SubcategoryCardRow, { items: parentLadder, label: "Parent category" }) })), _jsx(SubcategoryCardRow, { items: toSubcategoryItems(directChildrenWithListings, childCategories) })] }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
|
|
601
|
+
})(), directChildrenWithListings.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: _jsxs(Div, { padding: "b-2xs", children: [parentLadder.length > 0 && (_jsx(Div, { padding: "b-2xs", children: _jsx(SubcategoryCardRow, { items: parentLadder, label: "Parent category" }) })), _jsx(SubcategoryCardRow, { items: toSubcategoryItems(directChildrenWithListings, childCategories) })] }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, totals: totals, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
|
|
591
602
|
}
|
|
@@ -17,9 +17,15 @@ export interface CategoryDetailTabsProps {
|
|
|
17
17
|
* were structurally uncountable and could never hide (Root Cause #61).
|
|
18
18
|
*/
|
|
19
19
|
counts?: Record<string, number | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Every published row per tab, all availability scopes. Drives hide-at-zero
|
|
22
|
+
* and the "N ended — show all" hint; see `ListingTabCountResult`. Omitted,
|
|
23
|
+
* it falls back to `counts`, i.e. the pre-split behaviour.
|
|
24
|
+
*/
|
|
25
|
+
totals?: Record<string, number | undefined>;
|
|
20
26
|
/** Enabled listing types (e.g. ["standard","auction","pre-order"]). When omitted, all tabs shown. */
|
|
21
27
|
enabledListingTypes?: string[];
|
|
22
28
|
/** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
|
|
23
29
|
enabledCategoryTypes?: string[];
|
|
24
30
|
}
|
|
25
|
-
export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, initialStores, counts, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): React.JSX.Element;
|
|
31
|
+
export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, initialStores, counts, totals, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): React.JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { EmptyState, Tabs, TabsList, TabsTrigger } from "../../../ui/index.js";
|
|
5
|
+
import { AvailabilityScopeHint } from "../../products/components/AvailabilityScopeHint.js";
|
|
5
6
|
import { CategoryProductsListing } from "./CategoryProductsListing.js";
|
|
6
7
|
import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing.js";
|
|
7
8
|
import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing.js";
|
|
@@ -29,6 +30,21 @@ const TAB_TYPE_MAP = {
|
|
|
29
30
|
bundles: { kind: "category", type: "bundle" },
|
|
30
31
|
stores: { kind: "entity", type: "stores" },
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The listing types a tab spans, for the availability-scope hint.
|
|
35
|
+
*
|
|
36
|
+
* Derived from `TAB_TYPE_MAP` rather than hand-written: that map is already
|
|
37
|
+
* built from `ALL_LISTING_TYPES`, so a tenth type is covered without a second
|
|
38
|
+
* enumeration (Root Cause #61). The combined Art & Stickers tab is the one
|
|
39
|
+
* entry that spans two, and `GENERIC_TAB_LISTING_TYPES` already says so.
|
|
40
|
+
*/
|
|
41
|
+
function typesForTab(id) {
|
|
42
|
+
const generic = GENERIC_TAB_LISTING_TYPES[id];
|
|
43
|
+
if (generic)
|
|
44
|
+
return generic;
|
|
45
|
+
const mapping = TAB_TYPE_MAP[id];
|
|
46
|
+
return mapping?.kind === "listing" ? [mapping.type] : [];
|
|
47
|
+
}
|
|
32
48
|
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
33
49
|
const GENERIC_TAB_LISTING_TYPES = {
|
|
34
50
|
[pluginFor("standard").tabSlug]: ["standard"],
|
|
@@ -50,8 +66,12 @@ const GENERIC_TAB_LISTING_TYPES = {
|
|
|
50
66
|
*/
|
|
51
67
|
[pluginFor("spares").tabSlug]: ["spares"],
|
|
52
68
|
};
|
|
53
|
-
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
69
|
+
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, totals, enabledListingTypes, enabledCategoryTypes, }) {
|
|
54
70
|
const countFor = (id) => counts?.[id];
|
|
71
|
+
// Falls back to the badge when no totals map is supplied, so a caller that
|
|
72
|
+
// has not been migrated keeps today's hide behaviour rather than hiding
|
|
73
|
+
// everything.
|
|
74
|
+
const totalFor = (id) => totals?.[id] ?? counts?.[id];
|
|
55
75
|
const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
|
|
56
76
|
const mapping = TAB_TYPE_MAP[t.id];
|
|
57
77
|
if (mapping) {
|
|
@@ -64,11 +84,13 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
|
|
|
64
84
|
return false;
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
87
|
+
// 🛑 TOTAL, not the badge. The badge counts what the default (available)
|
|
88
|
+
// scope renders, and a tab whose rows have all ended badges 0 while still
|
|
89
|
+
// holding a browsable archive behind the Ended/All scopes — hiding it
|
|
90
|
+
// would strand that archive. `undefined` still means the count query
|
|
91
|
+
// FAILED, so the tab stays visible: an empty tab is a far smaller error
|
|
92
|
+
// than hiding a tab that holds real listings (Root Cause #59).
|
|
93
|
+
const count = totalFor(t.id);
|
|
72
94
|
return count === undefined || count > 0;
|
|
73
95
|
});
|
|
74
96
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
@@ -78,5 +100,5 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
|
|
|
78
100
|
if (visibleTabs.length === 0) {
|
|
79
101
|
return (_jsx(EmptyState, { title: "Nothing listed here yet", description: "This category has no active listings right now. Check back soon." }));
|
|
80
102
|
}
|
|
81
|
-
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: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
|
|
103
|
+
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))) }) }), _jsx(AvailabilityScopeHint, { total: totalFor(activeTab), available: countFor(activeTab), types: typesForTab(activeTab) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
|
|
82
104
|
}
|
|
@@ -5,8 +5,18 @@ export interface CategoryCardProps {
|
|
|
5
5
|
href?: string;
|
|
6
6
|
onClick?: (category: CategoryItem) => void;
|
|
7
7
|
className?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Owner-only pencil / clone / bin, for an admin (categories have no owner,
|
|
10
|
+
* so only an admin ever sees it). Pass `<OwnerQuickActions>`.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 Rendered OUTSIDE this card's `<Link>`, as a sibling. A `<button>`
|
|
13
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
14
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
15
|
+
* button's.
|
|
16
|
+
*/
|
|
17
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
8
18
|
}
|
|
9
|
-
export declare function CategoryCard({ category, href, onClick, className, }: CategoryCardProps): React.JSX.Element;
|
|
19
|
+
export declare function CategoryCard({ category, href, onClick, className, renderOwnerActions, }: CategoryCardProps): React.JSX.Element;
|
|
10
20
|
export interface CategoryGridProps {
|
|
11
21
|
categories: CategoryItem[];
|
|
12
22
|
getHref?: (category: CategoryItem) => string;
|
|
@@ -12,7 +12,7 @@ const __O = {
|
|
|
12
12
|
hidden: "overflow-hidden",
|
|
13
13
|
};
|
|
14
14
|
const CLS_FEATURED_DOT = "absolute left-2 top-2 rounded-full bg-warning-surface p-[var(--appkit-space-1)] leading-none";
|
|
15
|
-
export function CategoryCard({ category, href, onClick, className = "", }) {
|
|
15
|
+
export function CategoryCard({ category, href, onClick, className = "", renderOwnerActions, }) {
|
|
16
16
|
// totalProductCount is the rollup (self + all descendant categories),
|
|
17
17
|
// maintained incrementally by onProductWrite — prefer it over the
|
|
18
18
|
// own-node-only productCount so a parent category's card shows the same
|
|
@@ -32,9 +32,16 @@ export function CategoryCard({ category, href, onClick, className = "", }) {
|
|
|
32
32
|
if (href) {
|
|
33
33
|
return (_jsx(Link, { href: href, className: cardClass, children: inner }));
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
const card = (_jsx(Div, { role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
|
|
36
36
|
? (e) => (e.key === "Enter" || e.key === " ") && onClick(category)
|
|
37
37
|
: undefined, onClick: onClick ? () => onClick(category) : undefined, className: cardClass, children: inner }));
|
|
38
|
+
if (!renderOwnerActions)
|
|
39
|
+
return card;
|
|
40
|
+
/*
|
|
41
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
42
|
+
* beats the featured dot at `left-2 top-2`.
|
|
43
|
+
*/
|
|
44
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
38
45
|
}
|
|
39
46
|
export function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel = "No categories found", className = "", }) {
|
|
40
47
|
if (categories.length === 0) {
|
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* purpose from `checkout-actions.ts`'s third-party shipping consent OTP —
|
|
7
7
|
* same crypto primitives, separate Firestore namespace and copy.
|
|
8
8
|
*/
|
|
9
|
-
export type CheckoutValueOtpChannel = "email" | "whatsapp";
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Email is the only checkout-OTP channel. WhatsApp was the other arm until
|
|
11
|
+
* 2026-10-20, when the Cloud API integration was removed — see
|
|
12
|
+
* `features/orders/utils/payment-proof-message.ts` for why the `wa.me`
|
|
13
|
+
* links survived and the API did not. Kept as a single-member union rather
|
|
14
|
+
* than deleted so the parameter stays self-documenting and a second channel
|
|
15
|
+
* has an obvious seam.
|
|
16
|
+
*/
|
|
17
|
+
export type CheckoutValueOtpChannel = "email";
|
|
18
|
+
/**
|
|
19
|
+
* Sends a checkout-value OTP by email.
|
|
15
20
|
*
|
|
16
21
|
* If the chosen channel (and, for email, its skip-worthy fallback) is
|
|
17
22
|
* currently circuit-broken or unconfigured, the OTP gate is treated as
|
|
@@ -9,17 +9,12 @@
|
|
|
9
9
|
import { timingSafeEqual } from "crypto";
|
|
10
10
|
import { ValidationError } from "../../../errors/index.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
|
-
import { safeRead } from "../../../errors/safe-read.js";
|
|
13
12
|
import { serverLogger } from "../../../monitoring/index.js";
|
|
14
13
|
import { sendEmail } from "../../contact/email.js";
|
|
15
14
|
import { getAdminDb } from "../../../providers/db-firebase/index.js";
|
|
16
15
|
import { resolveDate } from "../../../utils/index.js";
|
|
17
|
-
import { addressesRepository } from "../../addresses/repository/addresses.repository.js";
|
|
18
|
-
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository.js";
|
|
19
|
-
import { sendWhatsAppBusinessMessage } from "../../whatsapp-bot/helpers/whatsapp.js";
|
|
20
16
|
import { isChannelHealthy, withChannelRetry } from "../../../_internal/server/notifications/channel-health.js";
|
|
21
|
-
import {
|
|
22
|
-
import { CHECKOUT_VALUE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS, hashOtp, generateOtpCode, checkoutValueOtpRef, enforceCheckoutValueOtpRateLimit, buildCheckoutValueOtpMessage, } from "../../auth/checkout-value-otp.js";
|
|
17
|
+
import { CHECKOUT_VALUE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS, hashOtp, generateOtpCode, checkoutValueOtpRef, enforceCheckoutValueOtpRateLimit, } from "../../auth/checkout-value-otp.js";
|
|
23
18
|
function maskEmail(userEmail) {
|
|
24
19
|
const [local, domain] = userEmail.split("@");
|
|
25
20
|
return domain
|
|
@@ -27,10 +22,7 @@ function maskEmail(userEmail) {
|
|
|
27
22
|
: "***";
|
|
28
23
|
}
|
|
29
24
|
/**
|
|
30
|
-
* Sends a checkout-value OTP
|
|
31
|
-
* (opt-in — the buyer must explicitly choose it in the UI). WhatsApp phone
|
|
32
|
-
* is always resolved server-side from the selected shipping address —
|
|
33
|
-
* never trust a client-supplied phone number.
|
|
25
|
+
* Sends a checkout-value OTP by email.
|
|
34
26
|
*
|
|
35
27
|
* If the chosen channel (and, for email, its skip-worthy fallback) is
|
|
36
28
|
* currently circuit-broken or unconfigured, the OTP gate is treated as
|
|
@@ -45,84 +37,6 @@ export async function sendCheckoutValueOtp(userId, userEmail, channel = "email",
|
|
|
45
37
|
const codeHash = hashOtp(code);
|
|
46
38
|
const expiresAt = new Date(Date.now() + CHECKOUT_VALUE_OTP_EXPIRY_MS);
|
|
47
39
|
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || "LetItRip";
|
|
48
|
-
if (channel === "whatsapp") {
|
|
49
|
-
if (!addressId) {
|
|
50
|
-
throw new ValidationError("Select a delivery address before sending the code via WhatsApp.");
|
|
51
|
-
}
|
|
52
|
-
// A failed settings read makes the WhatsApp channel look deliberately
|
|
53
|
-
// switched off ("send the code by email instead") — a defensible fallback,
|
|
54
|
-
// but one an operator has to be able to see, since it is indistinguishable
|
|
55
|
-
// from an admin having disabled it.
|
|
56
|
-
const settings = await safeRead(() => siteSettingsRepository.getSingleton(), {
|
|
57
|
-
route: "checkout/value-otp",
|
|
58
|
-
key: "checkoutValueOtp.siteSettings",
|
|
59
|
-
fallback: null,
|
|
60
|
-
});
|
|
61
|
-
const creds = settings?.credentials ?? {};
|
|
62
|
-
const whatsappOtpEnabled = settings?.notificationChannels?.whatsapp?.otpEnabled === true;
|
|
63
|
-
const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
|
|
64
|
-
const accessToken = creds.whatsappCloudApiToken?.trim() ?? "";
|
|
65
|
-
const credsReady = phoneNumberId && accessToken && !phoneNumberId.includes("PLACEHOLDER") && !accessToken.includes("PLACEHOLDER");
|
|
66
|
-
if (!whatsappOtpEnabled || !credsReady) {
|
|
67
|
-
throw new ValidationError("WhatsApp verification isn't available right now — send the code by email instead.");
|
|
68
|
-
}
|
|
69
|
-
const address = await addressesRepository.findById(addressId);
|
|
70
|
-
/*
|
|
71
|
-
* REVEALED: this phone receives the high-value checkout OTP, so it has to
|
|
72
|
-
* be the real number. `addressesRepository` is ciphertext-by-default, and
|
|
73
|
-
* `.trim()` on the stored value is what the nominal brand caught — the
|
|
74
|
-
* old intersection form let `Encrypted<string>` call string methods.
|
|
75
|
-
*/
|
|
76
|
-
const addressPlain = address ? addressesRepository.revealAddress(address) : null;
|
|
77
|
-
const phone = addressPlain?.phone?.trim();
|
|
78
|
-
if (!phone) {
|
|
79
|
-
throw new ValidationError("The selected address has no phone number on file — send the code by email instead.");
|
|
80
|
-
}
|
|
81
|
-
if (!(await isChannelHealthy("whatsapp"))) {
|
|
82
|
-
serverLogger.warn("sendCheckoutValueOtp: WhatsApp channel unhealthy — skipping OTP gate", { uid: userId, channel });
|
|
83
|
-
return { skipped: true };
|
|
84
|
-
}
|
|
85
|
-
await checkoutValueOtpRef(db, userId).set({
|
|
86
|
-
codeHash,
|
|
87
|
-
expiresAt,
|
|
88
|
-
attempts: 0,
|
|
89
|
-
verified: false,
|
|
90
|
-
createdAt: new Date(),
|
|
91
|
-
});
|
|
92
|
-
const message = buildCheckoutValueOtpMessage(code, siteName);
|
|
93
|
-
// Same guard as the email branch below, same reasoning: transactional, so
|
|
94
|
-
// the kill switch does not apply (the buyer cannot check out without the
|
|
95
|
-
// code), but the daily ceiling does.
|
|
96
|
-
const waGuard = {
|
|
97
|
-
channel: "whatsapp",
|
|
98
|
-
feature: "checkout_value_otp",
|
|
99
|
-
audience: "user",
|
|
100
|
-
transactional: true,
|
|
101
|
-
};
|
|
102
|
-
const waDecision = await guardSend(waGuard);
|
|
103
|
-
if (!waDecision.allow) {
|
|
104
|
-
logSuppressedSend(waGuard, waDecision);
|
|
105
|
-
throw new ValidationError("We couldn't send the verification code via WhatsApp — please try again, or send it by email instead.");
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const sent = await withChannelRetry("whatsapp", async () => {
|
|
109
|
-
const ok = await sendWhatsAppBusinessMessage({ toPhone: phone, message, phoneNumberId, accessToken });
|
|
110
|
-
if (!ok)
|
|
111
|
-
throw new Error("WhatsApp Cloud API returned a non-OK response");
|
|
112
|
-
return ok;
|
|
113
|
-
});
|
|
114
|
-
if (!sent)
|
|
115
|
-
throw new Error("WhatsApp send failed");
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
void normalizeError(err);
|
|
119
|
-
serverLogger.error("Failed to send checkout value OTP via WhatsApp", { uid: userId, err });
|
|
120
|
-
throw new ValidationError("We couldn't send the verification code via WhatsApp — please try again, or send it by email instead.");
|
|
121
|
-
}
|
|
122
|
-
const maskedPhone = phone.length > 4 ? `${"*".repeat(phone.length - 4)}${phone.slice(-4)}` : "***";
|
|
123
|
-
serverLogger.info(`Checkout value OTP sent via WhatsApp: uid=${userId}`);
|
|
124
|
-
return { maskedPhone };
|
|
125
|
-
}
|
|
126
40
|
if (!(await isChannelHealthy("email"))) {
|
|
127
41
|
serverLogger.warn("sendCheckoutValueOtp: email channel unhealthy — skipping OTP gate", { uid: userId, channel });
|
|
128
42
|
return { skipped: true };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link.js";
|
|
3
|
+
import { OwnerQuickActions } from "../../../features/admin/components/OwnerQuickActions.js";
|
|
3
4
|
import { getClassifiedForDetail } from "../../../_internal/server/features/classified/data.js";
|
|
4
5
|
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 dark:hover:text-primary-400 transition-colors";
|
|
5
6
|
const CLS_CLASSIFIED_BADGE = "inline-block rounded-full bg-cyan-100 dark:bg-cyan-900/30 px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-cyan-700 dark:text-cyan-300";
|
|
@@ -23,6 +24,7 @@ import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal.j
|
|
|
23
24
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
24
25
|
import { DEFAULT_MIN_OFFER_PERCENT, offersEnabledFor, resolveOfferBounds, } from "../../../_internal/shared/features/offers/config.js";
|
|
25
26
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
27
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
26
28
|
function toDescriptionHtml(raw) {
|
|
27
29
|
if (!raw)
|
|
28
30
|
return "";
|
|
@@ -43,7 +45,15 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
|
|
|
43
45
|
const currency = p.currency || getDefaultCurrency();
|
|
44
46
|
const title = String(p.title ?? p.name ?? "Classified Listing");
|
|
45
47
|
const price = typeof p.price === "number" ? p.price : null;
|
|
46
|
-
|
|
48
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
49
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
50
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
51
|
+
const galleryUrls = productImageUrls(p.images);
|
|
52
|
+
const images = galleryUrls.length > 0
|
|
53
|
+
? galleryUrls
|
|
54
|
+
: typeof p.mainImage === "string"
|
|
55
|
+
? [p.mainImage]
|
|
56
|
+
: [];
|
|
47
57
|
const meta = product.classified;
|
|
48
58
|
const location = meta?.meetupArea ? [meta.meetupArea.locality, meta.meetupArea.city].filter(Boolean).join(", ") : null;
|
|
49
59
|
const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? String(p.categorySlugs[0]) : (typeof p.category === "string" ? p.category : null);
|
|
@@ -93,7 +103,7 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
|
|
|
93
103
|
price: price ?? undefined,
|
|
94
104
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
95
105
|
storeName: storeName ?? undefined,
|
|
96
|
-
} }), _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.CLASSIFIED), className: CLS_BREADCRUMB_LINK, children: "Classifieds" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_CLASSIFIED_BADGE, children: "Classified" }), meta?.negotiable && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Negotiable" })), meta?.acceptsShipping && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Shipping available" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), location && (_jsxs(Row, { color: "muted", textSize: "sm", align: "center", gap: "xs", children: [_jsx(Span, { children: "\uD83D\uDCCD" }), _jsx(Span, { children: location }), meta?.meetupArea?.pincode && _jsxs(Span, { children: ["\u00B7 ", meta.meetupArea.pincode] })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) })), renderBuyBar: () => (_jsx(Stack, { id: "classified-contact-bar", className: "p-[var(--appkit-space-5)]", border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: offersAvailable && offerBounds ? (_jsxs(_Fragment, { children: [renderOfferAction({
|
|
106
|
+
} }), _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.CLASSIFIED), className: CLS_BREADCRUMB_LINK, children: "Classifieds" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_CLASSIFIED_BADGE, children: "Classified" }), meta?.negotiable && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Negotiable" })), meta?.acceptsShipping && (_jsx(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: "Shipping available" }))] }), _jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(p.title ?? p.slug ?? p.id), storeId: p.storeId ? String(p.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(p.id))) }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), location && (_jsxs(Row, { color: "muted", textSize: "sm", align: "center", gap: "xs", children: [_jsx(Span, { children: "\uD83D\uDCCD" }), _jsx(Span, { children: location }), meta?.meetupArea?.pincode && _jsxs(Span, { children: ["\u00B7 ", meta.meetupArea.pincode] })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) })), renderBuyBar: () => (_jsx(Stack, { id: "classified-contact-bar", className: "p-[var(--appkit-space-5)]", border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: offersAvailable && offerBounds ? (_jsxs(_Fragment, { children: [renderOfferAction({
|
|
97
107
|
productId: String(product.id),
|
|
98
108
|
price: price,
|
|
99
109
|
currency,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link.js";
|
|
3
|
+
import { OwnerQuickActions } from "../../../features/admin/components/OwnerQuickActions.js";
|
|
3
4
|
import { getDigitalCodeForDetail } from "../../../_internal/server/features/digital-code/data.js";
|
|
4
5
|
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 dark:hover:text-primary-400 transition-colors";
|
|
5
6
|
const CLS_DIGITALCODE_BADGE = "inline-block rounded-full bg-violet-100 dark:bg-violet-900/30 px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-violet-700 dark:text-violet-300";
|
|
@@ -21,6 +22,7 @@ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListing
|
|
|
21
22
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data.js";
|
|
22
23
|
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel.js";
|
|
23
24
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
25
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
24
26
|
function toDescriptionHtml(raw) {
|
|
25
27
|
if (!raw)
|
|
26
28
|
return "";
|
|
@@ -41,7 +43,15 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
41
43
|
const currency = p.currency || getDefaultCurrency();
|
|
42
44
|
const title = String(p.title ?? p.name ?? "Digital Code");
|
|
43
45
|
const price = typeof p.price === "number" ? p.price : null;
|
|
44
|
-
|
|
46
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
47
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
48
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
49
|
+
const galleryUrls = productImageUrls(p.images);
|
|
50
|
+
const images = galleryUrls.length > 0
|
|
51
|
+
? galleryUrls
|
|
52
|
+
: typeof p.mainImage === "string"
|
|
53
|
+
? [p.mainImage]
|
|
54
|
+
: [];
|
|
45
55
|
const meta = product.digitalCode;
|
|
46
56
|
const codesLeft = meta?.codesAvailable ?? 0;
|
|
47
57
|
const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? String(p.categorySlugs[0]) : (typeof p.category === "string" ? p.category : null);
|
|
@@ -64,7 +74,7 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
64
74
|
price: price ?? undefined,
|
|
65
75
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
66
76
|
storeName: storeName ?? undefined,
|
|
67
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) })), renderBuyBar: () => (_jsx(Div, { id: "digital-code-buy-bar", children: renderPrimaryActions?.({
|
|
77
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(p.title ?? p.slug ?? p.id), storeId: p.storeId ? String(p.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(p.id))) }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) })), renderBuyBar: () => (_jsx(Div, { id: "digital-code-buy-bar", children: renderPrimaryActions?.({
|
|
68
78
|
productId: String(product.id),
|
|
69
79
|
productSlug: String(p.slug ?? product.id),
|
|
70
80
|
productTitle: title,
|
|
@@ -12,6 +12,16 @@ interface EventCardProps {
|
|
|
12
12
|
selectable?: boolean;
|
|
13
13
|
isSelected?: boolean;
|
|
14
14
|
onSelect?: (id: string, selected: boolean) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Owner-only pencil / clone / bin, for an admin or whoever owns this record.
|
|
17
|
+
* Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
|
|
18
|
+
*
|
|
19
|
+
* 🛑 Rendered OUTSIDE this card's own `<Link>`/`<TextLink>`, as a sibling.
|
|
20
|
+
* A `<button>` inside an `<a>` is invalid HTML that browsers resolve by
|
|
21
|
+
* closing the anchor early, which breaks the CARD's navigation rather than
|
|
22
|
+
* just the button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
23
|
+
*/
|
|
24
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
15
25
|
}
|
|
16
|
-
export declare function EventCard({ event, labels, onParticipate, className, selectable, isSelected, onSelect, }: EventCardProps): import("react").JSX.Element;
|
|
26
|
+
export declare function EventCard({ event, labels, onParticipate, className, selectable, isSelected, onSelect, renderOwnerActions, }: EventCardProps): import("react").JSX.Element;
|
|
17
27
|
export {};
|
|
@@ -24,7 +24,7 @@ const TYPE_ICONS = {
|
|
|
24
24
|
spin_wheel: "🎡",
|
|
25
25
|
lottery: "🎰",
|
|
26
26
|
};
|
|
27
|
-
export function EventCard({ event, labels = {}, onParticipate, className = "", selectable = false, isSelected = false, onSelect, }) {
|
|
27
|
+
export function EventCard({ event, labels = {}, onParticipate, className = "", selectable = false, isSelected = false, onSelect, renderOwnerActions, }) {
|
|
28
28
|
const longPress = useLongPress(() => onSelect?.(event.id, !isSelected));
|
|
29
29
|
const safeTitle = event.title?.trim() || "Untitled event";
|
|
30
30
|
const now = new Date();
|
|
@@ -32,6 +32,13 @@ export function EventCard({ event, labels = {}, onParticipate, className = "", s
|
|
|
32
32
|
const msLeft = endsAt.getTime() - now.getTime();
|
|
33
33
|
const daysLeft = Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24)));
|
|
34
34
|
const detailHref = String(ROUTES.PUBLIC.EVENT_DETAIL(event.slug ?? event.id));
|
|
35
|
-
|
|
35
|
+
const card = (_jsxs(Stack, { as: "article", border: "default", rounded: "xl", shadow: "hover-md", gap: "none", className: `group relative h-full overflow-hidden bg-[var(--appkit-color-surface)] ${isSelected ? "border-primary outline outline-2 outline-primary" : " "} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(event.id, !isSelected); }, label: isSelected ? "Deselect event" : "Select event", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsx(Link, { href: detailHref, className: "block flex-shrink-0", children: event.coverImageUrl || event.coverImage?.url ? (_jsx(Div, { className: `relative aspect-video ${__O.hidden}`, children: _jsx(MediaImage, { src: event.coverImageUrl || event.coverImage?.url || "", alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Row, { surface: "muted", className: "aspect-video", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-40", size: "5xl", "aria-hidden": "true", children: TYPE_ICONS[event.type] }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { className: "mb-2", align: "start", justify: "between", gap: "sm", children: [_jsx(Span, { size: "lg", "aria-hidden": "true", children: TYPE_ICONS[event.type] }), _jsx(EventStatusBadge, { status: event.status })] }), _jsx(Link, { href: detailHref, className: "block", children: _jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)] leading-snug mb-1 group-hover:text-primary transition-colors", size: "base", weight: "semibold", children: safeTitle }) }), _jsx(RichText, { html: normalizeRichTextHtml(event.description ?? ""), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mb-3 line-clamp-3 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]" }), _jsxs(Row, { color: "muted", textSize: "xs", className: "mb-3 mt-auto", align: "center", justify: "between", children: [event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE &&
|
|
36
36
|
daysLeft > 0 && _jsxs(Span, { children: ["\u23F1 ", daysLeft, "d remaining"] }), _jsxs(Span, { children: ["\uD83D\uDC65 ", event.stats.totalEntries, " ", labels.entries ?? "entries"] })] }), event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE && onParticipate ? (_jsx(Button, { rounded: "lg", type: "button", onClick: () => onParticipate(event), className: "w-full bg-primary py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-white transition-colors hover:bg-primary-600", children: labels.participate ?? "Participate" })) : (_jsxs(TextLink, { rounded: "lg", paddingX: "sm", paddingY: "xs", href: detailHref, layout: "inline-flex", align: "center", justify: "center", gap: "xs", className: "w-full border border-[var(--appkit-color-border)] transition-colors hover:bg-[var(--appkit-color-bg)]", color: "primary", size: "sm", weight: "medium", children: [labels.viewDetails ?? "View details", " \u2192"] }))] })] }));
|
|
37
|
+
if (!renderOwnerActions)
|
|
38
|
+
return card;
|
|
39
|
+
/*
|
|
40
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
41
|
+
* beats this card's own overlays, which all sit at `z-10`.
|
|
42
|
+
*/
|
|
43
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
37
44
|
}
|
|
@@ -4,6 +4,8 @@ import { Container, Div, Heading, Section, Span, Stack, Text, } from "../../../u
|
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
5
5
|
import { GroupMemberPicker } from "../../products/components/GroupMemberPicker.js";
|
|
6
6
|
import { readGroupRules } from "../../../_internal/shared/features/products/group-rules.js";
|
|
7
|
+
import { OwnerQuickActions } from "../../../features/admin/components/OwnerQuickActions.js";
|
|
8
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
9
|
const __O = { hidden: "overflow-hidden" };
|
|
8
10
|
/** Human label per theme — mirrors GROUP_THEME_TITLE on the carousel. */
|
|
9
11
|
const THEME_LABEL = {
|
|
@@ -14,7 +16,7 @@ const THEME_LABEL = {
|
|
|
14
16
|
generic: "Grouped listing",
|
|
15
17
|
};
|
|
16
18
|
export function GroupedListingDetailView({ group, members }) {
|
|
17
|
-
return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(Heading, { level: 1, children: group.title }), group.description && (_jsx(Text, { color: "muted", children: group.description })), _jsxs(Text, { size: "sm", color: "muted", children: [members.length, " item", members.length === 1 ? "" : "s", " in this group"] })] }), members.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "lg", children: "Pick what you want" }), _jsx(Text, { size: "sm", color: "muted", children: "Choose a quantity for each item \u2014 they'll go into your cart as a single line you can edit later." }), _jsx(GroupMemberPicker, { groupId: group.id, groupSource: "grouped-listing", members: members,
|
|
19
|
+
return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(OwnerQuickActions, { resource: "GROUP", placement: "detail", recordLabel: String(group.title), storeId: group.storeId ? String(group.storeId) : undefined, editHref: String(ROUTES.ADMIN.GROUPED_LISTINGS_EDIT(String(group.id))) }), _jsx(Heading, { level: 1, children: group.title }), group.description && (_jsx(Text, { color: "muted", children: group.description })), _jsxs(Text, { size: "sm", color: "muted", children: [members.length, " item", members.length === 1 ? "" : "s", " in this group"] })] }), members.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "lg", children: "Pick what you want" }), _jsx(Text, { size: "sm", color: "muted", children: "Choose a quantity for each item \u2014 they'll go into your cart as a single line you can edit later." }), _jsx(GroupMemberPicker, { groupId: group.id, groupSource: "grouped-listing", members: members,
|
|
18
20
|
/*
|
|
19
21
|
* Off the group document this view already loaded. The picker
|
|
20
22
|
* and `addGroupLineToCart` run the SAME `checkGroupRules`, so
|