@mohasinac/appkit 4.53.2 → 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/server/features/auth/capabilities.js +7 -5
- 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/products/listing-tab-counts.js +81 -25
- 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/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/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 +21 -15
- package/dist/constants/api-endpoints.js +7 -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 +4 -3
- 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/components/BrandDetailPageView.js +2 -1
- package/dist/features/categories/components/BundleDetailView.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- 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 +2 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -1
- 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 +2 -1
- 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 +4 -3
- 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.js +1 -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 +27 -2
- 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/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/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/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/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 +415 -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/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 +8 -10
- package/dist/server.js +16 -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/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
|
@@ -17,5 +17,15 @@ export interface MarketplaceBundleCardProps {
|
|
|
17
17
|
hrefBuilder?: (bundle: MarketplaceBundleCardData) => string;
|
|
18
18
|
onNavigate?: (href: string) => void;
|
|
19
19
|
labels?: MarketplaceBundleCardLabels;
|
|
20
|
+
/**
|
|
21
|
+
* Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
|
|
22
|
+
* nothing for a visitor who does not own the record.
|
|
23
|
+
*
|
|
24
|
+
* 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
|
|
25
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
26
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
27
|
+
* button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
28
|
+
*/
|
|
29
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
20
30
|
}
|
|
21
|
-
export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplaceBundleCardProps): import("react").JSX.Element;
|
|
31
|
+
export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplaceBundleCardProps): import("react").JSX.Element;
|
|
@@ -27,7 +27,7 @@ function resolveHref(bundle, href, hrefBuilder) {
|
|
|
27
27
|
return hrefBuilder(bundle);
|
|
28
28
|
return ROUTES.PUBLIC.BUNDLE_DETAIL(bundle.slug);
|
|
29
29
|
}
|
|
30
|
-
export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
|
|
30
|
+
export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
|
|
31
31
|
const router = useRouter();
|
|
32
32
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
33
33
|
const detailHref = resolveHref(bundle, href, hrefBuilder);
|
|
@@ -49,7 +49,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
49
49
|
}
|
|
50
50
|
router.push(String(detailHref));
|
|
51
51
|
}, [detailHref, onNavigate, router]);
|
|
52
|
-
|
|
52
|
+
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
53
53
|
event.stopPropagation();
|
|
54
54
|
onSelect(bundle.id, !isSelected);
|
|
55
55
|
}, className: selectable || isSelected
|
|
@@ -58,4 +58,11 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
58
58
|
? formatCurrency(price, getDefaultCurrency())
|
|
59
59
|
: "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }) }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
|
|
60
60
|
handleNavigate(); }, children: "View Bundle" })] })] }));
|
|
61
|
+
if (!renderOwnerActions)
|
|
62
|
+
return card;
|
|
63
|
+
/*
|
|
64
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
65
|
+
* clears this card's own overlays, which all sit at `z-10`.
|
|
66
|
+
*/
|
|
67
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
61
68
|
}
|
|
@@ -20,5 +20,15 @@ export interface MarketplacePrizeDrawCardProps {
|
|
|
20
20
|
hrefBuilder?: (product: MarketplacePrizeDrawCardData) => string;
|
|
21
21
|
onNavigate?: (href: string) => void;
|
|
22
22
|
labels?: MarketplacePrizeDrawCardLabels;
|
|
23
|
+
/**
|
|
24
|
+
* Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
|
|
25
|
+
* nothing for a visitor who does not own the record.
|
|
26
|
+
*
|
|
27
|
+
* 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
|
|
28
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
29
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
30
|
+
* button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
31
|
+
*/
|
|
32
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
23
33
|
}
|
|
24
|
-
export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
|
|
34
|
+
export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
|
|
@@ -65,7 +65,7 @@ function formatCountdown(target) {
|
|
|
65
65
|
const minutes = Math.floor((diff % 3600000) / 60000);
|
|
66
66
|
return `${hours}h ${minutes}m`;
|
|
67
67
|
}
|
|
68
|
-
export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
|
|
68
|
+
export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
|
|
69
69
|
const router = useRouter();
|
|
70
70
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
71
71
|
const detailHref = resolveHref(product, href, hrefBuilder);
|
|
@@ -90,7 +90,7 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
90
90
|
}
|
|
91
91
|
router.push(String(detailHref));
|
|
92
92
|
}, [detailHref, onNavigate, router]);
|
|
93
|
-
|
|
93
|
+
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
|
|
94
94
|
const it = thumbItems[i];
|
|
95
95
|
const img = it?.images?.[0];
|
|
96
96
|
return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (_jsx(MediaImage, { src: img, alt: it?.title ?? `Prize ${i + 1}`, size: "card" })) : null }, `thumb-${i}`));
|
|
@@ -100,4 +100,11 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
100
100
|
}, className: selectable || isSelected
|
|
101
101
|
? "opacity-100"
|
|
102
102
|
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }) }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
103
|
+
if (!renderOwnerActions)
|
|
104
|
+
return card;
|
|
105
|
+
/*
|
|
106
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
107
|
+
* clears this card's own overlays, which all sit at `z-10`.
|
|
108
|
+
*/
|
|
109
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
103
110
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link.js";
|
|
3
|
+
import { OwnerQuickActions } from "../../../features/admin/components/OwnerQuickActions.js";
|
|
3
4
|
import { orderRepository, productRepository } from "../../../repositories/index.js";
|
|
4
5
|
const __P = {
|
|
5
6
|
p3: "p-[var(--appkit-space-3)]",
|
|
@@ -131,5 +132,5 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
|
|
|
131
132
|
price: pricePerEntry,
|
|
132
133
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
133
134
|
storeName: storeName ?? undefined,
|
|
134
|
-
} }), _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", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Prize Draws" }), _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(PrizeDrawCollage, { items: items, hideWonState: true })), 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_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "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 })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
|
|
135
|
+
} }), _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", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Prize Draws" }), _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(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(product.title ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))) }), _jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "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 })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
|
|
135
136
|
}
|
|
@@ -31,6 +31,9 @@ import { ctaAnchorProps } from "../../layout/cta-anchor.js";
|
|
|
31
31
|
import { ContextualTrail } from "../../categories/components/ContextualTrail.js";
|
|
32
32
|
import { productCrumbs, withTrailHead } from "../../categories/utils/breadcrumb-chain.js";
|
|
33
33
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
34
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
35
|
+
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
36
|
+
import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
|
|
34
37
|
import { Button, Container, Div, Heading, Main, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, SpecTable, Icon, } from "../../../ui/index.js";
|
|
35
38
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
36
39
|
import { safeDisplayName } from "../../../security/index.js";
|
|
@@ -350,7 +353,29 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
350
353
|
price: product.price,
|
|
351
354
|
storeId: product.storeId,
|
|
352
355
|
storeName: product.storeName,
|
|
353
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [
|
|
356
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: title || String(product.slug ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))), onClone: async () => {
|
|
357
|
+
const res = await fetch(SELLER_ENDPOINTS.PRODUCT_DUPLICATE(String(product.id)), { method: "POST" });
|
|
358
|
+
if (!res.ok)
|
|
359
|
+
return;
|
|
360
|
+
const json = (await res.json());
|
|
361
|
+
const newId = json?.data?.id;
|
|
362
|
+
/*
|
|
363
|
+
* Land on the COPY's editor. The endpoint returns the new
|
|
364
|
+
* draft's id precisely so a one-click clone ends somewhere
|
|
365
|
+
* useful rather than back on the public page, where the
|
|
366
|
+
* draft is invisible.
|
|
367
|
+
*/
|
|
368
|
+
if (newId && typeof window !== "undefined") {
|
|
369
|
+
window.location.assign(String(ROUTES.STORE.PRODUCTS_EDIT(newId)));
|
|
370
|
+
}
|
|
371
|
+
}, onDelete: async () => {
|
|
372
|
+
await fetch(SELLER_ENDPOINTS.PRODUCT_BY_ID(String(product.id)), {
|
|
373
|
+
method: "DELETE",
|
|
374
|
+
});
|
|
375
|
+
if (typeof window !== "undefined") {
|
|
376
|
+
window.location.assign(String(ROUTES.PUBLIC.PRODUCTS));
|
|
377
|
+
}
|
|
378
|
+
} }), _jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
|
|
354
379
|
? "Brand New"
|
|
355
380
|
: condition === "like_new"
|
|
356
381
|
? "Like New"
|
|
@@ -421,7 +446,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
421
446
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
422
447
|
: undefined, renderGroupSection: groupId
|
|
423
448
|
? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
|
|
424
|
-
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, 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: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
|
|
449
|
+
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, 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: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsxs(Stack, { gap: "md", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _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) })), renderBundleSection: bundleMemberships.length > 0
|
|
425
450
|
? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
|
|
426
451
|
: undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryNames[0] || categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
|
|
427
452
|
? renderPrimaryActions({
|
|
@@ -16,8 +16,21 @@ interface ProductCardProps<T extends ProductItem = ProductItem> {
|
|
|
16
16
|
selectionMode?: boolean;
|
|
17
17
|
isSelected?: boolean;
|
|
18
18
|
onSelect?: (id: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Owner-only pencil / clone / bin, for an admin or the seller who owns this
|
|
21
|
+
* listing. Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 Rendered OUTSIDE this card's `<Link>`, at `top-14 right-2` — below the
|
|
24
|
+
* wishlist heart and clear of the checkbox, badge stack and IconOverlay. See
|
|
25
|
+
* the return statement for why it cannot live inside the card body.
|
|
26
|
+
*
|
|
27
|
+
* Suppressed while a selection is active: a bulk-select gesture and a
|
|
28
|
+
* per-record edit are different intents, and offering both at once invites
|
|
29
|
+
* the wrong one.
|
|
30
|
+
*/
|
|
31
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
19
32
|
}
|
|
20
|
-
export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, }: ProductCardProps<T>): React.JSX.Element;
|
|
33
|
+
export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, renderOwnerActions, }: ProductCardProps<T>): React.JSX.Element;
|
|
21
34
|
export interface ProductCardContext<T extends ProductItem = ProductItem> {
|
|
22
35
|
onClick?: (product: T) => void;
|
|
23
36
|
onWishlistToggle?: (productId: string) => void;
|
|
@@ -45,7 +45,7 @@ const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
|
|
|
45
45
|
const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
|
|
46
46
|
const CLS_STAR_BARE = "text-warning";
|
|
47
47
|
const CLS_BID_TEXT = "text-[11px] text-error";
|
|
48
|
-
export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, }) {
|
|
48
|
+
export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, renderOwnerActions, }) {
|
|
49
49
|
const discount = product.originalPrice && product.originalPrice > product.price
|
|
50
50
|
? Math.round(((product.originalPrice - product.price) / product.originalPrice) *
|
|
51
51
|
100)
|
|
@@ -124,10 +124,27 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
124
124
|
e.preventDefault();
|
|
125
125
|
onAddToCart(product);
|
|
126
126
|
}, className: "justify-center text-[length:var(--appkit-text-xs)]", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
const wrapped = href && !selectionMode ? (_jsx(Link, { href: href, className: "block h-full", children: cardBody })) : (cardBody);
|
|
128
|
+
if (!renderOwnerActions || selectionMode || isSelected)
|
|
129
|
+
return wrapped;
|
|
130
|
+
/*
|
|
131
|
+
* 🛑 OUTSIDE the anchor, as a sibling — never inside `cardBody`.
|
|
132
|
+
*
|
|
133
|
+
* This card wraps its entire body in a `<Link>` whenever `href` is set, and
|
|
134
|
+
* `InteractiveProductCard` wraps it in another one. A `<button>` inside an
|
|
135
|
+
* `<a>` is invalid HTML that browsers resolve by closing the anchor early,
|
|
136
|
+
* which breaks the CARD's navigation rather than just the button's —
|
|
137
|
+
* stopPropagation stops the click but cannot fix the parse.
|
|
138
|
+
*
|
|
139
|
+
* Positioned `top-14 right-2`: directly below the 44px wishlist heart at
|
|
140
|
+
* `top-2 right-2`, so it clears every occupied corner (checkbox and badges
|
|
141
|
+
* top-left, IconOverlay bottom-right) without needing to know the hero's
|
|
142
|
+
* height. The hero is `aspect-square`, so it is always taller than
|
|
143
|
+
* 3.5rem + 44px and the toolbar lands on the photo.
|
|
144
|
+
*
|
|
145
|
+
* `z-20` beats the in-card overlays, which all sit at `z-10`.
|
|
146
|
+
*/
|
|
147
|
+
return (_jsxs(Div, { className: "relative h-full", children: [wrapped, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
|
|
131
148
|
}
|
|
132
149
|
function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
133
150
|
const discount = product.originalPrice && product.originalPrice > product.price
|
|
@@ -221,9 +221,9 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
221
221
|
itemCount: items.reduce((sum, it) => sum + it.quantity, 0),
|
|
222
222
|
totalValue: items.reduce((sum, it) => sum + (it.price ?? 0) * it.quantity, 0),
|
|
223
223
|
};
|
|
224
|
-
showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency())), "success");
|
|
224
|
+
showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency()), canSeePrices), "success");
|
|
225
225
|
dispatchCartUpdated(detail);
|
|
226
|
-
}, [localCart, showToast]);
|
|
226
|
+
}, [localCart, showToast, canSeePrices]);
|
|
227
227
|
const handleBuyNow = useCallback((product) => {
|
|
228
228
|
requireAuth(ACTION_ID.BUY_NOW, () => {
|
|
229
229
|
const snapshot = {
|
|
@@ -11,8 +11,21 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { mediaUrlSchema } from "../../../../validation/schemas.js";
|
|
14
|
-
import { getProviders } from "../../../../contracts/index.js";
|
|
15
14
|
import { createRouteHandler } from "../../../../next/api/routeHandler.js";
|
|
15
|
+
/*
|
|
16
|
+
* 🛑 The real repository, not `db.getRepository<Review>("reviews")`.
|
|
17
|
+
*
|
|
18
|
+
* The generic FirebaseRepository knows nothing about this collection, so every
|
|
19
|
+
* read and write through it skipped ReviewRepository's overrides:
|
|
20
|
+
*
|
|
21
|
+
* - `update` encrypts `userName` (PII) and rebuilds `searchTxt`. Writing
|
|
22
|
+
* through the generic repo left the tokens describing the OLD text, so an
|
|
23
|
+
* edited review was findable only by what it used to say.
|
|
24
|
+
* - `mapDoc` decrypts `userName` and strips `helpfulVoterIds`. Reading
|
|
25
|
+
* through the generic repo returned the name as raw `enc:v1:…` ciphertext
|
|
26
|
+
* and published the voter list.
|
|
27
|
+
*/
|
|
28
|
+
import { reviewRepository } from "../../repository/reviews.repository.js";
|
|
16
29
|
import { normalizeError } from "../../../../errors/normalize.js";
|
|
17
30
|
import { isAdminUser, isModeratorUser } from "../../../auth/role-predicates.js";
|
|
18
31
|
const ERR_REVIEW_NOT_FOUND = "Review not found";
|
|
@@ -20,12 +33,14 @@ const ERR_REVIEW_NOT_FOUND = "Review not found";
|
|
|
20
33
|
export async function GET(_request, context) {
|
|
21
34
|
try {
|
|
22
35
|
const { id } = await context.params;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
/*
|
|
37
|
+
* Through `reviewRepository`, so `mapDoc` runs: `userName` is decrypted
|
|
38
|
+
* and `helpfulVoterIds` is stripped. The generic repo did neither, so this
|
|
39
|
+
* public endpoint returned the reviewer's name as raw `enc:v1:…`
|
|
40
|
+
* ciphertext and published the list of everyone who had voted the review
|
|
41
|
+
* helpful.
|
|
42
|
+
*/
|
|
43
|
+
const review = await reviewRepository.findById(id);
|
|
29
44
|
if (!review) {
|
|
30
45
|
return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
|
|
31
46
|
}
|
|
@@ -39,6 +54,19 @@ export async function GET(_request, context) {
|
|
|
39
54
|
}
|
|
40
55
|
export { GET as reviewItemGET };
|
|
41
56
|
// ---- PATCH /api/reviews/[id] -------------------------------------------------
|
|
57
|
+
/*
|
|
58
|
+
* 🛑 `.strict()`, not `.passthrough()`.
|
|
59
|
+
*
|
|
60
|
+
* It was passthrough, so any unlisted key reached the write — including
|
|
61
|
+
* `userId`, `verified`, `helpfulCount` and `featured`. A reviewer could have
|
|
62
|
+
* marked their own review a verified purchase, or featured it on the homepage,
|
|
63
|
+
* by adding one field to the request body. Nothing in the handler below looked
|
|
64
|
+
* at the extra keys; they went straight into Firestore.
|
|
65
|
+
*
|
|
66
|
+
* `.strict()` rejects them with a 400 naming the field rather than silently
|
|
67
|
+
* dropping them, which is the honest answer to a caller sending something this
|
|
68
|
+
* endpoint does not own.
|
|
69
|
+
*/
|
|
42
70
|
const reviewUpdateSchema = z
|
|
43
71
|
.object({
|
|
44
72
|
rating: z
|
|
@@ -53,10 +81,28 @@ const reviewUpdateSchema = z
|
|
|
53
81
|
title: z.string().min(1).max(100).optional(),
|
|
54
82
|
comment: z.string().min(1).max(2000).optional(),
|
|
55
83
|
images: z.array(mediaUrlSchema).max(10).optional(),
|
|
56
|
-
video: mediaUrlSchema.optional(),
|
|
57
84
|
status: z.enum(["pending", "approved", "rejected"]).optional(),
|
|
58
85
|
})
|
|
59
|
-
.
|
|
86
|
+
.strict();
|
|
87
|
+
/*
|
|
88
|
+
* 🛑 `video` is deliberately NOT accepted here, and that is a fix rather than
|
|
89
|
+
* an omission.
|
|
90
|
+
*
|
|
91
|
+
* It used to be declared as a bare `mediaUrlSchema` — a URL string — while
|
|
92
|
+
* `ReviewDocument.video` is a `ReviewVideoField` object. So a caller sending
|
|
93
|
+
* the real shape failed validation, and a caller sending a string wrote a
|
|
94
|
+
* string into a field every renderer reads as an object, leaving `video.url`
|
|
95
|
+
* undefined and the player with nothing.
|
|
96
|
+
*
|
|
97
|
+
* Accepting the object instead would still be wrong on this route: the URL
|
|
98
|
+
* would arrive as an unfinalised `tmp/` reference, because nothing here calls
|
|
99
|
+
* `finalizeStagedMediaUrl`. `updateReview` (features/reviews/actions) is the
|
|
100
|
+
* path that owns review media — it finalises the upload and builds the full
|
|
101
|
+
* field — and `REVIEW_UPDATABLE_FIELDS` already documents the updatable set as
|
|
102
|
+
* rating / title / comment / images, with no video in it.
|
|
103
|
+
*
|
|
104
|
+
* One owner for media writes, and this is not it.
|
|
105
|
+
*/
|
|
60
106
|
export const reviewItemPATCH = createRouteHandler({
|
|
61
107
|
auth: true,
|
|
62
108
|
schema: reviewUpdateSchema,
|
|
@@ -64,11 +110,7 @@ export const reviewItemPATCH = createRouteHandler({
|
|
|
64
110
|
const url = new URL(request.url);
|
|
65
111
|
const segments = url.pathname.split("/");
|
|
66
112
|
const id = segments[segments.length - 1];
|
|
67
|
-
const
|
|
68
|
-
if (!db)
|
|
69
|
-
return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
|
|
70
|
-
const repo = db.getRepository("reviews");
|
|
71
|
-
const review = await repo.findById(id);
|
|
113
|
+
const review = await reviewRepository.findById(id);
|
|
72
114
|
if (!review)
|
|
73
115
|
return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
|
|
74
116
|
const isOwner = review.userId === user.uid;
|
|
@@ -77,13 +119,34 @@ export const reviewItemPATCH = createRouteHandler({
|
|
|
77
119
|
if (!isOwner && !isModerator && !isAdmin) {
|
|
78
120
|
return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
|
|
79
121
|
}
|
|
80
|
-
|
|
122
|
+
/*
|
|
123
|
+
* Owners edit content, not moderation state. Pinning `status` rather than
|
|
124
|
+
* rejecting the field is deliberate and pre-existing: an approved review
|
|
125
|
+
* edited by its author stays approved and is NOT sent back for
|
|
126
|
+
* re-moderation.
|
|
127
|
+
*/
|
|
81
128
|
const patch = isOwner && !isModerator && !isAdmin
|
|
82
129
|
? { ...body, status: review.status }
|
|
83
130
|
: body;
|
|
84
|
-
|
|
131
|
+
/*
|
|
132
|
+
* 🛑 `hasImages` is DERIVED and has to move with `images`, or the two
|
|
133
|
+
* disagree. It is a real queryable field (`hasImages` is `canFilter` in
|
|
134
|
+
* SIEVE_FIELDS and backs the "with photos" review filter), so a review
|
|
135
|
+
* whose last photo was removed stayed in that filter for ever, and one
|
|
136
|
+
* that gained its first was absent from it.
|
|
137
|
+
*/
|
|
138
|
+
const nextImages = body?.images;
|
|
139
|
+
const derived = nextImages !== undefined ? { hasImages: nextImages.length > 0 } : {};
|
|
140
|
+
/*
|
|
141
|
+
* 🛑 A `Date`, not an ISO string. Everything else in this codebase writes
|
|
142
|
+
* `Date` to `updatedAt`, and `reviewRepository`'s own Sieve config marks it
|
|
143
|
+
* sortable — so a string here sorts lexicographically beside real
|
|
144
|
+
* timestamps and silently orders wrong.
|
|
145
|
+
*/
|
|
146
|
+
const updated = await reviewRepository.update(id, {
|
|
85
147
|
...patch,
|
|
86
|
-
|
|
148
|
+
...derived,
|
|
149
|
+
updatedAt: new Date(),
|
|
87
150
|
});
|
|
88
151
|
return NextResponse.json({ success: true, data: updated });
|
|
89
152
|
},
|
|
@@ -95,11 +158,8 @@ export const reviewItemDELETE = createRouteHandler({
|
|
|
95
158
|
const url = new URL(request.url);
|
|
96
159
|
const segments = url.pathname.split("/");
|
|
97
160
|
const id = segments[segments.length - 1];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
|
|
101
|
-
const repo = db.getRepository("reviews");
|
|
102
|
-
const review = await repo.findById(id);
|
|
161
|
+
// Same repository as GET and PATCH — one collection, one repository.
|
|
162
|
+
const review = await reviewRepository.findById(id);
|
|
103
163
|
if (!review)
|
|
104
164
|
return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
|
|
105
165
|
const isOwner = review.userId === user.uid;
|
|
@@ -108,7 +168,7 @@ export const reviewItemDELETE = createRouteHandler({
|
|
|
108
168
|
if (!isOwner && !isModerator && !isAdmin) {
|
|
109
169
|
return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
|
|
110
170
|
}
|
|
111
|
-
await
|
|
171
|
+
await reviewRepository.delete(id);
|
|
112
172
|
return NextResponse.json({ success: true });
|
|
113
173
|
},
|
|
114
174
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The strip and the review drawer, connected.
|
|
5
|
+
*
|
|
6
|
+
* Exists because the two halves need shared client state (which review is
|
|
7
|
+
* open) while the product detail page that mounts them is an async Server
|
|
8
|
+
* Component. One small client island is cheaper than making the detail view
|
|
9
|
+
* take two render-prop slots and thread state between them.
|
|
10
|
+
*
|
|
11
|
+
* @tag domain:reviews
|
|
12
|
+
* @tag layer:components
|
|
13
|
+
* @tag access:client
|
|
14
|
+
*/
|
|
15
|
+
import { useCallback, useState } from "react";
|
|
16
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
17
|
+
import { YourPurchaseStrip } from "../../orders/components/YourPurchaseStrip.js";
|
|
18
|
+
import { ReviewFormDrawer } from "./ReviewFormDrawer.js";
|
|
19
|
+
export function PurchaseReviewAffordance({ productId, storeId, className, }) {
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const [editingId, setEditingId] = useState(undefined);
|
|
23
|
+
/*
|
|
24
|
+
* Invalidate the strip's own query, not just the reviews list. The strip is
|
|
25
|
+
* what shows "Your review: 4/5", so saving without invalidating it leaves
|
|
26
|
+
* the buyer looking at their old rating and reasonably concluding the edit
|
|
27
|
+
* did not take.
|
|
28
|
+
*/
|
|
29
|
+
const handleSaved = useCallback(() => {
|
|
30
|
+
void queryClient.invalidateQueries({
|
|
31
|
+
queryKey: ["user", "purchases", productId],
|
|
32
|
+
});
|
|
33
|
+
void queryClient.invalidateQueries({ queryKey: ["reviews"] });
|
|
34
|
+
}, [queryClient, productId]);
|
|
35
|
+
return (_jsxs(_Fragment, { children: [_jsx(YourPurchaseStrip, { productId: productId, className: className, onEditReview: (reviewId) => {
|
|
36
|
+
setEditingId(reviewId);
|
|
37
|
+
setOpen(true);
|
|
38
|
+
}, onWriteReview: () => {
|
|
39
|
+
setEditingId(undefined);
|
|
40
|
+
setOpen(true);
|
|
41
|
+
} }), _jsx(ReviewFormDrawer, { isOpen: open, onClose: () => setOpen(false), reviewId: editingId, productId: productId, storeId: storeId, onSaved: handleSaved })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors the PATCH route's schema, which is `.strict()`. A field this form can
|
|
4
|
+
* send that the route does not accept is a 400 naming it — which is the point:
|
|
5
|
+
* the two were allowed to disagree before, and the route silently accepted
|
|
6
|
+
* anything.
|
|
7
|
+
*/
|
|
8
|
+
export declare const reviewFormSchema: z.ZodObject<{
|
|
9
|
+
rating: z.ZodNumber;
|
|
10
|
+
title: z.ZodString;
|
|
11
|
+
comment: z.ZodString;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
title: string;
|
|
14
|
+
rating: number;
|
|
15
|
+
comment: string;
|
|
16
|
+
}, {
|
|
17
|
+
title: string;
|
|
18
|
+
rating: number;
|
|
19
|
+
comment: string;
|
|
20
|
+
}>;
|
|
21
|
+
export type ReviewFormValues = z.infer<typeof reviewFormSchema>;
|
|
22
|
+
export interface ReviewFormDrawerProps {
|
|
23
|
+
isOpen: boolean;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
/** Present = editing that review. Absent = writing a new one. */
|
|
26
|
+
reviewId?: string;
|
|
27
|
+
/** Required when writing; the review is filed against this product. */
|
|
28
|
+
productId?: string;
|
|
29
|
+
storeId?: string;
|
|
30
|
+
initialValues?: Partial<ReviewFormValues>;
|
|
31
|
+
/** Called after a successful save, so the caller can refetch. */
|
|
32
|
+
onSaved?: () => void;
|
|
33
|
+
}
|
|
34
|
+
export declare function ReviewFormDrawer({ isOpen, onClose, reviewId, productId, storeId, initialValues, onSaved, }: ReviewFormDrawerProps): import("react").JSX.Element;
|