@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
|
@@ -10,6 +10,16 @@ const CLS_ICON_ROW = "w-4 h-4 flex-shrink-0 text-[var(--appkit-color-text-muted)
|
|
|
10
10
|
const CLS_TRUNCATE = "font-medium truncate";
|
|
11
11
|
import { Button, Div, Input, Li, Row, Select, Span, Stack, Text, Ul } from "../../../ui/index.js";
|
|
12
12
|
import { useNavSuggestions } from "../hooks/useNavSuggestions.js";
|
|
13
|
+
import { useSiteSearch } from "../hooks/useSiteSearch.js";
|
|
14
|
+
/**
|
|
15
|
+
* The dropdown renders no group headings, so it needs no labels — but
|
|
16
|
+
* `useSiteSearch` requires one because `/search` does.
|
|
17
|
+
*
|
|
18
|
+
* 🛑 Declared at MODULE scope, not inline. It is a `useMemo` dependency
|
|
19
|
+
* inside the hook, and an inline arrow is a new identity on every render,
|
|
20
|
+
* which would re-run the scan on every keystroke of an unchanged query.
|
|
21
|
+
*/
|
|
22
|
+
const identityLabel = (typeKey) => typeKey;
|
|
13
23
|
const DEFAULT_TYPE_ICONS = {
|
|
14
24
|
page: "📄",
|
|
15
25
|
category: "🗂️",
|
|
@@ -28,7 +38,7 @@ function SuggestionItem({ suggestion, itemIndex, activeIndex, onSelect, onHover,
|
|
|
28
38
|
const isActive = activeIndex === itemIndex;
|
|
29
39
|
return (_jsx(Li, { children: _jsxs(Button, { type: "button", variant: "ghost", onClick: () => onSelect(suggestion), onMouseEnter: () => onHover(itemIndex), className: `w-full flex items-center gap-[var(--appkit-space-3)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] text-left transition-colors border-b border-[var(--appkit-color-border)] last:border-b-0 ${isActive ? "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]" : CLS_HOVER_ROW}`, children: [_jsx(Span, { size: "sm", children: typeIcons[suggestion.type] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", className: CLS_TRUNCATE, children: suggestion.title }), suggestion.subtitle && (_jsx(Text, { variant: "secondary", size: "xs", className: "truncate", children: suggestion.subtitle }))] }), _jsx(Span, { size: "xs", className: `${typeBadges[suggestion.type]}`, rounded: "full", padding: "pill-xs", children: suggestion.type })] }) }));
|
|
30
40
|
}
|
|
31
|
-
export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, placeholder, deferred = true, debounceMs = 300, onClear, className, router, quickLinks = [], labels, suggestionTypeIcons, suggestionTypeBadges, resourceTypes, defaultResourceType = "products", storageKey = "letitrip_search_type", }) {
|
|
41
|
+
export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, placeholder, deferred = true, debounceMs = 300, onClear, className, router, quickLinks = [], labels, suggestionTypeIcons, suggestionTypeBadges, resourceTypes, defaultResourceType = "products", storageKey = "letitrip_search_type", searchIndexEnabled, }) {
|
|
32
42
|
const isInlineMode = value !== undefined;
|
|
33
43
|
const [query, setQuery] = useState(isInlineMode ? value : "");
|
|
34
44
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
@@ -37,7 +47,33 @@ export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, pla
|
|
|
37
47
|
const inputRef = useRef(null);
|
|
38
48
|
const debounceRef = useRef(null);
|
|
39
49
|
const inlineBlurRef = useRef(null);
|
|
40
|
-
|
|
50
|
+
/*
|
|
51
|
+
* 🛑 BOTH hooks are called unconditionally — React's rules leave no choice,
|
|
52
|
+
* and it costs nothing here because each one is self-gating.
|
|
53
|
+
*
|
|
54
|
+
* `useSiteSearch` is gated on `active`: it issues no request until the user
|
|
55
|
+
* focuses or types, and then exactly one, for the whole page session.
|
|
56
|
+
* `useNavSuggestions` is gated on its own 3-character minimum AND on
|
|
57
|
+
* `remoteQuery` below, which is blank whenever the index is answering — so
|
|
58
|
+
* the fallback path makes no network request at all in the normal case.
|
|
59
|
+
*/
|
|
60
|
+
const [searchIntent, setSearchIntent] = useState(false);
|
|
61
|
+
const local = useSiteSearch(query, {
|
|
62
|
+
active: searchIntent,
|
|
63
|
+
indexEnabled: searchIndexEnabled,
|
|
64
|
+
label: identityLabel,
|
|
65
|
+
});
|
|
66
|
+
/*
|
|
67
|
+
* The index is authoritative the moment it is `ready`. `loading` must NOT
|
|
68
|
+
* fall through to the remote path: that would fire the very round trips
|
|
69
|
+
* this replaces, for the one second before the local answer arrives, on
|
|
70
|
+
* every first keystroke.
|
|
71
|
+
*/
|
|
72
|
+
const useLocal = local.status === "ready";
|
|
73
|
+
const remoteQuery = useLocal || local.status === "loading" ? "" : query;
|
|
74
|
+
const { suggestions: remoteSuggestions, isLoading: remoteLoading } = useNavSuggestions(remoteQuery, selectedType);
|
|
75
|
+
const suggestions = useLocal ? local.suggestions : remoteSuggestions;
|
|
76
|
+
const suggestionsLoading = useLocal ? false : local.status === "loading" || remoteLoading;
|
|
41
77
|
const typeIcons = { ...DEFAULT_TYPE_ICONS, ...suggestionTypeIcons };
|
|
42
78
|
const typeBadges = { ...DEFAULT_TYPE_BADGES, ...suggestionTypeBadges };
|
|
43
79
|
// Restore persisted type from localStorage after mount
|
|
@@ -90,6 +126,7 @@ export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, pla
|
|
|
90
126
|
setActiveIndex(-1);
|
|
91
127
|
}, [isOpen, isInlineMode, query, suggestions.length]);
|
|
92
128
|
const handleInlineChange = (nextValue) => {
|
|
129
|
+
setSearchIntent(true);
|
|
93
130
|
setQuery(nextValue);
|
|
94
131
|
if (!deferred) {
|
|
95
132
|
if (debounceRef.current)
|
|
@@ -209,6 +246,10 @@ export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, pla
|
|
|
209
246
|
}
|
|
210
247
|
};
|
|
211
248
|
return (_jsxs(Row, { className: `relative ${className ?? ""}`, gap: "sm", children: [_jsxs(Row, { className: "relative flex-1 min-w-0", children: [_jsx("svg", { className: "absolute left-3 w-4 h-4 text-zinc-400 pointer-events-none", "aria-hidden": "true", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: SVG_PATH_SEARCH }) }), _jsx(Input, { ref: inputRef, bare: true, type: "search", value: query, onChange: (event) => handleInlineChange(event.target.value), onKeyDown: handleInlineKeyDown, onFocus: () => {
|
|
249
|
+
// Focus is the earliest honest signal of intent, and it buys
|
|
250
|
+
// the ~900 ms the artifact takes to land before the first
|
|
251
|
+
// keystroke needs an answer.
|
|
252
|
+
setSearchIntent(true);
|
|
212
253
|
if (inlineBlurRef.current)
|
|
213
254
|
clearTimeout(inlineBlurRef.current);
|
|
214
255
|
setIsInlineOpen(true);
|
|
@@ -292,7 +333,10 @@ export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, pla
|
|
|
292
333
|
};
|
|
293
334
|
if (!isOpen)
|
|
294
335
|
return null;
|
|
295
|
-
return (_jsx(Div, { border: "bottom-subtle", className: "animate-[fadeIn_150ms_ease-out]", surface: "default", shadow: "md", children: _jsxs(Div, { className: "container mx-auto md:py-[1rem] relative", padding: "inline", children: [_jsxs(Row, { gap: "sm", className: "md:gap-[0.75rem]", children: [_jsx(Div, { className: "flex-1 relative min-w-0", children: _jsx(Input, { ref: inputRef, bare: true, type: "search", placeholder: placeholder ?? labels.placeholder, value: query,
|
|
336
|
+
return (_jsx(Div, { border: "bottom-subtle", className: "animate-[fadeIn_150ms_ease-out]", surface: "default", shadow: "md", children: _jsxs(Div, { className: "container mx-auto md:py-[1rem] relative", padding: "inline", children: [_jsxs(Row, { gap: "sm", className: "md:gap-[0.75rem]", children: [_jsx(Div, { className: "flex-1 relative min-w-0", children: _jsx(Input, { ref: inputRef, bare: true, type: "search", placeholder: placeholder ?? labels.placeholder, value: query, onFocus: () => setSearchIntent(true), onChange: (event) => {
|
|
337
|
+
setSearchIntent(true);
|
|
338
|
+
setQuery(event.target.value);
|
|
339
|
+
}, onKeyDown: handleOverlayKeyDown, className: "w-full rounded-lg border bg-[var(--appkit-color-surface)] text-zinc-900 placeholder:text-zinc-400 focus:border-primary focus:ring-2 focus:ring-primary/20 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:text-white dark:placeholder:text-slate-500" }) }), resourceTypes && resourceTypes.length > 0 && (_jsx(Select, { options: resourceTypes, value: selectedType, onValueChange: handleTypeChange, "aria-label": labels.resourceTypeLabel ?? "Search in", wrapperClassName: "flex-shrink-0 max-w-[92px] sm:max-w-[150px]" })), _jsxs(Button, { gap: "md", onClick: handleOverlaySearch, variant: "primary", size: "md", className: "hidden sm:", children: [_jsx("svg", { className: "w-5 h-5 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: SVG_PATH_SEARCH }) }), labels.title] }), _jsx(Button, { rounded: "xl", variant: "ghost", onClick: () => onClose?.(), className: "p-[var(--appkit-space-2-5)] md:p-[var(--appkit-space-3)] transition-colors text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-[var(--appkit-color-text)] flex-shrink-0", "aria-label": labels.closeAriaLabel, children: _jsx("svg", { className: "w-6 h-6 md:w-7 md:h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), (filteredSiteLinks.length > 0 || query) && (_jsxs(Stack, { className: "absolute top-full left-0 right-0 z-50 pt-[0.5rem] pb-[1rem]", padding: "x-md", gap: "sm", children: [filteredSiteLinks.length > 0 && !suggestionsLoading && (_jsxs(Div, { className: `${__O.hidden}`, rounded: "xl", shadow: "lg", surface: "default", border: "default", children: [_jsx(Div, { border: "default", className: "border-b", paddingY: "y-xs", paddingX: "x-md", children: _jsx(Text, { variant: "secondary", size: "xs", className: "uppercase tracking-wider", weight: "semibold", children: labels.quickLinks }) }), _jsx(Ul, { children: filteredSiteLinks.map((link) => {
|
|
296
340
|
const Icon = link.icon;
|
|
297
341
|
const itemIndex = quickLinkItems.findIndex((item) => item.link.href === link.href);
|
|
298
342
|
const isActive = activeIndex === itemIndex;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FacetCount } from "../../../_internal/shared/search/to-groups";
|
|
3
|
+
export interface SearchFacetRailProps {
|
|
4
|
+
facets: readonly FacetCount[];
|
|
5
|
+
/** Currently applied `k:v` pairs. */
|
|
6
|
+
selected: readonly string[];
|
|
7
|
+
onToggle: (pair: string) => void;
|
|
8
|
+
/** Values shown per key before the rest are hidden. */
|
|
9
|
+
maxPerKey?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function SearchFacetRail({ facets, selected, onToggle, maxPerKey, }: SearchFacetRailProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/*
|
|
4
|
+
* WHY: Refining a 3,700-record local result set is free; refining a Firestore
|
|
5
|
+
* one is an aggregation query per facet value plus a composite index per
|
|
6
|
+
* shape. This rail only exists because Tier 1 made it cost nothing.
|
|
7
|
+
* WHAT: Toggleable `k:v` facet chips with live counts over the CURRENT hits.
|
|
8
|
+
*
|
|
9
|
+
* ## 🛑 Counts are over the current result set, not the catalogue
|
|
10
|
+
*
|
|
11
|
+
* Tick one chip and the others shrink, truthfully, because every number is a
|
|
12
|
+
* reduce over the hits the query just produced. `BaseRepository.facetCounts`
|
|
13
|
+
* cannot do this — it counts the collection, one aggregation query per value,
|
|
14
|
+
* and returns `undefined` on a missing index so the chip renders with no
|
|
15
|
+
* number at all. A count that can silently vanish is worse than no rail.
|
|
16
|
+
*
|
|
17
|
+
* ## 🛑 A chip whose count would be zero is not rendered
|
|
18
|
+
*
|
|
19
|
+
* It cannot be: the counts come from the hits, so a value with no hits has no
|
|
20
|
+
* entry. That is the structural version of Root Cause #72's rule — there, a
|
|
21
|
+
* hide-when-empty tab survived because its count map returned `undefined`
|
|
22
|
+
* ("unknown") rather than `0`, so four tabs were unhideable. Here there is no
|
|
23
|
+
* count map to be incomplete.
|
|
24
|
+
*
|
|
25
|
+
* @tag domain:search
|
|
26
|
+
* @tag layer:feature
|
|
27
|
+
* @tag pattern:none
|
|
28
|
+
* @tag access:client
|
|
29
|
+
* @tag consumers:SearchResultsClient
|
|
30
|
+
* @tag sideEffects:none
|
|
31
|
+
*/
|
|
32
|
+
import React from "react";
|
|
33
|
+
import { Button, Div, Row, Span, Stack, Text } from "../../../ui/index.js";
|
|
34
|
+
/**
|
|
35
|
+
* Short code -> heading.
|
|
36
|
+
*
|
|
37
|
+
* 🛑 `Partial`, and an unknown code is SKIPPED rather than shown raw. The
|
|
38
|
+
* codes are two letters to keep the artifact small (`lt:auction` over
|
|
39
|
+
* `listingType:auction`, ×3,700), which means a new one added on the builder
|
|
40
|
+
* side would otherwise surface to a visitor as a heading reading "sz". A
|
|
41
|
+
* facet nobody can name is not a refinement, and dropping it loses nothing
|
|
42
|
+
* the groups above do not already show.
|
|
43
|
+
*/
|
|
44
|
+
const FACET_KEY_LABEL = {
|
|
45
|
+
lt: "Type",
|
|
46
|
+
br: "Brand",
|
|
47
|
+
cn: "Condition",
|
|
48
|
+
fr: "Franchise",
|
|
49
|
+
ct: "Category",
|
|
50
|
+
};
|
|
51
|
+
/** Slug -> words. The artifact stores slugs; nobody wants to read one. */
|
|
52
|
+
function humanise(value) {
|
|
53
|
+
const spaced = value.replace(/[-_]/g, " ");
|
|
54
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
55
|
+
}
|
|
56
|
+
export function SearchFacetRail({ facets, selected, onToggle, maxPerKey = 6, }) {
|
|
57
|
+
const selectedSet = React.useMemo(() => new Set(selected), [selected]);
|
|
58
|
+
const byKey = React.useMemo(() => {
|
|
59
|
+
const map = new Map();
|
|
60
|
+
for (const f of facets) {
|
|
61
|
+
if (!FACET_KEY_LABEL[f.key])
|
|
62
|
+
continue; // see FACET_KEY_LABEL's 🛑
|
|
63
|
+
const bucket = map.get(f.key);
|
|
64
|
+
if (bucket)
|
|
65
|
+
bucket.push(f);
|
|
66
|
+
else
|
|
67
|
+
map.set(f.key, [f]);
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
}, [facets]);
|
|
71
|
+
/*
|
|
72
|
+
* A key offering ONE value is not a refinement — every result already has
|
|
73
|
+
* it, so ticking it changes nothing and the chip is pure noise. The
|
|
74
|
+
* exception is a value already selected: hiding the chip you used to narrow
|
|
75
|
+
* the page would leave no way to un-narrow it.
|
|
76
|
+
*/
|
|
77
|
+
const keys = [...byKey.keys()].filter((k) => {
|
|
78
|
+
const values = byKey.get(k) ?? [];
|
|
79
|
+
return values.length > 1 || values.some((v) => selectedSet.has(`${v.key}:${v.value}`));
|
|
80
|
+
});
|
|
81
|
+
if (keys.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
return (_jsx(Stack, { gap: "sm", children: keys.map((key) => {
|
|
84
|
+
const values = byKey.get(key) ?? [];
|
|
85
|
+
const shown = values.slice(0, maxPerKey);
|
|
86
|
+
return (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", weight: "semibold", transform: "uppercase", children: FACET_KEY_LABEL[key] }), _jsxs(Row, { wrap: true, gap: "xs", children: [shown.map((f) => {
|
|
87
|
+
const pair = `${f.key}:${f.value}`;
|
|
88
|
+
const isOn = selectedSet.has(pair);
|
|
89
|
+
return (_jsxs(Button, { size: "sm", variant: isOn ? "primary" : "outline", "aria-pressed": isOn, onClick: () => onToggle(pair), children: [humanise(f.value), _jsxs(Span, { size: "xs", color: isOn ? "inherit" : "muted", numeric: true, children: [" ", f.count] })] }, pair));
|
|
90
|
+
}), values.length > shown.length && (_jsx(Div, { padding: "xs", children: _jsxs(Text, { size: "xs", color: "muted", children: ["+", values.length - shown.length, " more"] }) }))] })] }, key));
|
|
91
|
+
}) }));
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { SearchEverythingResult } from "../../../_internal/server/features/search/search-everything";
|
|
3
|
+
export interface SearchResultsClientProps {
|
|
4
|
+
/** The query, resolved server-side from `?q=`. */
|
|
5
|
+
q: string;
|
|
6
|
+
/**
|
|
7
|
+
* The Tier 2 result. Empty (and unread) whenever `indexEnabled` is true —
|
|
8
|
+
* see the header.
|
|
9
|
+
*/
|
|
10
|
+
initialResult: SearchEverythingResult;
|
|
11
|
+
indexEnabled: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a session exists. FALSE for a guest, and that is what makes
|
|
14
|
+
* "signed out issues zero authenticated requests" verifiable in the
|
|
15
|
+
* Network panel rather than merely true on the server.
|
|
16
|
+
*/
|
|
17
|
+
isSignedIn?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* `typeKey` -> heading.
|
|
20
|
+
*
|
|
21
|
+
* 🛑 A plain OBJECT, not a `(key: string) => string`. This component is
|
|
22
|
+
* rendered from a Server Component, and a function prop is not
|
|
23
|
+
* serialisable across the RSC boundary — it throws at render, not at
|
|
24
|
+
* compile. Same constraint `HomepageSectionBoundary` documents for its
|
|
25
|
+
* `sectionId`/`sectionType` strings.
|
|
26
|
+
*
|
|
27
|
+
* A key with no entry falls back to the key itself rather than being
|
|
28
|
+
* dropped: a group headed `scammer` is ugly, a group that silently
|
|
29
|
+
* disappears is a bug.
|
|
30
|
+
*/
|
|
31
|
+
labels: Readonly<Record<string, string>>;
|
|
32
|
+
}
|
|
33
|
+
export declare function SearchResultsClient({ q, initialResult, indexEnabled, isSignedIn, labels, }: SearchResultsClientProps): React.JSX.Element;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/*
|
|
4
|
+
* WHY: `/search` should answer from the local artifact — every type, mid-word
|
|
5
|
+
* matching, exact counts, zero Firestore reads — while still rendering
|
|
6
|
+
* something when the artifact is off or unreachable.
|
|
7
|
+
* WHAT: The client wrapper around `SearchResultsPageView`. Picks the tier,
|
|
8
|
+
* owns the facet selection, and renders the rail.
|
|
9
|
+
*
|
|
10
|
+
* ## 🛑 The server result is a FALLBACK, not a first paint to be replaced
|
|
11
|
+
*
|
|
12
|
+
* `/search/page.tsx` does NOT run the Firestore fan-out when the index is
|
|
13
|
+
* enabled — it passes an empty envelope and 0 reads. So `initialResult` is
|
|
14
|
+
* meaningful only in the kill-switch case, and swapping a populated server
|
|
15
|
+
* result out from under the user is a flicker that cannot happen here.
|
|
16
|
+
*
|
|
17
|
+
* That is also why `loading` renders a waiting line rather than the empty
|
|
18
|
+
* state: with no server result behind it, an empty state would say "nothing
|
|
19
|
+
* matched" about a query nobody has finished asking (the `GatedPrice`
|
|
20
|
+
* three-state discipline, one surface over).
|
|
21
|
+
*
|
|
22
|
+
* ## 🛑 Facet selection lives in component state, NOT the URL
|
|
23
|
+
*
|
|
24
|
+
* `/search` is `noindex` and every result links out to a real indexable
|
|
25
|
+
* listing page, so a shareable faceted search URL has no SEO value and would
|
|
26
|
+
* multiply the crawl trap the page is already careful about. It also keeps
|
|
27
|
+
* `useUrlTable` out of a page that has no pagination — the one thing the
|
|
28
|
+
* listing views need it for.
|
|
29
|
+
*
|
|
30
|
+
* @tag domain:search
|
|
31
|
+
* @tag layer:feature
|
|
32
|
+
* @tag pattern:none
|
|
33
|
+
* @tag access:client
|
|
34
|
+
* @tag consumers:app/[locale]/search/page.tsx
|
|
35
|
+
* @tag sideEffects:network
|
|
36
|
+
*/
|
|
37
|
+
import React from "react";
|
|
38
|
+
import { Text } from "../../../ui/index.js";
|
|
39
|
+
import { useSiteSearch } from "../hooks/useSiteSearch.js";
|
|
40
|
+
import { mergeSearchTiers, useScopedSearch } from "../hooks/useScopedSearch.js";
|
|
41
|
+
import { SearchFacetRail } from "./SearchFacetRail.js";
|
|
42
|
+
import { SearchResultsPageView } from "./SearchResultsPageView.js";
|
|
43
|
+
export function SearchResultsClient({ q, initialResult, indexEnabled, isSignedIn = false, labels, }) {
|
|
44
|
+
const [selectedFacets, setSelectedFacets] = React.useState([]);
|
|
45
|
+
// Stable across renders — it is a `useMemo` dependency inside the hook.
|
|
46
|
+
const label = React.useCallback((typeKey) => labels[typeKey] ?? typeKey, [labels]);
|
|
47
|
+
/*
|
|
48
|
+
* `active` is unconditionally true here, unlike the header. Arriving on
|
|
49
|
+
* `/search?q=…` IS the intent — there is no cheaper signal to wait for,
|
|
50
|
+
* and gating on focus would mean a page whose entire purpose is results
|
|
51
|
+
* sits idle until somebody clicks the box.
|
|
52
|
+
*/
|
|
53
|
+
const local = useSiteSearch(q, {
|
|
54
|
+
active: true,
|
|
55
|
+
indexEnabled,
|
|
56
|
+
facets: selectedFacets,
|
|
57
|
+
label,
|
|
58
|
+
});
|
|
59
|
+
const toggleFacet = React.useCallback((pair) => {
|
|
60
|
+
setSelectedFacets((prev) => prev.includes(pair) ? prev.filter((p) => p !== pair) : [...prev, pair]);
|
|
61
|
+
}, []);
|
|
62
|
+
/*
|
|
63
|
+
* Tier 2. `submittedQuery` is `q` — the URL parameter — so it changes when
|
|
64
|
+
* the user SUBMITS, not as they type. That is the whole gating story: the
|
|
65
|
+
* local tier runs per keystroke because it is free, this one runs per
|
|
66
|
+
* navigation because it is a bounded Firestore fan-out.
|
|
67
|
+
*/
|
|
68
|
+
const scoped = useScopedSearch({ enabled: isSignedIn, submittedQuery: q });
|
|
69
|
+
const usingLocal = local.status === "ready" && local.result !== null;
|
|
70
|
+
const baseResult = usingLocal && local.result ? local.result : initialResult;
|
|
71
|
+
const result = React.useMemo(() => scoped.groups.length > 0
|
|
72
|
+
? { ...baseResult, groups: mergeSearchTiers(baseResult.groups, scoped.groups) }
|
|
73
|
+
: baseResult, [baseResult, scoped.groups]);
|
|
74
|
+
/*
|
|
75
|
+
* 🛑 A dropped facet selection must not silently keep filtering.
|
|
76
|
+
*
|
|
77
|
+
* If the index fails after the user has ticked chips, the fallback result
|
|
78
|
+
* is unfiltered — so the chips would be lit while doing nothing. Clearing
|
|
79
|
+
* them is the honest state, and it is cheap because the rail is derived
|
|
80
|
+
* rather than stored.
|
|
81
|
+
*/
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!usingLocal && selectedFacets.length > 0)
|
|
84
|
+
setSelectedFacets([]);
|
|
85
|
+
}, [usingLocal, selectedFacets.length]);
|
|
86
|
+
const waiting = local.status === "loading" && q.trim().length > 0;
|
|
87
|
+
return (_jsx(SearchResultsPageView, { result: waiting ? { ...result, groups: [], totalHits: 0 } : result, footnote: usingLocal && local.builtAt
|
|
88
|
+
? `Catalogue index updated ${new Date(local.builtAt).toLocaleString()}.`
|
|
89
|
+
: undefined, renderAboveGroups: () => (_jsxs(_Fragment, { children: [waiting && _jsx(Text, { color: "muted", children: "Searching\u2026" }), usingLocal && (_jsx(SearchFacetRail, { facets: local.facets, selected: selectedFacets, onToggle: toggleFacet })), scoped.status === "failed" && (
|
|
90
|
+
/*
|
|
91
|
+
* Said out loud, for the same reason as the line below it. A
|
|
92
|
+
* seller whose draft is missing because the scoped fetch failed
|
|
93
|
+
* will read the silence as "that draft does not exist".
|
|
94
|
+
*/
|
|
95
|
+
_jsx(Text, { size: "sm", color: "muted", children: "Couldn't search your account \u2014 results below are public listings only." })), local.status === "failed" && (
|
|
96
|
+
/*
|
|
97
|
+
* Said out loud rather than degraded silently. `searchEverything`'s
|
|
98
|
+
* own header records that a degraded source "is indistinguishable
|
|
99
|
+
* from 'nothing matched'" — and on a page that otherwise rendered
|
|
100
|
+
* fine, silence tells someone we have none of what they asked for.
|
|
101
|
+
*/
|
|
102
|
+
_jsx(Text, { size: "sm", color: "muted", children: "Showing a reduced set \u2014 the full catalogue index could not be loaded." }))] })) }));
|
|
103
|
+
}
|
|
@@ -29,5 +29,19 @@ export interface SearchResultsPageViewProps {
|
|
|
29
29
|
result: SearchEverythingResult;
|
|
30
30
|
/** Rendered above the groups — the consumer owns the input control. */
|
|
31
31
|
renderSearchInput?: () => React.ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Rendered between the input and the groups. The facet rail lives here.
|
|
34
|
+
*
|
|
35
|
+
* A SLOT rather than a `facets` prop, because this component is also
|
|
36
|
+
* rendered from a Server Component with no facets at all (the kill-switch
|
|
37
|
+
* fallback), and a required-looking prop that is absent half the time
|
|
38
|
+
* invites a `?? []` that silently renders an empty rail.
|
|
39
|
+
*/
|
|
40
|
+
renderAboveGroups?: () => React.ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* A short line under the heading — "results as of 10:51". Optional because
|
|
43
|
+
* the server tier has no build time to report.
|
|
44
|
+
*/
|
|
45
|
+
footnote?: string;
|
|
32
46
|
}
|
|
33
|
-
export declare function SearchResultsPageView({ result, renderSearchInput, }: SearchResultsPageViewProps): React.JSX.Element;
|
|
47
|
+
export declare function SearchResultsPageView({ result, renderSearchInput, renderAboveGroups, footnote, }: SearchResultsPageViewProps): React.JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
3
3
|
import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink } from "../../../ui/index.js";
|
|
4
|
-
export function SearchResultsPageView({ result, renderSearchInput, }) {
|
|
4
|
+
export function SearchResultsPageView({ result, renderSearchInput, renderAboveGroups, footnote, }) {
|
|
5
5
|
const { q, terms, groups, totalHits } = result;
|
|
6
6
|
const hasQuery = q.trim().length > 0;
|
|
7
7
|
/*
|
|
@@ -18,5 +18,5 @@ export function SearchResultsPageView({ result, renderSearchInput, }) {
|
|
|
18
18
|
const normalisedAway = hasQuery && terms.length === 0;
|
|
19
19
|
return (_jsx(Section, { padding: "page", children: _jsx(Container, { children: _jsxs(Stack, { gap: "loose", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, children: hasQuery ? `Search: ${q}` : "Search" }), hasQuery && !normalisedAway && (_jsx(Text, { color: "muted", size: "sm", children: totalHits === 0
|
|
20
20
|
? "Nothing matched."
|
|
21
|
-
: `${totalHits}${groups.some((g) => g.truncated) ? "+" : ""} result${totalHits === 1 ? "" : "s"} across ${groups.length} section${groups.length === 1 ? "" : "s"}.` }))] }), renderSearchInput?.(), !hasQuery && (_jsx(Text, { color: "muted", children: "Search listings, categories, brands, stores, articles and help pages at once." })), normalisedAway && (_jsx(Text, { color: "muted", children: "That search had no searchable words in it. Try a product, part or category name." })), groups.map((group) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsxs(Heading, { level: 2, size: "lg", children: [group.label, group.truncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", group.hits.length, "+)"] }))] }), _jsxs(TextLink, { href: group.moreHref, children: ["See all ", group.label.toLowerCase(), " \u2192"] })] }), _jsx(Stack, { gap: "xs", children: group.hits.map((hit) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", children: [_jsx(TextLink, { href: hit.href, children: hit.title }), hit.detail && (_jsx(Text, { size: "xs", color: "muted", children: hit.detail }))] }, `${group.key}-${hit.id}`))) })] }, group.key))), hasQuery && !normalisedAway && totalHits === 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { color: "muted", children: ["No listings, categories, brands, stores, articles or help pages matched \u201C", q, "\u201D."] }), _jsx(TextLink, { href: ROUTES.PUBLIC.PRODUCTS, children: "Browse all listings \u2192" })] }))] }) }) }));
|
|
21
|
+
: `${totalHits}${groups.some((g) => g.truncated) ? "+" : ""} result${totalHits === 1 ? "" : "s"} across ${groups.length} section${groups.length === 1 ? "" : "s"}.` }))] }), footnote && (_jsx(Text, { size: "xs", color: "muted", children: footnote })), renderSearchInput?.(), renderAboveGroups?.(), !hasQuery && (_jsx(Text, { color: "muted", children: "Search listings, categories, brands, stores, articles and help pages at once." })), normalisedAway && (_jsx(Text, { color: "muted", children: "That search had no searchable words in it. Try a product, part or category name." })), groups.map((group) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsxs(Heading, { level: 2, size: "lg", children: [group.label, group.truncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", group.hits.length, "+)"] }))] }), _jsxs(TextLink, { href: group.moreHref, children: ["See all ", group.label.toLowerCase(), " \u2192"] })] }), _jsx(Stack, { gap: "xs", children: group.hits.map((hit) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", children: [_jsx(TextLink, { href: hit.href, children: hit.title }), hit.detail && (_jsx(Text, { size: "xs", color: "muted", children: hit.detail }))] }, `${group.key}-${hit.id}`))) })] }, group.key))), hasQuery && !normalisedAway && totalHits === 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { color: "muted", children: ["No listings, categories, brands, stores, articles or help pages matched \u201C", q, "\u201D."] }), _jsx(TextLink, { href: ROUTES.PUBLIC.PRODUCTS, children: "Browse all listings \u2192" })] }))] }) }) }));
|
|
22
22
|
}
|
|
@@ -1,8 +1,2 @@
|
|
|
1
1
|
export { Search } from "./Search";
|
|
2
2
|
export type { SearchLabels, SearchProps, SearchQuickLink, SearchRouterAdapter, SearchResourceType, SearchResourceTypeOption, } from "./Search";
|
|
3
|
-
export { SearchFiltersRow } from "./SearchFiltersRow";
|
|
4
|
-
export type {} from "./SearchFiltersRow";
|
|
5
|
-
export { SearchResultsSection } from "./SearchResultsSection";
|
|
6
|
-
export type { SearchResultsSectionProps } from "./SearchResultsSection";
|
|
7
|
-
export { SearchView } from "./SearchView";
|
|
8
|
-
export type { SearchViewProps } from "./SearchView";
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export interface NavSuggestionRecord {
|
|
2
2
|
objectID: string;
|
|
3
3
|
type: "page" | "category" | "blog" | "event" | "product";
|
|
4
|
+
/**
|
|
5
|
+
* The real index type key (`auction`, `part`, `faq`, `scammer`, …) when the
|
|
6
|
+
* row came from the local search artifact, which spans 21 types.
|
|
7
|
+
*
|
|
8
|
+
* 🛑 `type` above stays a 5-value union on purpose. It is what the
|
|
9
|
+
* dropdown's `DEFAULT_TYPE_ICONS` / `DEFAULT_TYPE_BADGES` maps switch on,
|
|
10
|
+
* so widening it would compile everywhere and render 16 types unstyled
|
|
11
|
+
* (Root Cause #20). `useSiteSearch.suggestionTypeFor` collapses the key
|
|
12
|
+
* into that union for presentation and puts the precise one here, so a
|
|
13
|
+
* badge can be specific where a map entry exists without any caller
|
|
14
|
+
* breaking where one does not.
|
|
15
|
+
*/
|
|
16
|
+
typeKey?: string;
|
|
4
17
|
title: string;
|
|
5
18
|
subtitle?: string;
|
|
6
19
|
url: string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SearchGroup } from "../../../_internal/server/features/search/search-everything";
|
|
2
|
+
export type ScopedSearchStatus = "idle" | "loading" | "ready" | "failed";
|
|
3
|
+
export interface UseScopedSearchResult {
|
|
4
|
+
readonly status: ScopedSearchStatus;
|
|
5
|
+
readonly groups: readonly SearchGroup[];
|
|
6
|
+
}
|
|
7
|
+
export interface UseScopedSearchOptions {
|
|
8
|
+
/** False for a guest. No request is issued at all. */
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
/** The committed query. Changing it re-fetches; typing does not. */
|
|
11
|
+
submittedQuery: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function useScopedSearch({ enabled, submittedQuery, }: UseScopedSearchOptions): UseScopedSearchResult;
|
|
14
|
+
/**
|
|
15
|
+
* Merge the two tiers.
|
|
16
|
+
*
|
|
17
|
+
* ## 🛑 Dedupe by DESTINATION, and local wins
|
|
18
|
+
*
|
|
19
|
+
* The tiers are NOT disjoint: a seller's published listing is in the public
|
|
20
|
+
* artifact AND in `my-listings`. Showing both is a duplicate-results bug;
|
|
21
|
+
* dropping the public one would lose the row a buyer would see.
|
|
22
|
+
*
|
|
23
|
+
* Local wins, so what survives in the scoped group is precisely the rows the
|
|
24
|
+
* artifact does not have — a seller's DRAFTS. Labelling that group "Your
|
|
25
|
+
* listings (incl. drafts)" is what makes the dedupe legible rather than
|
|
26
|
+
* mysterious.
|
|
27
|
+
*
|
|
28
|
+
* 🛑 The key is the HREF, not `${typeKey}:${id}`. The artifact keys products
|
|
29
|
+
* by SLUG and Firestore by document id, so a composite key would never
|
|
30
|
+
* collide and the dedupe would silently do nothing — the failure mode being
|
|
31
|
+
* a page that looks fine and shows everything twice. Both tiers already
|
|
32
|
+
* agree on where a row goes, which is the only identity they share.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mergeSearchTiers(local: readonly SearchGroup[], scoped: readonly SearchGroup[]): SearchGroup[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/*
|
|
3
|
+
* WHY: A signed-in viewer's own records belong in the same box as the public
|
|
4
|
+
* catalogue — but they cost real reads and must not be fetched the way
|
|
5
|
+
* the free tier is.
|
|
6
|
+
* WHAT: Tier 2's client half, plus the merge with Tier 1.
|
|
7
|
+
*
|
|
8
|
+
* ## 🛑 ON SUBMIT, never per keystroke
|
|
9
|
+
*
|
|
10
|
+
* Tier 1 is a local scan and costs nothing, so it runs on every character.
|
|
11
|
+
* This one is a bounded Firestore fan-out — up to ~320 documents for an
|
|
12
|
+
* admin — so it runs when the user commits to a query. A debounced
|
|
13
|
+
* per-keystroke version of this endpoint is how a 50k/day read tier
|
|
14
|
+
* disappears in an afternoon.
|
|
15
|
+
*
|
|
16
|
+
* ## 🛑 A guest issues ZERO requests
|
|
17
|
+
*
|
|
18
|
+
* Gated on `enabled`, which the caller sets from the session. The server
|
|
19
|
+
* refuses too (`auth: true`, and `scopedSourcesFor(null)` is `[]`), but the
|
|
20
|
+
* client gate is what makes "signed out = no authenticated request in the
|
|
21
|
+
* Network panel" true, which is the only form of that claim anyone can
|
|
22
|
+
* verify.
|
|
23
|
+
*
|
|
24
|
+
* @tag domain:search
|
|
25
|
+
* @tag layer:feature
|
|
26
|
+
* @tag pattern:hook
|
|
27
|
+
* @tag access:client
|
|
28
|
+
* @tag consumers:SearchResultsClient
|
|
29
|
+
* @tag sideEffects:network
|
|
30
|
+
*/
|
|
31
|
+
import * as React from "react";
|
|
32
|
+
import { apiClient } from "../../../http/index.js";
|
|
33
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
34
|
+
import { SEARCH_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
35
|
+
/** Matches the server-side gate, so the two cannot disagree about what is worth asking. */
|
|
36
|
+
const MIN_CHARS = 3;
|
|
37
|
+
export function useScopedSearch({ enabled, submittedQuery, }) {
|
|
38
|
+
const [status, setStatus] = React.useState("idle");
|
|
39
|
+
const [groups, setGroups] = React.useState([]);
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
const q = submittedQuery.trim();
|
|
42
|
+
if (!enabled || q.length < MIN_CHARS) {
|
|
43
|
+
setStatus("idle");
|
|
44
|
+
setGroups([]);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
/*
|
|
48
|
+
* An `AbortController` per query, mirroring `useNavSuggestions`. Without
|
|
49
|
+
* it, a slow response to an earlier query can land after a faster
|
|
50
|
+
* response to a later one and overwrite it — the user sees results for
|
|
51
|
+
* something they already moved on from, which looks like the search
|
|
52
|
+
* returning the wrong thing rather than like a race.
|
|
53
|
+
*/
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
setStatus("loading");
|
|
56
|
+
apiClient
|
|
57
|
+
.get(SEARCH_ENDPOINTS.SCOPED(q), { signal: controller.signal })
|
|
58
|
+
.then((res) => {
|
|
59
|
+
if (controller.signal.aborted)
|
|
60
|
+
return;
|
|
61
|
+
setGroups(res?.data?.groups ?? []);
|
|
62
|
+
setStatus("ready");
|
|
63
|
+
})
|
|
64
|
+
.catch((err) => {
|
|
65
|
+
if (controller.signal.aborted)
|
|
66
|
+
return;
|
|
67
|
+
const e = normalizeError(err);
|
|
68
|
+
/*
|
|
69
|
+
* `failed`, and the CALLER must say so out loud.
|
|
70
|
+
* `searchEverything`'s own header records that a degraded source "is
|
|
71
|
+
* indistinguishable from 'nothing matched'" — and on a page where
|
|
72
|
+
* the public half rendered fine, silence tells a seller their draft
|
|
73
|
+
* does not exist.
|
|
74
|
+
*/
|
|
75
|
+
void e;
|
|
76
|
+
setGroups([]);
|
|
77
|
+
setStatus("failed");
|
|
78
|
+
});
|
|
79
|
+
return () => controller.abort();
|
|
80
|
+
}, [enabled, submittedQuery]);
|
|
81
|
+
return { status, groups };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Merge the two tiers.
|
|
85
|
+
*
|
|
86
|
+
* ## 🛑 Dedupe by DESTINATION, and local wins
|
|
87
|
+
*
|
|
88
|
+
* The tiers are NOT disjoint: a seller's published listing is in the public
|
|
89
|
+
* artifact AND in `my-listings`. Showing both is a duplicate-results bug;
|
|
90
|
+
* dropping the public one would lose the row a buyer would see.
|
|
91
|
+
*
|
|
92
|
+
* Local wins, so what survives in the scoped group is precisely the rows the
|
|
93
|
+
* artifact does not have — a seller's DRAFTS. Labelling that group "Your
|
|
94
|
+
* listings (incl. drafts)" is what makes the dedupe legible rather than
|
|
95
|
+
* mysterious.
|
|
96
|
+
*
|
|
97
|
+
* 🛑 The key is the HREF, not `${typeKey}:${id}`. The artifact keys products
|
|
98
|
+
* by SLUG and Firestore by document id, so a composite key would never
|
|
99
|
+
* collide and the dedupe would silently do nothing — the failure mode being
|
|
100
|
+
* a page that looks fine and shows everything twice. Both tiers already
|
|
101
|
+
* agree on where a row goes, which is the only identity they share.
|
|
102
|
+
*/
|
|
103
|
+
export function mergeSearchTiers(local, scoped) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const g of local)
|
|
106
|
+
for (const h of g.hits)
|
|
107
|
+
seen.add(h.href);
|
|
108
|
+
const deduped = scoped
|
|
109
|
+
.map((g) => ({ ...g, hits: g.hits.filter((h) => !seen.has(h.href)) }))
|
|
110
|
+
// A scoped group that is now empty is dropped entirely rather than
|
|
111
|
+
// rendered as a heading over nothing, which reads as a broken section.
|
|
112
|
+
.filter((g) => g.hits.length > 0);
|
|
113
|
+
return [...local, ...deduped];
|
|
114
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type FacetCount } from "../../../_internal/shared/search/to-groups";
|
|
2
|
+
import { type SearchIndexStatus } from "./useSearchIndex";
|
|
3
|
+
import type { SearchEverythingResult } from "../../../_internal/server/features/search/search-everything";
|
|
4
|
+
import type { NavSuggestionRecord } from "./useNavSuggestions";
|
|
5
|
+
export interface UseSiteSearchOptions {
|
|
6
|
+
/** Flip true on focus or first keypress. Gates the artifact fetch. */
|
|
7
|
+
active: boolean;
|
|
8
|
+
/** `siteSettings.search?.indexEnabled` — read as `!== false`. */
|
|
9
|
+
indexEnabled?: boolean;
|
|
10
|
+
/** Restrict to these index type keys. Absent means every type. */
|
|
11
|
+
types?: readonly string[];
|
|
12
|
+
/** Require every one of these `k:v` pairs. The facet rail's selection. */
|
|
13
|
+
facets?: readonly string[];
|
|
14
|
+
/** `typeKey` -> heading. See `ToGroupsOptions.label`. */
|
|
15
|
+
label: (typeKey: string) => string;
|
|
16
|
+
}
|
|
17
|
+
export interface UseSiteSearchResult {
|
|
18
|
+
readonly status: SearchIndexStatus;
|
|
19
|
+
/** Null until the index is ready; never a fabricated empty result. */
|
|
20
|
+
readonly result: SearchEverythingResult | null;
|
|
21
|
+
readonly suggestions: readonly NavSuggestionRecord[];
|
|
22
|
+
readonly facets: readonly FacetCount[];
|
|
23
|
+
/** The index build this answered from, for a "results as of" line. */
|
|
24
|
+
readonly builtAt: string | null;
|
|
25
|
+
}
|
|
26
|
+
export declare function useSiteSearch(query: string, { active, indexEnabled, types, facets, label }: UseSiteSearchOptions): UseSiteSearchResult;
|