@mohasinac/appkit 4.53.0 → 4.53.2
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/scaffolds/AppShell.js +1 -1
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/auctions/og.d.ts +3 -1
- package/dist/_internal/server/features/auctions/og.js +3 -1
- package/dist/_internal/server/features/bundles/actions.js +1 -1
- package/dist/_internal/server/features/business-documents/download.d.ts +41 -0
- package/dist/_internal/server/features/business-documents/download.js +91 -0
- package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
- package/dist/_internal/server/features/categories/adapters.js +10 -0
- 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 +14 -0
- package/dist/_internal/server/features/checkout/actions.js +106 -12
- package/dist/_internal/server/features/checkout/bundle-expansion.js +17 -0
- package/dist/_internal/server/features/checkout/data.d.ts +22 -0
- package/dist/_internal/server/features/checkout/group-rule-revalidation.d.ts +34 -0
- package/dist/_internal/server/features/checkout/group-rule-revalidation.js +124 -0
- package/dist/_internal/server/features/checkout/supplier-identity.d.ts +55 -0
- package/dist/_internal/server/features/checkout/supplier-identity.js +81 -0
- package/dist/_internal/server/features/classified/metadata.js +1 -1
- package/dist/_internal/server/features/classified/og.d.ts +3 -1
- package/dist/_internal/server/features/classified/og.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +1 -1
- package/dist/_internal/server/features/digital-code/og.d.ts +3 -1
- package/dist/_internal/server/features/digital-code/og.js +3 -1
- package/dist/_internal/server/features/digital-code/pool.d.ts +19 -4
- package/dist/_internal/server/features/digital-code/pool.js +19 -4
- package/dist/_internal/server/features/live/metadata.js +1 -1
- package/dist/_internal/server/features/live/og.d.ts +3 -1
- package/dist/_internal/server/features/live/og.js +3 -1
- package/dist/_internal/server/features/messages/actions.js +1 -1
- package/dist/_internal/server/features/parts/og.js +1 -1
- package/dist/_internal/server/features/pre-orders/og.d.ts +3 -1
- package/dist/_internal/server/features/pre-orders/og.js +3 -1
- 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/price-index/search.d.ts +18 -0
- package/dist/_internal/server/features/price-index/search.js +47 -7
- package/dist/_internal/server/features/prize-draws/og.d.ts +3 -1
- package/dist/_internal/server/features/prize-draws/og.js +3 -1
- package/dist/_internal/server/features/products/group-members.d.ts +26 -0
- package/dist/_internal/server/features/products/group-members.js +7 -1
- 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 +128 -10
- package/dist/_internal/server/features/products/og.d.ts +3 -1
- package/dist/_internal/server/features/products/og.js +3 -1
- package/dist/_internal/server/features/search/scoped-sources.d.ts +26 -0
- package/dist/_internal/server/features/search/scoped-sources.js +252 -0
- package/dist/_internal/server/features/search/search-everything.d.ts +57 -0
- package/dist/_internal/server/features/search/search-everything.js +70 -20
- package/dist/_internal/server/features/seo/sitemap.js +6 -0
- package/dist/_internal/server/features/shared/listing-adapters.js +4 -2
- package/dist/_internal/server/features/sticker-trace/adapters.d.ts +11 -0
- package/dist/_internal/server/features/sticker-trace/adapters.js +1 -0
- package/dist/_internal/server/features/sticker-trace/promote-to-listings.d.ts +26 -0
- package/dist/_internal/server/features/sticker-trace/promote-to-listings.js +264 -0
- package/dist/_internal/server/features/sticker-trace/sheet-actions.d.ts +3 -1
- package/dist/_internal/server/features/sticker-trace/sheet-actions.js +32 -0
- package/dist/_internal/server/features/sticker-trace/storage.d.ts +20 -1
- package/dist/_internal/server/features/sticker-trace/storage.js +20 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +16 -0
- package/dist/_internal/server/features/stores/index.d.ts +1 -0
- package/dist/_internal/server/features/stores/index.js +1 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +30 -0
- package/dist/_internal/server/features/stores/tabs.js +124 -0
- package/dist/_internal/shared/checkout/rules/_defaults.js +0 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +14 -1
- package/dist/_internal/shared/checkout/rules/_limits.js +14 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -0
- package/dist/_internal/shared/checkout/rules/auction.rule.js +9 -1
- package/dist/_internal/shared/checkout/rules/bundle.rule.js +13 -0
- package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
- package/dist/_internal/shared/checkout/rules/index.js +9 -0
- package/dist/_internal/shared/checkout/rules/offer.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +26 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +133 -0
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +29 -8
- package/dist/_internal/shared/checkout/rules/spares.rule.d.ts +16 -0
- package/dist/_internal/shared/checkout/rules/spares.rule.js +5 -0
- package/dist/_internal/shared/checkout/rules/types.d.ts +26 -2
- package/dist/_internal/shared/config/schema.d.ts +122 -0
- package/dist/_internal/shared/features/cart/schema.js +3 -2
- package/dist/_internal/shared/features/products/group-rules.d.ts +75 -0
- package/dist/_internal/shared/features/products/group-rules.js +142 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +24 -0
- package/dist/_internal/shared/listing-types/_registry.js +39 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/config.js +2 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/config.js +2 -0
- package/dist/_internal/shared/listing-types/availability.js +48 -0
- package/dist/_internal/shared/listing-types/capabilities.js +19 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/classified/config.js +2 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +2 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +1 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/live/config.js +2 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +2 -0
- package/dist/_internal/shared/listing-types/spares/config.d.ts +74 -0
- package/dist/_internal/shared/listing-types/spares/config.js +60 -0
- package/dist/_internal/shared/listing-types/spares/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/spares/ctas.js +4 -0
- package/dist/_internal/shared/listing-types/spares/og.js +1 -0
- package/dist/_internal/shared/listing-types/spares/schema.d.ts +2 -0
- package/dist/_internal/shared/listing-types/spares/schema.js +3 -0
- package/dist/_internal/shared/listing-types/spares/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/spares/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/config.js +2 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
- package/dist/_internal/shared/search/match.d.ts +11 -0
- package/dist/_internal/shared/search/match.js +30 -3
- package/dist/_internal/shared/search/to-groups.d.ts +52 -0
- package/dist/_internal/shared/search/to-groups.js +141 -0
- package/dist/client.d.ts +8 -2
- package/dist/client.js +4 -1
- package/dist/constants/api-endpoints.d.ts +46 -15
- package/dist/constants/api-endpoints.js +35 -5
- package/dist/constants/cache-invalidation.js +3 -1
- package/dist/constants/field-names.d.ts +6 -0
- package/dist/constants/field-names.js +6 -0
- package/dist/features/about/components/PolicyPageView.d.ts +22 -2
- package/dist/features/about/components/PolicyPageView.js +14 -2
- package/dist/features/about/components/PublicProfileView.js +2 -1
- package/dist/features/about/components/ShippingPolicyView.js +42 -2
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/hooks/useProfileStats.js +5 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +16 -0
- package/dist/features/addresses/repository/addresses.repository.js +21 -0
- package/dist/features/admin/actions/admin-actions.js +16 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +19 -2
- package/dist/features/admin/components/AdminNavigationView.js +1 -1
- package/dist/features/admin/components/AdminPageHeader.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +17 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +39 -8
- package/dist/features/admin/components/AdminSparesView.d.ts +12 -0
- package/dist/features/admin/components/AdminSparesView.js +30 -0
- package/dist/features/admin/components/AdminUsersView.js +9 -7
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.js +7 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/firestore.d.ts +26 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +12 -18
- package/dist/features/auth/repository/user.repository.d.ts +13 -0
- package/dist/features/auth/repository/user.repository.js +28 -0
- package/dist/features/auth/schemas/firestore.d.ts +2 -0
- package/dist/features/auth/schemas/firestore.js +2 -0
- package/dist/features/business-documents/repository/business-documents.repository.d.ts +58 -0
- package/dist/features/business-documents/repository/business-documents.repository.js +54 -0
- package/dist/features/business-documents/schemas/firestore.d.ts +105 -0
- package/dist/features/business-documents/schemas/firestore.js +75 -0
- package/dist/features/cart/actions/cart-actions.js +83 -6
- package/dist/features/cart/repository/cart.repository.js +32 -4
- package/dist/features/cart/schemas/firestore.d.ts +20 -0
- package/dist/features/cart/schemas/index.d.ts +2 -2
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +1 -1
- package/dist/features/categories/api/[id]/price-guidance/route.d.ts +42 -0
- package/dist/features/categories/api/[id]/price-guidance/route.js +90 -0
- package/dist/features/categories/components/BrandDetailPageView.js +32 -8
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +28 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoriesIndexListing.js +23 -8
- package/dist/features/categories/components/CategoryDetailPageView.js +157 -20
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +41 -7
- package/dist/features/categories/components/CategoryGrid.js +10 -1
- package/dist/features/categories/components/PriceGuidanceHint.d.ts +29 -0
- package/dist/features/categories/components/PriceGuidanceHint.js +61 -0
- package/dist/features/categories/components/SubcategoryCardRow.d.ts +27 -0
- package/dist/features/categories/components/SubcategoryCardRow.js +98 -0
- package/dist/features/categories/constants/category-tabs.d.ts +20 -25
- package/dist/features/categories/constants/category-tabs.js +22 -39
- package/dist/features/categories/repository/categories.repository.d.ts +12 -0
- package/dist/features/categories/repository/categories.repository.js +20 -0
- package/dist/features/categories/schemas/firestore.d.ts +22 -1
- package/dist/features/categories/schemas/firestore.js +10 -0
- package/dist/features/categories/schemas/index.d.ts +6 -0
- package/dist/features/categories/schemas/index.js +2 -0
- package/dist/features/categories/types/index.d.ts +8 -0
- package/dist/features/categories/utils/taxonomy-visual.d.ts +16 -3
- package/dist/features/categories/utils/taxonomy-visual.js +26 -10
- package/dist/features/classified/components/ClassifiedDetailPageView.js +14 -10
- package/dist/features/contact/components/ContactPageView.d.ts +33 -1
- package/dist/features/contact/components/ContactPageView.js +58 -28
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +14 -10
- package/dist/features/grouped/components/GroupedListingDetailView.js +9 -1
- package/dist/features/grouped/schemas/firestore.d.ts +5 -0
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +3 -3
- package/dist/features/homepage/components/CollectionCardsSection.d.ts +28 -6
- package/dist/features/homepage/components/CollectionCardsSection.js +43 -8
- package/dist/features/homepage/components/HowItWorksSection.d.ts +23 -6
- package/dist/features/homepage/components/HowItWorksSection.js +4 -4
- package/dist/features/homepage/components/PromoGrid.d.ts +31 -2
- package/dist/features/homepage/components/PromoGrid.js +12 -3
- package/dist/features/homepage/components/TestimonialsCarousel.d.ts +34 -2
- package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
- package/dist/features/homepage/components/index.d.ts +0 -20
- package/dist/features/homepage/components/index.js +0 -11
- package/dist/features/homepage/index.d.ts +0 -2
- package/dist/features/homepage/index.js +0 -2
- package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
- package/dist/features/homepage/lib/section-renderer.js +202 -58
- package/dist/features/homepage/repository/homepage-sections.repository.d.ts +35 -3
- package/dist/features/homepage/repository/homepage-sections.repository.js +58 -8
- package/dist/features/homepage/schemas/firestore.d.ts +106 -3
- package/dist/features/homepage/schemas/firestore.js +32 -2
- package/dist/features/homepage/schemas/index.d.ts +36 -7
- package/dist/features/homepage/schemas/index.js +39 -27
- package/dist/features/homepage/server.d.ts +0 -3
- package/dist/features/homepage/server.js +0 -3
- package/dist/features/homepage/types/index.d.ts +0 -47
- package/dist/features/homepage/types/index.js +49 -0
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/BottomNavItem.js +1 -1
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +14 -10
- package/dist/features/media/hooks/useMedia.js +17 -0
- package/dist/features/media/repository/media-assets.repository.d.ts +46 -0
- package/dist/features/media/repository/media-assets.repository.js +31 -0
- package/dist/features/media/trust-signals.d.ts +85 -0
- package/dist/features/media/trust-signals.js +126 -0
- package/dist/features/parts/components/PartDetailPageView.d.ts +14 -1
- package/dist/features/parts/components/PartDetailPageView.js +2 -2
- package/dist/features/parts/components/PartsIndexPageView.d.ts +11 -2
- package/dist/features/parts/components/PartsIndexPageView.js +2 -2
- package/dist/features/parts/components/PartsSearchBox.d.ts +9 -0
- package/dist/features/parts/components/PartsSearchBox.js +57 -0
- package/dist/features/parts/schemas/firestore.d.ts +149 -3
- package/dist/features/parts/schemas/firestore.js +184 -0
- package/dist/features/payments/repository/payout.repository.d.ts +15 -0
- package/dist/features/payments/repository/payout.repository.js +21 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +12 -18
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +24 -1
- package/dist/features/price-index/components/PriceIndexToolView.js +299 -17
- 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/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +20 -2
- package/dist/features/products/components/CompareOverlay.js +23 -3
- package/dist/features/products/components/CustomSectionTabContent.d.ts +31 -0
- package/dist/features/products/components/CustomSectionTabContent.js +29 -3
- package/dist/features/products/components/GroupMemberPicker.d.ts +19 -1
- package/dist/features/products/components/GroupMemberPicker.js +46 -8
- package/dist/features/products/components/GroupSettingsPanel.d.ts +11 -1
- package/dist/features/products/components/GroupSettingsPanel.js +92 -3
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +15 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +70 -8
- package/dist/features/products/components/ProductDetailPageView.js +81 -17
- package/dist/features/products/components/ProductForm.d.ts +13 -1
- package/dist/features/products/components/ProductForm.js +16 -2
- package/dist/features/products/components/ProductsIndexListing.d.ts +16 -1
- package/dist/features/products/components/ProductsIndexListing.js +3 -3
- package/dist/features/products/components/ShowGroupSection.js +15 -2
- package/dist/features/products/components/SparesListView.d.ts +7 -0
- package/dist/features/products/components/SparesListView.js +39 -0
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/repository/products.repository.d.ts +99 -0
- package/dist/features/products/repository/products.repository.js +240 -18
- package/dist/features/products/schemas/firestore.d.ts +97 -4
- package/dist/features/products/schemas/firestore.js +104 -0
- package/dist/features/products/schemas/index.d.ts +10 -19
- package/dist/features/products/schemas/index.js +23 -6
- package/dist/features/products/types/index.d.ts +45 -1
- package/dist/features/products/utils/group-member-image.d.ts +46 -0
- package/dist/features/products/utils/group-member-image.js +4 -0
- package/dist/features/products/utils/listing-badge-variant.js +4 -0
- package/dist/features/reviews/components/ReviewsList.js +1 -1
- package/dist/features/scams/components/ScamProfileView.js +1 -1
- package/dist/features/search/components/EntityTypeahead.d.ts +44 -0
- package/dist/features/search/components/EntityTypeahead.js +92 -0
- package/dist/features/search/components/Search.d.ts +12 -1
- package/dist/features/search/components/Search.js +47 -3
- package/dist/features/search/components/SearchFacetRail.d.ts +11 -0
- package/dist/features/search/components/SearchFacetRail.js +92 -0
- package/dist/features/search/components/SearchResultsClient.d.ts +33 -0
- package/dist/features/search/components/SearchResultsClient.js +103 -0
- package/dist/features/search/components/SearchResultsPageView.d.ts +15 -1
- package/dist/features/search/components/SearchResultsPageView.js +2 -2
- package/dist/features/search/components/index.d.ts +0 -6
- package/dist/features/search/components/index.js +0 -3
- package/dist/features/search/hooks/useNavSuggestions.d.ts +13 -0
- package/dist/features/search/hooks/useScopedSearch.d.ts +34 -0
- package/dist/features/search/hooks/useScopedSearch.js +114 -0
- package/dist/features/search/hooks/useSiteSearch.d.ts +26 -0
- package/dist/features/search/hooks/useSiteSearch.js +149 -0
- package/dist/features/search/index.d.ts +0 -4
- package/dist/features/search/index.js +0 -4
- package/dist/features/search/manifest.d.ts +12 -0
- package/dist/features/search/manifest.js +17 -2
- package/dist/features/search/server.d.ts +0 -3
- package/dist/features/search/server.js +0 -3
- package/dist/features/seller/components/SellerListingTypeView.d.ts +1 -1
- package/dist/features/seller/components/SellerListingTypeView.js +20 -0
- package/dist/features/seller/components/SellerPayoutSettingsView.js +35 -14
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductShell.js +2 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/seller/components/SellerSparesView.d.ts +17 -0
- package/dist/features/seller/components/SellerSparesView.js +6 -0
- package/dist/features/seller/components/index.d.ts +2 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/shell/CommandPalette.js +1 -1
- package/dist/features/sticker-trace/schemas/firestore.d.ts +19 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +7 -82
- package/dist/features/stores/components/StoreHeader.js +1 -2
- package/dist/features/stores/components/StoreSparesPageView.d.ts +27 -0
- package/dist/features/stores/components/StoreSparesPageView.js +48 -0
- package/dist/features/stores/schemas/firestore.d.ts +19 -0
- package/dist/features/tester/seed-data/authored/buying__spares-and-groups.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__spares-and-groups.js +96 -0
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +392 -45
- package/dist/features/tester/seed-data/authored/content-discovery__category-tabs-franchise-bundles.js +54 -32
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +3 -2
- package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.js +73 -0
- package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.js +340 -0
- package/dist/features/tester/seed-data/authored/index.js +16 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.d.ts +1 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -0
- package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.js +77 -0
- package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.js +58 -0
- package/dist/features/tester/seed-data/authored/tools__sticker-tracer.js +5 -4
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +384 -18
- package/dist/index.d.ts +17 -57
- package/dist/index.js +35 -96
- package/dist/next/routing/route-map.d.ts +42 -1
- package/dist/next/routing/route-map.js +45 -0
- package/dist/providers/db-firebase/base.repository.d.ts +22 -0
- package/dist/providers/db-firebase/base.repository.js +36 -0
- package/dist/schemas/registry.d.ts +15 -6
- package/dist/security/blind-index-tokens.d.ts +54 -0
- package/dist/security/blind-index-tokens.js +152 -0
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/settings-encryption.d.ts +35 -0
- package/dist/security/settings-encryption.js +91 -0
- package/dist/seed/_helpers/brand-rows.js +38 -14
- package/dist/seed/_helpers/category-forest.js +73 -7
- package/dist/seed/_helpers/category-images-generated.d.ts +25 -6
- package/dist/seed/_helpers/category-images-generated.js +81 -6
- package/dist/seed/_helpers/category-models.d.ts +1 -1
- package/dist/seed/_helpers/category-models.js +7 -1
- package/dist/seed/_helpers/category-parts-glossary-map.js +8 -0
- package/dist/seed/_helpers/category-parts.d.ts +1 -1
- package/dist/seed/_helpers/category-parts.js +50 -2
- package/dist/seed/_helpers/character-features.d.ts +1 -1
- package/dist/seed/_helpers/character-features.js +2 -1
- package/dist/seed/_helpers/franchise-rows.js +10 -2
- package/dist/seed/_helpers/media.d.ts +47 -0
- package/dist/seed/_helpers/media.js +34 -0
- package/dist/seed/authenticity-knowledge-seed-data.d.ts +49 -5
- package/dist/seed/authenticity-knowledge-seed-data.js +10001 -549
- package/dist/seed/categories-seed-data.js +124 -20
- package/dist/seed/grouped-listings-seed-data.js +16 -0
- package/dist/seed/homepage-sections-seed-data.js +94 -5
- package/dist/seed/index.d.ts +2 -3
- package/dist/seed/index.js +2 -2
- package/dist/seed/manifest.js +3 -0
- package/dist/seed/media-assets-seed-data.d.ts +1 -1
- package/dist/seed/media-assets-seed-data.js +746 -196
- package/dist/seed/model-composition-seed-data.js +122 -6
- package/dist/seed/part-glossary-seed-data.d.ts +1 -1
- package/dist/seed/part-glossary-seed-data.js +37219 -463
- package/dist/seed/products-art-seed-data.js +7 -18
- package/dist/seed/products-auctions-seed-data.js +56 -47
- package/dist/seed/products-beyblade-x-seed-data.d.ts +2 -0
- package/dist/seed/products-beyblade-x-seed-data.js +476 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +5 -5
- package/dist/seed/products-catalogue-seed-data.js +1500 -1240
- package/dist/seed/products-classifieds-seed-data.js +9 -27
- package/dist/seed/products-digital-codes-seed-data.js +9 -18
- package/dist/seed/products-live-items-seed-data.js +5 -13
- package/dist/seed/products-preorders-seed-data.js +67 -33
- package/dist/seed/products-prize-draws-seed-data.js +8 -17
- package/dist/seed/products-spares-seed-data.d.ts +2 -0
- package/dist/seed/products-spares-seed-data.js +368 -0
- package/dist/seed/products-standard-seed-data.js +39 -126
- package/dist/seed/products-stickers-seed-data.js +59 -15
- package/dist/seed/site-settings-seed-data.js +8 -0
- package/dist/seo/json-ld.d.ts +25 -0
- package/dist/seo/json-ld.js +28 -0
- package/dist/server-entry.d.ts +3 -1
- package/dist/server-entry.js +2 -2
- package/dist/server.d.ts +11 -9
- package/dist/server.js +35 -25
- package/dist/styles.css +202 -1
- package/dist/tailwind-input.css +31 -17
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Accordion.js +1 -1
- package/dist/ui/components/Breadcrumb.js +1 -1
- package/dist/ui/components/DetailPageGallery.js +0 -2
- package/dist/ui/components/DetailPageHero.js +0 -1
- package/dist/ui/components/DetailPageTabs.js +1 -1
- package/dist/ui/components/Divider.js +3 -3
- package/dist/ui/components/GatedPrice.js +5 -1
- package/dist/ui/components/HorizontalScroller.js +1 -1
- package/dist/ui/components/ListingLayout.js +3 -3
- package/dist/ui/components/MediaTabs.style.css +81 -0
- package/dist/ui/components/Pagination.js +1 -1
- package/dist/ui/components/PendingValue.d.ts +45 -0
- package/dist/ui/components/PendingValue.js +7 -0
- package/dist/ui/components/Radio.style.css +119 -0
- package/dist/ui/components/RatingDisplay.js +2 -2
- package/dist/ui/components/SettingsSection.js +0 -2
- package/dist/ui/components/SiteLogo.js +3 -0
- package/dist/ui/components/SiteMark.js +3 -0
- package/dist/ui/components/SortDropdown.js +1 -1
- package/dist/ui/components/SpecTable.d.ts +58 -0
- package/dist/ui/components/SpecTable.js +8 -0
- package/dist/ui/components/StarRating.js +1 -3
- package/dist/ui/components/SummaryCard.js +1 -1
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/icons/icon-registry.d.ts +1 -0
- package/dist/ui/icons/icon-registry.js +9 -1
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/utils/search-txt-query.d.ts +12 -0
- package/dist/utils/search-txt-query.js +12 -0
- package/package.json +1 -1
- package/scripts/dedupe-peer-deps.mjs +18 -0
- package/scripts/seed-cli.mjs +250 -35
- package/scripts/verify-css-build.mjs +118 -37
- package/dist/_internal/server/features/search/actions.d.ts +0 -7
- package/dist/_internal/server/features/search/actions.js +0 -21
- package/dist/_internal/server/features/search/data.d.ts +0 -15
- package/dist/_internal/server/features/search/data.js +0 -18
- package/dist/_internal/server/features/search/index.d.ts +0 -3
- package/dist/_internal/server/features/search/index.js +0 -2
- package/dist/features/homepage/api/[id]/route.d.ts +0 -23
- package/dist/features/homepage/api/[id]/route.js +0 -103
- package/dist/features/homepage/api/route.d.ts +0 -14
- package/dist/features/homepage/api/route.js +0 -140
- package/dist/features/homepage/api/sections/[id]/route.d.ts +0 -26
- package/dist/features/homepage/api/sections/[id]/route.js +0 -95
- package/dist/features/homepage/api/sections/route.d.ts +0 -13
- package/dist/features/homepage/api/sections/route.js +0 -82
- package/dist/features/homepage/components/AdvertisementBanner.d.ts +0 -14
- package/dist/features/homepage/components/AdvertisementBanner.js +0 -35
- package/dist/features/homepage/components/BeforeAfterCard.d.ts +0 -9
- package/dist/features/homepage/components/BeforeAfterCard.js +0 -73
- package/dist/features/homepage/components/FeaturedResultsSection.d.ts +0 -17
- package/dist/features/homepage/components/FeaturedResultsSection.js +0 -17
- package/dist/features/homepage/components/HeroBanner.d.ts +0 -7
- package/dist/features/homepage/components/HeroBanner.js +0 -43
- package/dist/features/homepage/components/HeroSection.d.ts +0 -7
- package/dist/features/homepage/components/HeroSection.js +0 -6
- package/dist/features/homepage/components/HomepageSkeleton.d.ts +0 -2
- package/dist/features/homepage/components/HomepageSkeleton.js +0 -21
- package/dist/features/homepage/components/HomepageView.d.ts +0 -25
- package/dist/features/homepage/components/HomepageView.js +0 -5
- package/dist/features/homepage/components/NewsletterBanner.d.ts +0 -23
- package/dist/features/homepage/components/NewsletterBanner.js +0 -37
- package/dist/features/homepage/components/SiteFeaturesSection.d.ts +0 -14
- package/dist/features/homepage/components/SiteFeaturesSection.js +0 -11
- package/dist/features/homepage/components/TrustBadges.d.ts +0 -6
- package/dist/features/homepage/components/TrustBadges.js +0 -46
- package/dist/features/homepage/components/TrustIndicatorsSection.d.ts +0 -12
- package/dist/features/homepage/components/TrustIndicatorsSection.js +0 -11
- package/dist/features/homepage/hooks/useHomepage.d.ts +0 -11
- package/dist/features/homepage/hooks/useHomepage.js +0 -17
- package/dist/features/homepage/hooks/useHomepageSections.d.ts +0 -2
- package/dist/features/homepage/hooks/useHomepageSections.js +0 -10
- package/dist/features/homepage/repository/homepage.repository.d.ts +0 -11
- package/dist/features/homepage/repository/homepage.repository.js +0 -25
- package/dist/features/search/actions/index.d.ts +0 -1
- package/dist/features/search/actions/index.js +0 -1
- package/dist/features/search/actions/search-actions.d.ts +0 -18
- package/dist/features/search/actions/search-actions.js +0 -43
- package/dist/features/search/api/route.d.ts +0 -22
- package/dist/features/search/api/route.js +0 -128
- package/dist/features/search/columns/index.d.ts +0 -17
- package/dist/features/search/columns/index.js +0 -49
- package/dist/features/search/components/SearchFiltersRow.d.ts +0 -22
- package/dist/features/search/components/SearchFiltersRow.js +0 -21
- package/dist/features/search/components/SearchResultsSection.d.ts +0 -53
- package/dist/features/search/components/SearchResultsSection.js +0 -35
- package/dist/features/search/components/SearchView.d.ts +0 -27
- package/dist/features/search/components/SearchView.js +0 -8
- package/dist/features/search/hooks/useSearch.d.ts +0 -21
- package/dist/features/search/hooks/useSearch.js +0 -33
- package/dist/features/search/repository/search.repository.d.ts +0 -14
- package/dist/features/search/repository/search.repository.js +0 -59
- package/dist/features/search/schemas/index.d.ts +0 -70
- package/dist/features/search/schemas/index.js +0 -36
- package/dist/features/search/types/index.d.ts +0 -48
- package/dist/features/stores/components/StoreScopedSearch.d.ts +0 -17
- package/dist/features/stores/components/StoreScopedSearch.js +0 -27
- package/dist/seed/defaults/homepage-sections.d.ts +0 -2
- package/dist/seed/defaults/homepage-sections.js +0 -60
- package/dist/seed/factories/homepage-section.factory.d.ts +0 -10
- package/dist/seed/factories/homepage-section.factory.js +0 -13
- /package/dist/{features/search/types/index.js → _internal/shared/listing-types/spares/og.d.ts} +0 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* `GET /api/products?ids=<csv>` (BK3 listByIds endpoint).
|
|
16
16
|
*/
|
|
17
17
|
import type { ListingType } from "../types";
|
|
18
|
+
import type { ProductImage } from "../schemas/firestore";
|
|
18
19
|
/** Subset of ProductDocument the overlay needs. Kept loose so it can be fed
|
|
19
20
|
* sanitized public payloads directly. */
|
|
20
21
|
export interface CompareProductLike {
|
|
@@ -24,7 +25,7 @@ export interface CompareProductLike {
|
|
|
24
25
|
price?: number;
|
|
25
26
|
currency?: string;
|
|
26
27
|
mainImage?: string;
|
|
27
|
-
images?: string[];
|
|
28
|
+
images?: readonly (string | ProductImage)[];
|
|
28
29
|
condition?: string;
|
|
29
30
|
brand?: string;
|
|
30
31
|
category?: string;
|
|
@@ -60,6 +61,8 @@ export interface CompareOverlayLabels {
|
|
|
60
61
|
close?: string;
|
|
61
62
|
remove?: string;
|
|
62
63
|
view?: string;
|
|
64
|
+
/** Placeholder/aria-label for the "add another" typeahead. Only rendered when `onAdd` is passed. */
|
|
65
|
+
addAnother?: string;
|
|
63
66
|
field?: Partial<CompareFieldLabels>;
|
|
64
67
|
}
|
|
65
68
|
export interface CompareOverlayProps {
|
|
@@ -73,7 +76,22 @@ export interface CompareOverlayProps {
|
|
|
73
76
|
onClose: () => void;
|
|
74
77
|
/** Called when a column's Remove button is clicked. */
|
|
75
78
|
onRemove?: (id: string) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Called with a resolved slug when the user picks a result from the
|
|
81
|
+
* "Add another" typeahead. Omit to leave the overlay exactly as it was —
|
|
82
|
+
* every existing caller keeps working unchanged.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 Resolution only, no fetch. The overlay already knows how to turn an
|
|
85
|
+
* id into a full `CompareProductLike` via `productIds` (`useFetchedProducts`
|
|
86
|
+
* fetches `/api/products?ids=…`); this hands back ONE more id for that same
|
|
87
|
+
* mechanism, rather than teaching the overlay a second way to load a
|
|
88
|
+
* product. The caller decides whether to append to its own `productIds`
|
|
89
|
+
* state or ignore it.
|
|
90
|
+
*/
|
|
91
|
+
onAdd?: (id: string) => void;
|
|
92
|
+
/** `siteSettings.search?.indexEnabled`. Threaded through to the typeahead. */
|
|
93
|
+
searchIndexEnabled?: boolean;
|
|
76
94
|
labels?: CompareOverlayLabels;
|
|
77
95
|
}
|
|
78
|
-
export declare function CompareOverlay({ isOpen, items: passedItems, productIds, productType, onClose, onRemove, labels: labelOverrides, }: CompareOverlayProps): import("react").JSX.Element | null;
|
|
96
|
+
export declare function CompareOverlay({ isOpen, items: passedItems, productIds, productType, onClose, onRemove, onAdd, searchIndexEnabled, labels: labelOverrides, }: CompareOverlayProps): import("react").JSX.Element | null;
|
|
79
97
|
export {};
|
|
@@ -20,12 +20,15 @@ import { COMPARE_MAX_ITEMS } from "../constants/action-defs.js";
|
|
|
20
20
|
import { normalizeListingType } from "../utils/listing-type.js";
|
|
21
21
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
22
22
|
import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
23
|
+
import { EntityTypeahead } from "../../search/components/EntityTypeahead.js";
|
|
24
|
+
import { productImageUrls } from "../schemas/firestore.js";
|
|
23
25
|
const DEFAULT_LABELS = {
|
|
24
26
|
title: "Compare items",
|
|
25
27
|
empty: "Select up to 4 items from the listing to compare.",
|
|
26
28
|
close: "Close",
|
|
27
29
|
remove: "Remove",
|
|
28
30
|
view: "View",
|
|
31
|
+
addAnother: "Add another to compare",
|
|
29
32
|
field: {
|
|
30
33
|
image: "Photo",
|
|
31
34
|
price: "Price",
|
|
@@ -35,6 +38,12 @@ const DEFAULT_LABELS = {
|
|
|
35
38
|
store: "Store",
|
|
36
39
|
},
|
|
37
40
|
};
|
|
41
|
+
/** Index type keys for each `productType` discriminator the overlay accepts. */
|
|
42
|
+
const TYPEAHEAD_TYPES = {
|
|
43
|
+
product: ["standard"],
|
|
44
|
+
auction: ["auction"],
|
|
45
|
+
preorder: ["pre-order"],
|
|
46
|
+
};
|
|
38
47
|
const OVERLAY_CLASS = "fixed inset-0 flex flex-col bg-[var(--appkit-color-bg)] overflow-hidden z-[var(--appkit-z-modal,60)]";
|
|
39
48
|
const HEADER_CLASS = "flex items-center justify-between gap-[var(--appkit-space-3)] border-b border-[var(--appkit-color-border-subtle)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)]";
|
|
40
49
|
const FIELD_LABEL_CLASS = "text-[length:var(--appkit-text-xs)] font-medium uppercase tracking-wider text-[var(--appkit-color-text-muted)]";
|
|
@@ -66,7 +75,7 @@ function customFieldValue(field) {
|
|
|
66
75
|
}
|
|
67
76
|
function CompareColumn({ item, productType, labels, onRemove, onClose, customFieldKeys }) {
|
|
68
77
|
const href = detailHref(item, productType);
|
|
69
|
-
const img = item.mainImage
|
|
78
|
+
const img = item.mainImage || productImageUrls(item.images)[0];
|
|
70
79
|
return (_jsxs(Stack, { gap: "md", className: COLUMN_CARD_CLASS, children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, className: "text-[length:var(--appkit-text-sm)] font-semibold text-[var(--appkit-color-text)] hover:underline line-clamp-2", children: item.title ?? item.id }), onRemove && (_jsx(IconButton, { type: "button", onClick: () => onRemove(item.id), "aria-label": `${labels.remove} ${item.title ?? item.id}`, variant: "ghost", rounded: "full", className: CLS_REMOVE_BTN, children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsx(FieldRow, { label: labels.field.image, children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, children: _jsx(Div, { className: `relative aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: img ? (_jsx(MediaImage, { src: img, alt: item.title ?? item.id, size: "card" })) : null }) }) }), _jsx(FieldRow, { label: labels.field.price, children: _jsx(Text, { size: "base", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: priceLabel(item) }) }) }), _jsx(FieldRow, { label: labels.field.condition, children: _jsx(Text, { children: item.condition ? _jsx(Span, { className: CHIP_CLASS, children: item.condition }) : "—" }) }), _jsx(FieldRow, { label: labels.field.brand, children: _jsx(Text, { children: item.brand ? _jsx(Span, { className: CHIP_CLASS, children: item.brand }) : "—" }) }), _jsx(FieldRow, { label: labels.field.category, children: _jsx(Text, { children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) || (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) ? (_jsx(Span, { className: CHIP_CLASS, children: (Array.isArray(item.categoryNames) ? item.categoryNames[0] : item.categoryName) ?? (Array.isArray(item.categorySlugs) ? item.categorySlugs[0] : item.category) })) : ("—") }) }), _jsx(FieldRow, { label: labels.field.store, children: _jsx(Text, { size: "sm", color: "muted", children: item.storeName ?? "—" }) }), customFieldKeys.map((key) => (_jsx(FieldRow, { label: key, children: _jsx(Text, { size: "sm", children: customFieldValue(item.customFields?.find((f) => f.key === key)) }) }, key))), _jsx(Button, { asChild: true, variant: "primary", size: "sm", className: "mt-auto", children: _jsx(Link, { href: href, target: "_blank", rel: REL_NOOPENER, onClick: onClose, children: labels.view }) })] }));
|
|
71
80
|
}
|
|
72
81
|
function useFetchedProducts(productIds, passedItems, isOpen) {
|
|
@@ -108,7 +117,7 @@ function useFetchedProducts(productIds, passedItems, isOpen) {
|
|
|
108
117
|
isLoading: !passedItems && isLoading,
|
|
109
118
|
};
|
|
110
119
|
}
|
|
111
|
-
export function CompareOverlay({ isOpen, items: passedItems, productIds, productType = "product", onClose, onRemove, labels: labelOverrides, }) {
|
|
120
|
+
export function CompareOverlay({ isOpen, items: passedItems, productIds, productType = "product", onClose, onRemove, onAdd, searchIndexEnabled, labels: labelOverrides, }) {
|
|
112
121
|
const labels = useMemo(() => ({
|
|
113
122
|
...DEFAULT_LABELS,
|
|
114
123
|
...labelOverrides,
|
|
@@ -167,7 +176,18 @@ export function CompareOverlay({ isOpen, items: passedItems, productIds, product
|
|
|
167
176
|
}, [onRemove]);
|
|
168
177
|
if (!isOpen)
|
|
169
178
|
return null;
|
|
170
|
-
return (_jsxs(Div, { className: OVERLAY_CLASS, role: "dialog", "aria-modal": "true", "aria-label": labels.title, children: [_jsxs(Row, { justify: "between", align: "center", className: HEADER_CLASS, children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: labels.title }), _jsx(IconButton, { type: "button", onClick: onClose, "aria-label": labels.close, variant: "ghost", rounded: "full", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsx(Div, { className:
|
|
179
|
+
return (_jsxs(Div, { className: OVERLAY_CLASS, role: "dialog", "aria-modal": "true", "aria-label": labels.title, children: [_jsxs(Row, { justify: "between", align: "center", className: HEADER_CLASS, children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: labels.title }), _jsx(IconButton, { type: "button", onClick: onClose, "aria-label": labels.close, variant: "ghost", rounded: "full", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]", children: _jsx(X, { className: "h-5 w-5" }) })] }), onAdd && trimmed.length < COMPARE_MAX_ITEMS && (_jsx(Div, { className: `${__P.p4} border-b border-[var(--appkit-color-border-subtle)]`, children: _jsx(EntityTypeahead, { types: TYPEAHEAD_TYPES[productType], indexEnabled: searchIndexEnabled, label: labels.addAnother, placeholder: labels.addAnother, limit: 5, onPick: (pick) => {
|
|
180
|
+
/*
|
|
181
|
+
* The LAST path segment, not a type-specific pattern. The
|
|
182
|
+
* three listing types this overlay accepts have three
|
|
183
|
+
* different detail routes (/products, /auctions,
|
|
184
|
+
* /pre-orders), and every one of them ends in the slug — the
|
|
185
|
+
* same thing `productIds` already expects.
|
|
186
|
+
*/
|
|
187
|
+
const slug = pick.href.split("/").filter(Boolean).pop();
|
|
188
|
+
if (slug)
|
|
189
|
+
onAdd(slug);
|
|
190
|
+
} }) })), _jsx(Div, { className: `flex-1 ${__O.auto} ${__P.p4}`, children: isLoading ? (
|
|
171
191
|
// audit-content-alignment-ok: loading-state text inside a transient overlay panel, not marketing content
|
|
172
192
|
_jsx(Text, { paddingY: "3xl", variant: "secondary", align: "center", children: "Loading\u00E2\u20AC\u00A6" })) : trimmed.length === 0 ? (
|
|
173
193
|
// audit-content-alignment-ok: empty-state inside a transient overlay panel, not marketing content
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { CustomSection } from "../schemas/firestore";
|
|
3
|
+
/**
|
|
4
|
+
* Shape `ProductTabsShell` expects for `customTabs`. Duplicated here rather
|
|
5
|
+
* than imported from `ProductTabsShell.tsx` — that file is `"use client"` and
|
|
6
|
+
* this one is a plain function, so importing its type keeps this module
|
|
7
|
+
* server-renderable. The two must stay structurally compatible; they already
|
|
8
|
+
* do (`{ id, label, content }`).
|
|
9
|
+
*/
|
|
10
|
+
interface CustomSectionTab {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
content: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `customSections` → `ProductTabsShell`'s `customTabs` prop, in one place.
|
|
17
|
+
*
|
|
18
|
+
* Six detail views (`ProductDetailPageView`, `AuctionDetailPageView`,
|
|
19
|
+
* `PreOrderDetailPageView`, `ClassifiedDetailPageView`,
|
|
20
|
+
* `DigitalCodeDetailPageView`, `LiveItemDetailPageView`) had this EXACT
|
|
21
|
+
* five-line block — the same `Array.isArray` guard and the same `.map` —
|
|
22
|
+
* copy-pasted six times. Rule-of-Three material by the Duplication Decision
|
|
23
|
+
* Framework (same prop surface, same shape, a bug fix would have needed six
|
|
24
|
+
* commits), consolidated here rather than left as six places that could
|
|
25
|
+
* silently drift the next time one of them is touched and the other five are
|
|
26
|
+
* not.
|
|
27
|
+
*
|
|
28
|
+
* Takes the raw, possibly-untyped `customSections` field directly (every call
|
|
29
|
+
* site reads it off a cast Firestore document) so callers do one line instead
|
|
30
|
+
* of two: `customTabs={toCustomSectionTabs(p.customSections)}`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toCustomSectionTabs(input: unknown): CustomSectionTab[];
|
|
3
33
|
export declare function CustomSectionTabContent({ section }: {
|
|
4
34
|
section: CustomSection;
|
|
5
35
|
}): React.JSX.Element;
|
|
36
|
+
export {};
|
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { Anchor, Dd, Dl, Dt, RichText, Stack, Text } from "../../../ui/index.js";
|
|
2
|
+
import { RichText, SpecTable, Stack, Text } from "../../../ui/index.js";
|
|
4
3
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
5
4
|
function renderFieldValue(f) {
|
|
6
5
|
if (f.type === "boolean")
|
|
7
6
|
return f.value === "true" ? "Yes" : "No";
|
|
8
7
|
return f.value + (f.unit ? ` ${f.unit}` : "");
|
|
9
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* `customSections` → `ProductTabsShell`'s `customTabs` prop, in one place.
|
|
11
|
+
*
|
|
12
|
+
* Six detail views (`ProductDetailPageView`, `AuctionDetailPageView`,
|
|
13
|
+
* `PreOrderDetailPageView`, `ClassifiedDetailPageView`,
|
|
14
|
+
* `DigitalCodeDetailPageView`, `LiveItemDetailPageView`) had this EXACT
|
|
15
|
+
* five-line block — the same `Array.isArray` guard and the same `.map` —
|
|
16
|
+
* copy-pasted six times. Rule-of-Three material by the Duplication Decision
|
|
17
|
+
* Framework (same prop surface, same shape, a bug fix would have needed six
|
|
18
|
+
* commits), consolidated here rather than left as six places that could
|
|
19
|
+
* silently drift the next time one of them is touched and the other five are
|
|
20
|
+
* not.
|
|
21
|
+
*
|
|
22
|
+
* Takes the raw, possibly-untyped `customSections` field directly (every call
|
|
23
|
+
* site reads it off a cast Firestore document) so callers do one line instead
|
|
24
|
+
* of two: `customTabs={toCustomSectionTabs(p.customSections)}`.
|
|
25
|
+
*/
|
|
26
|
+
export function toCustomSectionTabs(input) {
|
|
27
|
+
const sections = Array.isArray(input)
|
|
28
|
+
? input
|
|
29
|
+
: [];
|
|
30
|
+
return sections.map((s) => ({
|
|
31
|
+
id: s.id,
|
|
32
|
+
label: s.title,
|
|
33
|
+
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
10
36
|
export function CustomSectionTabContent({ section }) {
|
|
11
37
|
const html = section.text ? normalizeRichTextHtml(section.text) : null;
|
|
12
38
|
const fields = section.fields?.filter((f) => f.key && f.value) ?? [];
|
|
13
|
-
return (_jsxs(Stack, { gap: "md", children: [html && (_jsx(RichText, { html: html, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })), fields.length > 0 && (_jsx(
|
|
39
|
+
return (_jsxs(Stack, { gap: "md", children: [html && (_jsx(RichText, { html: html, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })), fields.length > 0 && (_jsx(SpecTable, { rows: fields.map((f) => ({ name: f.key, value: String(f.value), unit: f.unit })) })), !html && fields.length === 0 && (_jsx(Text, { size: "sm", color: "faint", children: "No content in this section." }))] }));
|
|
14
40
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* consumer pages untouched.
|
|
17
17
|
*/
|
|
18
18
|
import React from "react";
|
|
19
|
+
import { type GroupRules } from "../../../_internal/shared/features/products/group-rules";
|
|
19
20
|
import type { ListingType } from "../types";
|
|
20
21
|
export interface GroupPickerMember {
|
|
21
22
|
id: string;
|
|
@@ -29,6 +30,14 @@ export interface GroupPickerMember {
|
|
|
29
30
|
isGroupParent?: boolean;
|
|
30
31
|
condition?: string;
|
|
31
32
|
availableQuantity?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Printed or built when ordered — no stock ceiling.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 Both the "Out of stock" refusal and the stepper's `max` read
|
|
37
|
+
* `availableQuantity`, so without this a print-on-demand member at quantity 0
|
|
38
|
+
* renders as unselectable while being perfectly buyable.
|
|
39
|
+
*/
|
|
40
|
+
madeToOrder?: boolean;
|
|
32
41
|
storeId?: string;
|
|
33
42
|
storeName?: string;
|
|
34
43
|
isSold?: boolean;
|
|
@@ -38,9 +47,18 @@ export interface GroupMemberPickerProps {
|
|
|
38
47
|
groupId: string;
|
|
39
48
|
groupSource: "product-group" | "grouped-listing";
|
|
40
49
|
members: GroupPickerMember[];
|
|
50
|
+
/**
|
|
51
|
+
* The group's own selection rules, from whichever home owns them.
|
|
52
|
+
*
|
|
53
|
+
* 🛑 Absent means UNRESTRICTED, which is what every group that exists today
|
|
54
|
+
* is. The server enforces the same rules through `checkGroupRules` whatever
|
|
55
|
+
* is passed here, so omitting them costs correctness nothing — the buyer just
|
|
56
|
+
* finds out on submit instead of while choosing.
|
|
57
|
+
*/
|
|
58
|
+
rules?: GroupRules;
|
|
41
59
|
/** Called after a successful add so the host can close its overlay. */
|
|
42
60
|
onAdded?: () => void;
|
|
43
61
|
/** Called when the API reports the caller isn't signed in. */
|
|
44
62
|
onAuthRequired?: () => void;
|
|
45
63
|
}
|
|
46
|
-
export declare function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAuthRequired, }: GroupMemberPickerProps): React.JSX.Element;
|
|
64
|
+
export declare function GroupMemberPicker({ groupId, groupSource, members, rules, onAdded, onAuthRequired, }: GroupMemberPickerProps): React.JSX.Element;
|
|
@@ -27,10 +27,10 @@ import { CART_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
|
27
27
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
28
28
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
29
29
|
import { normalizeListingType } from "../utils/listing-type.js";
|
|
30
|
+
import { resolveGroupMemberImage } from "../utils/group-member-image.js";
|
|
31
|
+
import { checkGroupRules, describeGroupRules, EMPTY_GROUP_RULES, } from "../../../_internal/shared/features/products/group-rules.js";
|
|
30
32
|
const __O = { xAuto: "overflow-x-auto", hidden: "overflow-hidden" };
|
|
31
|
-
|
|
32
|
-
return m.image ?? m.images?.[0] ?? "";
|
|
33
|
-
}
|
|
33
|
+
const memberImage = resolveGroupMemberImage;
|
|
34
34
|
function memberHref(m) {
|
|
35
35
|
return pluginFor(normalizeListingType(m)).detailRoute(m.slug ?? m.id);
|
|
36
36
|
}
|
|
@@ -40,7 +40,10 @@ function blockedReason(m) {
|
|
|
40
40
|
return "Sold";
|
|
41
41
|
if (m.status && m.status !== "published")
|
|
42
42
|
return "Unavailable";
|
|
43
|
-
|
|
43
|
+
// Made to order has no stock to be out of — checked BEFORE the quantity
|
|
44
|
+
// test, and after `isSold`, which stays the seller's kill switch whatever
|
|
45
|
+
// the production model.
|
|
46
|
+
if (m.madeToOrder !== true && (m.availableQuantity ?? 0) <= 0)
|
|
44
47
|
return "Out of stock";
|
|
45
48
|
// `cartLine: "blocked"` is how a classified / live listing says "contact the
|
|
46
49
|
// seller instead" — those can never join a cart line.
|
|
@@ -48,7 +51,7 @@ function blockedReason(m) {
|
|
|
48
51
|
return "Not purchasable here";
|
|
49
52
|
return null;
|
|
50
53
|
}
|
|
51
|
-
export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAuthRequired, }) {
|
|
54
|
+
export function GroupMemberPicker({ groupId, groupSource, members, rules = EMPTY_GROUP_RULES, onAdded, onAuthRequired, }) {
|
|
52
55
|
const [qty, setQty] = useState({});
|
|
53
56
|
const [isSaving, setIsSaving] = useState(false);
|
|
54
57
|
const [error, setError] = useState(null);
|
|
@@ -64,6 +67,32 @@ export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAu
|
|
|
64
67
|
const selectedTotal = selected.reduce((sum, s) => sum + s.member.price * s.quantity, 0);
|
|
65
68
|
const selectedUnits = selected.reduce((sum, s) => sum + s.quantity, 0);
|
|
66
69
|
const currency = members[0]?.currency ?? "INR";
|
|
70
|
+
/*
|
|
71
|
+
* The SAME predicate the server applies, so the button's enabled state and
|
|
72
|
+
* the API's answer cannot disagree about what a valid selection is. A
|
|
73
|
+
* server-only rule reads as a broken button; a client-only one is not a rule.
|
|
74
|
+
*/
|
|
75
|
+
const violations = useMemo(() => checkGroupRules(rules, selected.map((s) => ({
|
|
76
|
+
productId: s.member.id,
|
|
77
|
+
quantity: s.quantity,
|
|
78
|
+
title: s.member.title,
|
|
79
|
+
}))), [rules, selected]);
|
|
80
|
+
/*
|
|
81
|
+
* The discount, shown but NOT the number charged.
|
|
82
|
+
*
|
|
83
|
+
* 🛑 This is display arithmetic over a quoted percentage; the authoritative
|
|
84
|
+
* price is computed server-side inside `deriveLinePrice`, the single writer
|
|
85
|
+
* of a grouped line's `price`. Showing it here is honest (it is the figure
|
|
86
|
+
* the buyer is being offered) and computing it here is unavoidable (there is
|
|
87
|
+
* no cart line yet) — but it must never become a second money rule. The 2dp
|
|
88
|
+
* rounding deliberately matches `deriveLinePrice`'s so the two agree to the
|
|
89
|
+
* paisa rather than drifting by a rounding step.
|
|
90
|
+
*/
|
|
91
|
+
const discountPercent = Math.min(100, Math.max(0, rules.groupDiscountPercent ?? 0));
|
|
92
|
+
const discountedTotal = discountPercent > 0
|
|
93
|
+
? Math.round(selectedTotal * (1 - discountPercent / 100) * 100) / 100
|
|
94
|
+
: selectedTotal;
|
|
95
|
+
const ruleNotes = useMemo(() => describeGroupRules(rules), [rules]);
|
|
67
96
|
const handleAdd = async () => {
|
|
68
97
|
setError(null);
|
|
69
98
|
setIsSaving(true);
|
|
@@ -101,8 +130,17 @@ export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAu
|
|
|
101
130
|
};
|
|
102
131
|
return (_jsxs(Stack, { gap: "sm", children: [isCrossStore && (_jsx(Alert, { variant: "info", children: "These items are sold by different sellers, so they can't be combined into one cart line. Open an item to buy it on its own." })), _jsx(Div, { className: __O.xAuto, children: _jsxs(Table, { className: "text-left min-w-[440px]", children: [_jsx(Thead, { children: _jsxs(Tr, { border: "default", children: [_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Image" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Name" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Price" }), !isCrossStore && (_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Qty" })), _jsx(Th, { paddingY: "sm", paddingSide: "pb-sm", color: "muted", size: "xs", weight: "semibold" })] }) }), _jsx(Tbody, { children: members.map((m) => {
|
|
103
132
|
const blocked = blockedReason(m);
|
|
104
|
-
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: _jsx(GatedPrice, { children: formatCurrency(m.price, m.currency ?? currency) }) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0,
|
|
105
|
-
|
|
133
|
+
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: _jsx(GatedPrice, { children: formatCurrency(m.price, m.currency ?? currency) }) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0,
|
|
134
|
+
/*
|
|
135
|
+
* `undefined` means uncapped — `QuantityStepper`
|
|
136
|
+
* only disables its increment when a `max` is given.
|
|
137
|
+
* A made-to-order member has no ceiling; the line's
|
|
138
|
+
* own 99-per-line cap still applies at the cart.
|
|
139
|
+
*/
|
|
140
|
+
max: m.madeToOrder === true ? undefined : m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx("a", { href: memberHref(m), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
|
|
141
|
+
}) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [ruleNotes.length > 0 && (_jsx(Text, { size: "xs", color: "muted", children: ruleNotes.join(" · ") })), error && _jsx(Alert, { variant: "error", children: error }), selectedUnits > 0 && violations.length > 0 && (_jsx(Alert, { variant: "warning", children: _jsx(Stack, { gap: "dense", children: violations.map((v) => (_jsx(Text, { size: "sm", children: v.message }, v.code + v.message))) }) })), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [_jsxs(Text, { size: "sm", color: "muted", children: [selectedUnits > 0
|
|
106
142
|
? `${selectedUnits} item${selectedUnits === 1 ? "" : "s"}`
|
|
107
|
-
: "Nothing selected yet", selectedUnits > 0 && (_jsx(PricesOnly, { children:
|
|
143
|
+
: "Nothing selected yet", selectedUnits > 0 && (_jsx(PricesOnly, { children: discountPercent > 0
|
|
144
|
+
? ` · ${formatCurrency(discountedTotal, currency)} (${discountPercent}% off ${formatCurrency(selectedTotal, currency)})`
|
|
145
|
+
: ` · ${formatCurrency(selectedTotal, currency)}` }))] }), _jsx(Button, { action: ACTIONS.CART["add-group-to-cart"], disabled: selectedUnits === 0 || violations.length > 0, isLoading: isSaving, onClick: handleAdd })] })] }))] }));
|
|
108
146
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { GroupRules } from "../../../_internal/shared/features/products/group-rules";
|
|
2
3
|
export interface GroupSettingsPanelProps {
|
|
3
4
|
productId: string;
|
|
4
5
|
productSlug: string;
|
|
@@ -7,8 +8,17 @@ export interface GroupSettingsPanelProps {
|
|
|
7
8
|
groupParentSlug?: string;
|
|
8
9
|
groupChildSlugs?: string[];
|
|
9
10
|
groupTitle?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The group's current selection rules, for seeding the form.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 Seeded from the PARENT PRODUCT the editor is already showing — not
|
|
15
|
+
* re-fetched. The rules live on the parent and this panel is mounted from the
|
|
16
|
+
* parent's own editor, so the values are already in hand; a second read would
|
|
17
|
+
* be a second source of truth for the same five fields.
|
|
18
|
+
*/
|
|
19
|
+
groupRules?: GroupRules;
|
|
10
20
|
isAuction?: boolean;
|
|
11
21
|
storeProductsEndpoint: string;
|
|
12
22
|
onGroupChanged: () => void;
|
|
13
23
|
}
|
|
14
|
-
export declare function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, isAuction, storeProductsEndpoint, onGroupChanged, }: GroupSettingsPanelProps): React.JSX.Element | null;
|
|
24
|
+
export declare function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, groupRules, isAuction, storeProductsEndpoint, onGroupChanged, }: GroupSettingsPanelProps): React.JSX.Element | null;
|
|
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
4
4
|
import React, { useEffect, useState } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { Button, Code, ConfirmDeleteModal, Div, Heading, Modal, Row, SideDrawer, Span, Stack, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast } from "../../../ui/index.js";
|
|
6
|
+
import { Button, Checkbox, Code, ConfirmDeleteModal, Div, Grid, Heading, Input, Modal, Row, SideDrawer, Span, Stack, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast } from "../../../ui/index.js";
|
|
7
7
|
import { FormShellContext, useFormShellState, FormErrorSummary } from "../../../ui/forms/index.js";
|
|
8
|
+
import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
8
9
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
10
|
import { apiClient } from "../../../http/index.js";
|
|
10
11
|
import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
@@ -14,6 +15,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names.js";
|
|
|
14
15
|
import { annotate } from "../../shell/field-ui-meta.js";
|
|
15
16
|
import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell/index.js";
|
|
16
17
|
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
|
|
18
|
+
import { productImageUrls } from "../schemas/firestore.js";
|
|
17
19
|
const groupTitleSchema = z.object({
|
|
18
20
|
groupTitle: annotate(z.string().min(1, "Group title is required"), {
|
|
19
21
|
section: "group", sectionLabel: "Group", sectionRequired: true, quick: true,
|
|
@@ -50,7 +52,7 @@ const CONDITION_OPTIONS = [
|
|
|
50
52
|
{ value: "graded", label: "Graded" },
|
|
51
53
|
{ value: "refurbished", label: "Refurbished" },
|
|
52
54
|
];
|
|
53
|
-
export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, isAuction, storeProductsEndpoint, onGroupChanged, }) {
|
|
55
|
+
export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupParent, groupParentSlug, groupChildSlugs, groupTitle, groupRules, isAuction, storeProductsEndpoint, onGroupChanged, }) {
|
|
54
56
|
const { showToast } = useToast();
|
|
55
57
|
const [open, setOpen] = useState(false);
|
|
56
58
|
const [loading, setLoading] = useState(false);
|
|
@@ -64,6 +66,21 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
|
|
|
64
66
|
});
|
|
65
67
|
const [linkTargets, setLinkTargets] = useState([]);
|
|
66
68
|
const [children, setChildren] = useState(null);
|
|
69
|
+
/*
|
|
70
|
+
* Rule drafts as STRINGS, not numbers. An empty input has to be
|
|
71
|
+
* distinguishable from zero — "" means "no limit" and `0` would mean "pick at
|
|
72
|
+
* most none" — and a numeric state cannot express the empty case without a
|
|
73
|
+
* separate null, which is the same thing with extra steps.
|
|
74
|
+
*/
|
|
75
|
+
const [ruleDraft, setRuleDraft] = useState({
|
|
76
|
+
min: groupRules?.groupMinMembers?.toString() ?? "",
|
|
77
|
+
max: groupRules?.groupMaxMembers?.toString() ?? "",
|
|
78
|
+
perMember: groupRules?.groupMaxUnitsPerMember?.toString() ?? "",
|
|
79
|
+
discount: groupRules?.groupDiscountPercent?.toString() ?? "",
|
|
80
|
+
});
|
|
81
|
+
/** Per-member stock edits, keyed by product id. Only touched rows are sent. */
|
|
82
|
+
const [stockDraft, setStockDraft] = useState({});
|
|
83
|
+
const [motDraft, setMotDraft] = useState({});
|
|
67
84
|
const [confirmAction, setConfirmAction] = useState(null);
|
|
68
85
|
const titleSections = React.useMemo(() => buildSectionsFromSchema(groupTitleSchema), []);
|
|
69
86
|
const titleNav = useSectionFormNav(titleSections, titleDraft, { scope: "store:group-title" });
|
|
@@ -97,6 +114,77 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
|
|
|
97
114
|
setLoading(false);
|
|
98
115
|
}
|
|
99
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Save the selection rules.
|
|
119
|
+
*
|
|
120
|
+
* 🛑 An empty input sends `null`, not an omitted key — "no limit" has to be
|
|
121
|
+
* expressible. The route writes only keys PRESENT in the body, so omitting
|
|
122
|
+
* one means "leave it alone", which is the opposite of what clearing a field
|
|
123
|
+
* means. Those are two different intents and the body has to carry both.
|
|
124
|
+
*/
|
|
125
|
+
async function saveRules() {
|
|
126
|
+
setLoading(true);
|
|
127
|
+
try {
|
|
128
|
+
const n = (v) => (v.trim() === "" ? null : Number(v));
|
|
129
|
+
await apiClient.patch(groupEndpoint, {
|
|
130
|
+
groupMinMembers: n(ruleDraft.min),
|
|
131
|
+
groupMaxMembers: n(ruleDraft.max),
|
|
132
|
+
groupMaxUnitsPerMember: n(ruleDraft.perMember),
|
|
133
|
+
groupDiscountPercent: n(ruleDraft.discount),
|
|
134
|
+
});
|
|
135
|
+
showToast("Group rules saved.", "success");
|
|
136
|
+
onGroupChanged();
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
void normalizeError(e);
|
|
140
|
+
showToast(e?.message ?? "Failed to save group rules.", "error");
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
setLoading(false);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Save stock and made-to-order across the group.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 Sends ONLY the rows the admin touched. Sending every member would make
|
|
150
|
+
* an untouched draft child's stock a write on every save — and the route
|
|
151
|
+
* refuses an entry that names neither field anyway, so a blanket send would
|
|
152
|
+
* 400 as soon as one row was left alone.
|
|
153
|
+
*/
|
|
154
|
+
async function saveGroupStock() {
|
|
155
|
+
const touched = new Set([...Object.keys(stockDraft), ...Object.keys(motDraft)]);
|
|
156
|
+
if (touched.size === 0) {
|
|
157
|
+
showToast("Nothing changed.", "info");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
setLoading(true);
|
|
161
|
+
try {
|
|
162
|
+
const res = (await apiClient.patch(`${groupEndpoint}/stock`, {
|
|
163
|
+
members: [...touched].map((productId) => ({
|
|
164
|
+
productId,
|
|
165
|
+
...(stockDraft[productId] !== undefined && stockDraft[productId].trim() !== ""
|
|
166
|
+
? { stockQuantity: Number(stockDraft[productId]) }
|
|
167
|
+
: {}),
|
|
168
|
+
...(motDraft[productId] !== undefined ? { madeToOrder: motDraft[productId] } : {}),
|
|
169
|
+
})),
|
|
170
|
+
}));
|
|
171
|
+
const rejected = res.data?.rejected?.length ?? 0;
|
|
172
|
+
showToast(rejected > 0
|
|
173
|
+
? `Saved, but ${rejected} listing${rejected === 1 ? "" : "s"} could not be updated.`
|
|
174
|
+
: "Group stock saved.", rejected > 0 ? "warning" : "success");
|
|
175
|
+
setStockDraft({});
|
|
176
|
+
setMotDraft({});
|
|
177
|
+
void loadChildren();
|
|
178
|
+
onGroupChanged();
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
void normalizeError(e);
|
|
182
|
+
showToast(e?.message ?? "Failed to save group stock.", "error");
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
setLoading(false);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
100
188
|
async function startGroup() {
|
|
101
189
|
setLoading(true);
|
|
102
190
|
try {
|
|
@@ -264,7 +352,8 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
|
|
|
264
352
|
setOpen(next);
|
|
265
353
|
if (next && isGroupParent && children === null)
|
|
266
354
|
loadChildren();
|
|
267
|
-
}, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: titleSections, values: titleDraft, onChange: (partial) => setTitleDraft((d) => ({ ...d, ...partial })), onSubmit: () => void saveTitle(), onValidationChange: () => titleForm.validate(titleDraft), schema: groupTitleSchema, openIds: titleNav.openIds, onOpenChange: titleNav.setOpenIds, submitLabel: "Save title", isLoading: loading })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [child.images
|
|
355
|
+
}, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: titleSections, values: titleDraft, onChange: (partial) => setTitleDraft((d) => ({ ...d, ...partial })), onSubmit: () => void saveTitle(), onValidationChange: () => titleForm.validate(titleDraft), schema: groupTitleSchema, openIds: titleNav.openIds, onOpenChange: titleNav.setOpenIds, submitLabel: "Save title", isLoading: loading })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: "Selection rules" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "groupMinMembers", label: "Min items in a selection", type: "number", value: ruleDraft.min, onChange: (v) => setRuleDraft((d) => ({ ...d, min: v })), hint: "Blank = no minimum" }), _jsx(FieldInput, { name: "groupMaxMembers", label: "Max items in a selection", type: "number", value: ruleDraft.max, onChange: (v) => setRuleDraft((d) => ({ ...d, max: v })), hint: "Blank = no maximum" }), _jsx(FieldInput, { name: "groupMaxUnitsPerMember", label: "Max units of any one item", type: "number", value: ruleDraft.perMember, onChange: (v) => setRuleDraft((d) => ({ ...d, perMember: v })), hint: "Blank = no per-item cap" }), _jsx(FieldInput, { name: "groupDiscountPercent", label: "Group discount (%)", type: "number", value: ruleDraft.discount, onChange: (v) => setRuleDraft((d) => ({ ...d, discount: v })), hint: "Off the selection subtotal. Applies to new cart lines; a line already in a cart keeps the price it was quoted and is re-checked at checkout." })] }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", className: "mt-2", onClick: () => void saveRules(), isLoading: loading, children: "Save rules" })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), _jsx(Text, { className: "mb-2", color: "faint", size: "xs", children: "Each listing has its own stock. Editing here saves them together \u2014 it does not pool them." }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [productImageUrls(child.images)[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: productImageUrls(child.images)[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), child.status && child.status !== "published" ? (_jsx(Span, { size: "xs", surface: "warning-surface", className: "text-warning", padding: "pill-2xs", rounded: "default", children: child.status })) : null, (motDraft[child.id] ?? child.madeToOrder) === true ? (_jsx(Text, { size: "xs", color: "muted", children: "Made to order" })) : (_jsx(Input, { type: "number", "aria-label": `Stock for ${child.title}`, className: "w-20", value: stockDraft[child.id] ??
|
|
356
|
+
(child.availableQuantity ?? 0).toString(), onChange: (e) => setStockDraft((d) => ({ ...d, [child.id]: e.target.value })) })), _jsx(Checkbox, { label: "MTO", "aria-label": `Made to order for ${child.title}`, checked: motDraft[child.id] ?? child.madeToOrder === true, onChange: (e) => setMotDraft((d) => ({ ...d, [child.id]: e.target.checked })) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] })), (children ?? []).length > 0 && (_jsx(Button, { type: "button", variant: "secondary", size: "sm", className: "mt-2", onClick: () => void saveGroupStock(), isLoading: loading, children: "Save group stock" }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (() => {
|
|
268
357
|
const content = (_jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, storeProductsEndpoint: storeProductsEndpoint, linkTargets: linkTargets, setLinkTargets: setLinkTargets, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }));
|
|
269
358
|
const close = () => setShowAddModal(false);
|
|
270
359
|
return (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: close, title: "Add child listing", children: content })) : (_jsx(Modal, { open: showAddModal, onClose: close, title: "Add child listing", size: "lg", children: content }));
|
|
@@ -131,5 +131,5 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
|
|
|
131
131
|
price: pricePerEntry,
|
|
132
132
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
133
133
|
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 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 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 })] })] }));
|
|
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
135
|
}
|
|
@@ -12,7 +12,21 @@ export interface PrizeDrawEntryActionsProps {
|
|
|
12
12
|
* Client buy panel for a prize-draw detail page (SB4-G).
|
|
13
13
|
*
|
|
14
14
|
* - Enter Draw → NonRefundableConsentModal (listingType="prize-draw")
|
|
15
|
-
* - On confirm → add
|
|
15
|
+
* - On confirm → add the chosen number of entries to the cart, then /user/cart
|
|
16
16
|
* - "View RNG source" link → prizeGithubFileUrl
|
|
17
|
+
*
|
|
18
|
+
* 🛑 THIS FLOW DID NOT WORK AT ALL UNTIL 2026-10-19, and the docstring above
|
|
19
|
+
* described the behaviour it was supposed to have. `handleConfirm` pushed to
|
|
20
|
+
* `/checkout?directItem=<slug>&type=prize-draw` — and NOTHING anywhere reads
|
|
21
|
+
* `directItem` (CheckoutRouteClient reads only `coupon` and `lane`), and
|
|
22
|
+
* nothing added to the cart. A buyer pressing "Enter draw" landed on a
|
|
23
|
+
* checkout of whatever was already in their basket, with no entry in it.
|
|
24
|
+
* Buying an entry was impossible.
|
|
25
|
+
*
|
|
26
|
+
* 🛑 MULTI-ENTRY NEEDED NO SERVER WORK. `prizeDrawRule.splitMultipleOrders`
|
|
27
|
+
* already chunks a quantity into batches of PRIZE_DRAW_MAX_REVEALS_PER_ORDER
|
|
28
|
+
* (7 entries → [3,3,1]), `enforceMaxPerUserForCart` already caps per buyer, and
|
|
29
|
+
* the rule's preflight already checks `prizeMaxEntries`. The whole server half
|
|
30
|
+
* was written and unreachable because the client never sent a quantity.
|
|
17
31
|
*/
|
|
18
32
|
export declare function PrizeDrawEntryActions({ productId, productSlug, title, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }: PrizeDrawEntryActionsProps): import("react").JSX.Element;
|