@mohasinac/appkit 4.53.0 โ 4.53.1
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/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/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/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/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 +25 -0
- package/dist/_internal/server/features/stores/tabs.js +122 -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 +2 -1
- 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 +39 -15
- package/dist/constants/api-endpoints.js +28 -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 +4 -11
- 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 +113 -0
- package/dist/features/categories/components/BrandDetailPageView.js +19 -3
- package/dist/features/categories/components/BrandDetailTabs.js +10 -0
- package/dist/features/categories/components/CategoriesIndexListing.js +23 -8
- package/dist/features/categories/components/CategoryDetailPageView.js +141 -14
- package/dist/features/categories/components/CategoryDetailTabs.js +12 -0
- 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 +4 -9
- 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 +4 -9
- 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 +4 -9
- 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 +4 -11
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +3 -1
- package/dist/features/price-index/components/PriceIndexToolView.js +92 -13
- package/dist/features/products/components/CompareOverlay.d.ts +18 -1
- package/dist/features/products/components/CompareOverlay.js +21 -2
- 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 +91 -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 +74 -15
- 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/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 +98 -0
- package/dist/features/products/repository/products.repository.js +235 -17
- 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 +39 -0
- package/dist/features/products/utils/group-member-image.js +3 -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/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/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 +280 -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 +372 -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 +441 -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 +9 -9
- package/dist/server.js +27 -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 +135 -26
- 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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-categories โ GET /api/categories/[id]/price-guidance
|
|
3
|
+
*
|
|
4
|
+
* The seller-facing price hint `description-template/route.ts`'s own
|
|
5
|
+
* docstring names as "C1's job, from a surface that knows who is asking" โ
|
|
6
|
+
* this is that surface. It is deliberately a SEPARATE endpoint rather than a
|
|
7
|
+
* field added back to the public, CDN-cached template route: that route's
|
|
8
|
+
* whole point is that anyone can fetch it, and `priceGuidance` is withheld
|
|
9
|
+
* from signed-out visitors for the same reason a listing's own price is
|
|
10
|
+
* (CLAUDE.md ยง "Guest price gating") โ publishing the competitor-derived
|
|
11
|
+
* market band on an unauthenticated, publicly-cacheable response would be
|
|
12
|
+
* exactly the "a neighbouring surface publishes the range" leak that gate
|
|
13
|
+
* exists to close, even though the number is advisory rather than a real
|
|
14
|
+
* price.
|
|
15
|
+
*
|
|
16
|
+
* Consumer stub:
|
|
17
|
+
* ```ts
|
|
18
|
+
* // src/app/api/categories/[id]/price-guidance/route.ts
|
|
19
|
+
* export { categoryPriceGuidanceGET as GET } from "@mohasinac/appkit";
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## What this does NOT resolve
|
|
23
|
+
*
|
|
24
|
+
* `PriceGuidanceBasis.gstRate` โ "the rate that will be written onto this
|
|
25
|
+
* listing, resolved the same way `deriveTaxonomy` resolves it" โ is left
|
|
26
|
+
* `undefined` here on purpose. `deriveTaxonomy` is a private method on
|
|
27
|
+
* `ProductRepository`, tightly coupled to the product WRITE path (it also
|
|
28
|
+
* applies `DEFAULT_GST_RATE` as a last resort, which is correct for a
|
|
29
|
+
* document being saved and would be a guess presented as fact on a read-only
|
|
30
|
+
* hint). `describePriceGuidanceBasis` already handles an unresolved rate
|
|
31
|
+
* correctly and safely: for the common case (a seller whose prices already
|
|
32
|
+
* include GST โ the schema default), the rate is never consulted at all, so
|
|
33
|
+
* this omission costs nothing. Only an EXCLUSIVE-pricing seller sees the
|
|
34
|
+
* honest "could not be resolved" note instead of a converted figure โ which
|
|
35
|
+
* is the behaviour `price-guidance.ts`'s own docstring prescribes for exactly
|
|
36
|
+
* this situation, not a half-measure standing in for the real resolution.
|
|
37
|
+
*/
|
|
38
|
+
export declare const GET: (request: Request, context?: {
|
|
39
|
+
params: Promise<{
|
|
40
|
+
id: string;
|
|
41
|
+
}>;
|
|
42
|
+
} | undefined) => Promise<Response>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-categories โ GET /api/categories/[id]/price-guidance
|
|
3
|
+
*
|
|
4
|
+
* The seller-facing price hint `description-template/route.ts`'s own
|
|
5
|
+
* docstring names as "C1's job, from a surface that knows who is asking" โ
|
|
6
|
+
* this is that surface. It is deliberately a SEPARATE endpoint rather than a
|
|
7
|
+
* field added back to the public, CDN-cached template route: that route's
|
|
8
|
+
* whole point is that anyone can fetch it, and `priceGuidance` is withheld
|
|
9
|
+
* from signed-out visitors for the same reason a listing's own price is
|
|
10
|
+
* (CLAUDE.md ยง "Guest price gating") โ publishing the competitor-derived
|
|
11
|
+
* market band on an unauthenticated, publicly-cacheable response would be
|
|
12
|
+
* exactly the "a neighbouring surface publishes the range" leak that gate
|
|
13
|
+
* exists to close, even though the number is advisory rather than a real
|
|
14
|
+
* price.
|
|
15
|
+
*
|
|
16
|
+
* Consumer stub:
|
|
17
|
+
* ```ts
|
|
18
|
+
* // src/app/api/categories/[id]/price-guidance/route.ts
|
|
19
|
+
* export { categoryPriceGuidanceGET as GET } from "@mohasinac/appkit";
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## What this does NOT resolve
|
|
23
|
+
*
|
|
24
|
+
* `PriceGuidanceBasis.gstRate` โ "the rate that will be written onto this
|
|
25
|
+
* listing, resolved the same way `deriveTaxonomy` resolves it" โ is left
|
|
26
|
+
* `undefined` here on purpose. `deriveTaxonomy` is a private method on
|
|
27
|
+
* `ProductRepository`, tightly coupled to the product WRITE path (it also
|
|
28
|
+
* applies `DEFAULT_GST_RATE` as a last resort, which is correct for a
|
|
29
|
+
* document being saved and would be a guess presented as fact on a read-only
|
|
30
|
+
* hint). `describePriceGuidanceBasis` already handles an unresolved rate
|
|
31
|
+
* correctly and safely: for the common case (a seller whose prices already
|
|
32
|
+
* include GST โ the schema default), the rate is never consulted at all, so
|
|
33
|
+
* this omission costs nothing. Only an EXCLUSIVE-pricing seller sees the
|
|
34
|
+
* honest "could not be resolved" note instead of a converted figure โ which
|
|
35
|
+
* is the behaviour `price-guidance.ts`'s own docstring prescribes for exactly
|
|
36
|
+
* this situation, not a half-measure standing in for the real resolution.
|
|
37
|
+
*/
|
|
38
|
+
import { NextResponse } from "next/server.js";
|
|
39
|
+
import { normalizeError } from "../../../../../errors/normalize.js";
|
|
40
|
+
import { serverLogger } from "../../../../../monitoring/index.js";
|
|
41
|
+
import { createRouteHandler } from "../../../../../next/api/routeHandler.js";
|
|
42
|
+
import { getProviders } from "../../../../../contracts/index.js";
|
|
43
|
+
import { categoriesRepository } from "../../../repository/categories.repository.js";
|
|
44
|
+
import { isAdminUser } from "../../../../auth/role-predicates.js";
|
|
45
|
+
import { priceGuidanceOnSellerBasis, describePriceGuidanceBasis, } from "../../../../../_internal/shared/features/categories/price-guidance.js";
|
|
46
|
+
const MAX_ANCESTORS = 8;
|
|
47
|
+
/**
|
|
48
|
+
* Nearest-first ancestor walk for `priceGuidance`, same shape as
|
|
49
|
+
* `description-template/route.ts`'s `resolveInherited` โ the leaf's own band
|
|
50
|
+
* wins, and only a leaf with none falls back to the nearest ancestor that has
|
|
51
|
+
* one. ONE bounded `findByIds`, not a sequential walk: a seller form hitting
|
|
52
|
+
* this on every category change should not pay up to eight round trips for it.
|
|
53
|
+
*/
|
|
54
|
+
async function resolveInheritedGuidance(leaf) {
|
|
55
|
+
if (leaf.productDefaults?.priceGuidance)
|
|
56
|
+
return leaf.productDefaults.priceGuidance;
|
|
57
|
+
const parentIds = (leaf.parentIds ?? []).slice(0, MAX_ANCESTORS);
|
|
58
|
+
if (parentIds.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
const nearestFirst = [...parentIds].reverse();
|
|
61
|
+
const ancestors = await categoriesRepository.findByIds(nearestFirst);
|
|
62
|
+
const byId = new Map(ancestors.map((a) => [a.id, a]));
|
|
63
|
+
for (const pid of nearestFirst) {
|
|
64
|
+
const guidance = byId.get(pid)?.productDefaults?.priceGuidance;
|
|
65
|
+
if (guidance)
|
|
66
|
+
return guidance;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
export const GET = createRouteHandler({
|
|
71
|
+
roles: ["seller", "admin", "moderator"],
|
|
72
|
+
handler: async ({ user, params }) => {
|
|
73
|
+
try {
|
|
74
|
+
const categoryId = params?.id;
|
|
75
|
+
if (!categoryId) {
|
|
76
|
+
return NextResponse.json({ success: false, error: "Missing category id" }, { status: 400 });
|
|
77
|
+
}
|
|
78
|
+
const leafDoc = await categoriesRepository.findById(categoryId);
|
|
79
|
+
if (!leafDoc) {
|
|
80
|
+
return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
|
|
81
|
+
}
|
|
82
|
+
const guidance = await resolveInheritedGuidance(leafDoc);
|
|
83
|
+
if (!guidance) {
|
|
84
|
+
return NextResponse.json({ success: true, data: { guidance: null, basisNote: null } });
|
|
85
|
+
}
|
|
86
|
+
/*
|
|
87
|
+
* One read, admin exempted (no store to look up). `pricesIncludeGst`
|
|
88
|
+
* absent means `true` โ matching the schema default and the store
|
|
89
|
+
* route's own `?? true` (see `PriceGuidanceBasis.sellerIncludesGst`'s
|
|
90
|
+
* docstring for why this must stay consistent everywhere it's read).
|
|
91
|
+
*/
|
|
92
|
+
let sellerIncludesGst;
|
|
93
|
+
if (!isAdminUser(user)) {
|
|
94
|
+
const { db } = getProviders();
|
|
95
|
+
const storesRepo = db?.getRepository("stores");
|
|
96
|
+
const result = await storesRepo?.findAll({ filters: `ownerId==${user.uid}`, pageSize: 1 });
|
|
97
|
+
sellerIncludesGst = result?.data[0]?.pricesIncludeGst;
|
|
98
|
+
}
|
|
99
|
+
const basis = { sellerIncludesGst, gstRate: undefined };
|
|
100
|
+
const onSellerBasis = priceGuidanceOnSellerBasis(guidance, basis);
|
|
101
|
+
const basisNote = describePriceGuidanceBasis(basis);
|
|
102
|
+
return NextResponse.json({ success: true, data: { guidance: onSellerBasis, basisNote } }, {
|
|
103
|
+
// Per-seller basis, so never shared across callers.
|
|
104
|
+
headers: { "Cache-Control": "private, max-age=3600" },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const e = normalizeError(err);
|
|
109
|
+
serverLogger.error("price-guidance read failed", { message: e.message });
|
|
110
|
+
return NextResponse.json({ success: false, error: "Could not load price guidance" }, { status: 500 });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -7,6 +7,7 @@ import { ROUTES } from "../../../next/routing/route-map.js";
|
|
|
7
7
|
import { Anchor, Container, Div, Dl, Dt, Dd, Heading, Main, Nav, Section, Span, Stack, Text } from "../../../ui/index.js";
|
|
8
8
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
|
|
9
9
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
10
|
+
import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
|
|
10
11
|
import { BrandDetailTabs } from "./BrandDetailTabs.js";
|
|
11
12
|
import { CategoryGrid } from "./CategoryGrid.js";
|
|
12
13
|
import { CategoryHighlightsAndFaqSection } from "./CategoryHighlightsAndFaqSection.js";
|
|
@@ -27,8 +28,23 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
|
|
|
27
28
|
// The brand IS this page's subject โ a read failure must surface rather than
|
|
28
29
|
// render an anonymous, empty brand page. `undefined` still means "no such
|
|
29
30
|
// brand slug".
|
|
30
|
-
const
|
|
31
|
-
|
|
31
|
+
const brandDoc = initialBrand ?? (await categoriesRepository.getCategoryBySlug(slug)) ?? undefined;
|
|
32
|
+
const brand = (brandDoc ?? undefined);
|
|
33
|
+
/*
|
|
34
|
+
* ๐ Read from the DOCUMENT, not from the `CategoryItem` cast โ the same rule
|
|
35
|
+
* `CategoryDetailPageView` states for this exact field.
|
|
36
|
+
*
|
|
37
|
+
* `contentBody` is deliberately PRIVATE on the list projection: it is 400-900
|
|
38
|
+
* chars of HTML per row and `CategoryInlineSelect` fetches 200 rows on every
|
|
39
|
+
* seller-form open, so publishing it there would be the Fast-Origin-Transfer
|
|
40
|
+
* shape of Root Cause #104. Widening `CategoryItem` to satisfy one render site
|
|
41
|
+
* is exactly the invitation that comment warns about โ and it is what the
|
|
42
|
+
* first attempt at this did, which `tsc` caught.
|
|
43
|
+
*
|
|
44
|
+
* No extra read: this page already holds the whole document (`initialBrand`,
|
|
45
|
+
* or the fetch above). Only the local cast hid it.
|
|
46
|
+
*/
|
|
47
|
+
const brandContentBody = typeof brandDoc?.contentBody === "string" ? brandDoc.contentBody : null;
|
|
32
48
|
const brandName = brand?.name;
|
|
33
49
|
const [productsResult, tabCounts, allBundles, activeBrands, groupedListings, settings] = await Promise.all([
|
|
34
50
|
brandName
|
|
@@ -152,5 +168,5 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
|
|
|
152
168
|
const ldFaq = brand?.faqs && brand.faqs.length > 0
|
|
153
169
|
? faqJsonLd(brand.faqs.map((f) => ({ question: f.question, answer: f.answer })))
|
|
154
170
|
: null;
|
|
155
|
-
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [_jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: `${hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, rounded: "full", padding: "pill-sm-tall", children: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
|
|
171
|
+
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [_jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: `${hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, rounded: "full", padding: "pill-sm-tall", children: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), brandContentBody && brandContentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: brandContentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
|
|
156
172
|
}
|
|
@@ -35,6 +35,16 @@ const GENERIC_TAB_LISTING_TYPES = {
|
|
|
35
35
|
[pluginFor("digital-code").tabSlug]: ["digital-code"],
|
|
36
36
|
[pluginFor("live").tabSlug]: ["live"],
|
|
37
37
|
[pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
|
|
38
|
+
/*
|
|
39
|
+
* Spares render generically โ a product grid is the whole story, unlike an
|
|
40
|
+
* auction (countdown, bid history) or a prize draw (entries remaining).
|
|
41
|
+
*
|
|
42
|
+
* ๐ Hand-written map; a missing entry is a tab you can CLICK that renders an
|
|
43
|
+
* empty panel. `audit-tab-body-coverage` exists for exactly this, after this
|
|
44
|
+
* file silently DROPPED four types while its `CategoryDetailTabs` twin
|
|
45
|
+
* rendered them as blank panels โ two spellings of one gap.
|
|
46
|
+
*/
|
|
47
|
+
[pluginFor("spares").tabSlug]: ["spares"],
|
|
38
48
|
};
|
|
39
49
|
export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
40
50
|
const countFor = (id) => counts?.[id];
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
4
4
|
import { useCategoriesFiltered } from "../hooks/useCategories.js";
|
|
5
5
|
import { CATEGORY_TABS, DEFAULT_CATEGORY_TAB, matchesCategoryTab, } from "../constants/category-tabs.js";
|
|
6
6
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
|
-
import {
|
|
7
|
+
import { Div, Grid, ListingFilterDrawer, ListingToolbar, OverflowRow, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui/index.js";
|
|
8
8
|
import { CategoryCard } from "./CategoryGrid.js";
|
|
9
9
|
import { CategoryFilters } from "./CategoryFilters.js";
|
|
10
10
|
import { useUrlTable } from "../../../react/hooks/useUrlTable.js";
|
|
@@ -170,13 +170,28 @@ export function CategoriesIndexListing({ brandsOnly = false }) {
|
|
|
170
170
|
pageSize: PAGE_SIZE,
|
|
171
171
|
});
|
|
172
172
|
const activeSearch = table.get(TABLE_KEYS.QUERY) || "";
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
/*
|
|
174
|
+
* ๐ `<OverflowRow>`, not a hand-rolled row of ghost Buttons.
|
|
175
|
+
*
|
|
176
|
+
* This was five `<Button variant="ghost">`s in a `<Row>` with a hand-written
|
|
177
|
+
* underline โ no `role="tablist"`, no `role="tab"`, and no overflow
|
|
178
|
+
* behaviour at all, so on a narrow viewport the last tabs simply ran past
|
|
179
|
+
* the right edge with no scrollbar and no affordance. The Known-TS-Patterns
|
|
180
|
+
* table bans exactly that shape ("A hand-rolled overflow-x-auto row of
|
|
181
|
+
* pills/tabs โ <OverflowRow>"), and the primitive built for it had shipped
|
|
182
|
+
* three weeks earlier without this surface being migrated.
|
|
183
|
+
*
|
|
184
|
+
* `variant="tabs"` because these swap a panel in place โ they rewrite the
|
|
185
|
+
* `tab` URL param and re-partition one already-cached payload; they do not
|
|
186
|
+
* navigate. That is also why the items carry `onSelect` rather than `href`,
|
|
187
|
+
* and therefore why `linkProps`/`scroll` do not apply here.
|
|
188
|
+
*/
|
|
189
|
+
const tabItems = CATEGORY_TABS.map((tab) => ({
|
|
190
|
+
id: tab.key,
|
|
191
|
+
label: tab.label,
|
|
192
|
+
onSelect: () => switchTab(tab.key),
|
|
193
|
+
}));
|
|
194
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [!brandsOnly && (_jsx(Div, { className: "mb-2", children: _jsx(OverflowRow, { items: tabItems, activeId: activeTab, variant: "tabs", ariaLabel: "Category index sections", searchPlaceholder: "Search sections\u2026" }) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
|
|
180
195
|
{ label: "Featured only", active: table.get(TABLE_KEYS.IS_FEATURED) === "true", onChange: (next) => table.set(TABLE_KEYS.IS_FEATURED, next ? "true" : "") },
|
|
181
196
|
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryGrid({ isLoading, categories, view, activeSearch, activeTab, brandsOnly }) }), renderCategoryFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
|
|
182
197
|
}
|
|
@@ -7,8 +7,8 @@ import { categoriesRepository, productRepository, storeRepository, } from "../..
|
|
|
7
7
|
import { Container, Div, Heading, ImageMosaic, Main, MOSAIC_MIN, Section, Span, SplitTile, Stack, Text } from "../../../ui/index.js";
|
|
8
8
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
9
|
import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
|
|
10
|
-
import { OverflowRow } from "../../../ui/index.js";
|
|
11
10
|
import { resolveTaxonomyVisual } from "../utils/taxonomy-visual.js";
|
|
11
|
+
import { SubcategoryCardRow } from "./SubcategoryCardRow.js";
|
|
12
12
|
import { CategoryDetailTabs } from "./CategoryDetailTabs.js";
|
|
13
13
|
import { CategoryGrid } from "./CategoryGrid.js";
|
|
14
14
|
import { CategoryHighlightsAndFaqSection } from "./CategoryHighlightsAndFaqSection.js";
|
|
@@ -49,17 +49,73 @@ const __W = "max-w-7xl mx-auto";
|
|
|
49
49
|
* Admin surfaces that genuinely need both still show both โ
|
|
50
50
|
* `CategoryTableColumns` renders "own (total)" explicitly.
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
/*
|
|
53
|
+
* ๐ products + auctions, SUMMED.
|
|
54
|
+
*
|
|
55
|
+
* `metrics.productCount` EXCLUDES auctions โ `Tally` keeps `productIds` and
|
|
56
|
+
* `auctionIds` as separate arrays and writes `auctionCount` /
|
|
57
|
+
* `totalAuctionCount` alongside, so the comparable "how much is in here"
|
|
58
|
+
* figure is the sum of both. Reading only the product half understated every
|
|
59
|
+
* subcategory holding an auction, and understated it silently: the chip
|
|
60
|
+
* showed a plausible smaller number rather than anything that looked wrong.
|
|
61
|
+
*
|
|
62
|
+
* This page already derives its own header figures correctly that way
|
|
63
|
+
* (`productCount` and `auctionCount` resolved separately further down); this
|
|
64
|
+
* was the one place that took the product half for the whole.
|
|
65
|
+
*
|
|
66
|
+
* Prefer the rollup (`total*`) over the own-count for both halves, because a
|
|
67
|
+
* chip is a navigational promise about the whole subtree you are about to
|
|
68
|
+
* enter, not about what happens to be filed at that exact node. Shared by
|
|
69
|
+
* `toSubcategoryItems` (the chip's badge count) and the scroller's own
|
|
70
|
+
* filter below, so the two can't disagree about what "has listings" means.
|
|
71
|
+
*/
|
|
72
|
+
function listingCountFor(child) {
|
|
73
|
+
const products = child.metrics?.totalProductCount ?? child.metrics?.productCount ?? 0;
|
|
74
|
+
const auctions = child.metrics?.totalAuctionCount ?? child.metrics?.auctionCount ?? 0;
|
|
75
|
+
return products + auctions;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Direct children โ the Subcategory card row's items.
|
|
79
|
+
*
|
|
80
|
+
* `descendants` is the SAME array the page already fetched, passed in so each
|
|
81
|
+
* card can carry its own direct children's names for the filter. One level
|
|
82
|
+
* only โ see `SubcategoryCardRow`'s header for why matching the whole subtree
|
|
83
|
+
* would surface cards for names the visitor never sees.
|
|
84
|
+
*/
|
|
85
|
+
function toSubcategoryItems(children, descendants) {
|
|
86
|
+
/*
|
|
87
|
+
* Grandchildren indexed by parent in ONE pass. The alternative โ filtering
|
|
88
|
+
* `descendants` inside the map โ is O(children x descendants), and on the
|
|
89
|
+
* spinning-tops root that is 5 x ~1,200 per render for a list that never
|
|
90
|
+
* changes.
|
|
91
|
+
*
|
|
92
|
+
* ๐ Keyed on `parentIds[last]`, the IMMEDIATE parent. `parentIds` holds the
|
|
93
|
+
* full ancestor chain, so testing `.includes(child.id)` would match every
|
|
94
|
+
* descendant at any depth and quietly turn the one-level rule into a
|
|
95
|
+
* whole-subtree match.
|
|
96
|
+
*/
|
|
97
|
+
const childNamesByParent = new Map();
|
|
98
|
+
for (const d of descendants) {
|
|
99
|
+
const parent = d.parentIds?.[d.parentIds.length - 1];
|
|
100
|
+
if (!parent)
|
|
101
|
+
continue;
|
|
102
|
+
const list = childNamesByParent.get(parent);
|
|
103
|
+
if (list)
|
|
104
|
+
list.push(d.name);
|
|
105
|
+
else
|
|
106
|
+
childNamesByParent.set(parent, [d.name]);
|
|
107
|
+
}
|
|
53
108
|
return children.map((child) => {
|
|
54
|
-
const count = child
|
|
109
|
+
const count = listingCountFor(child);
|
|
55
110
|
return {
|
|
56
111
|
id: child.id,
|
|
57
|
-
|
|
112
|
+
name: child.name,
|
|
58
113
|
// Omitted rather than rendered as 0 โ `count` is "not counted" when
|
|
59
|
-
// undefined, and a
|
|
114
|
+
// undefined, and a card reading "0" invites a click that lands on an
|
|
60
115
|
// empty page.
|
|
61
116
|
count: count > 0 ? count : undefined,
|
|
62
117
|
href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)),
|
|
118
|
+
childNames: childNamesByParent.get(child.id) ?? [],
|
|
63
119
|
...resolveTaxonomyVisual(child),
|
|
64
120
|
};
|
|
65
121
|
});
|
|
@@ -216,9 +272,76 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
216
272
|
})
|
|
217
273
|
: Promise.resolve([]),
|
|
218
274
|
]);
|
|
275
|
+
// ๐ SAME TIER ONLY. `rootSiblingCategories` spans the whole root subtree โ
|
|
276
|
+
// siblings AND cousins across every depth โ so without a tier check this
|
|
277
|
+
// rendered deeper descendants (children, grandchildren) as "related",
|
|
278
|
+
// which reads as a second, redundant Subcategories row rather than a set
|
|
279
|
+
// of actual peers. A category with no siblings at its own tier correctly
|
|
280
|
+
// shows nothing here; the tier-grouped descendant tabs above already cover
|
|
281
|
+
// "what's below this category".
|
|
219
282
|
const relatedCategories = rootSiblingCategories
|
|
220
|
-
.filter((c) => c.id !== category?.id &&
|
|
283
|
+
.filter((c) => c.id !== category?.id &&
|
|
284
|
+
(!c.categoryType || c.categoryType === "category") &&
|
|
285
|
+
c.tier === category?.tier)
|
|
221
286
|
.slice(0, RELATED_CATEGORY_LIMIT);
|
|
287
|
+
// ๐ ONLY children that actually have something in them. A direct child
|
|
288
|
+
// with zero listings anywhere in its own subtree is a dead end โ clicking
|
|
289
|
+
// it opens an empty category page โ so it's dropped from the chip row
|
|
290
|
+
// rather than rendered with no count badge. `listingCountFor` reads the
|
|
291
|
+
// ROLLUP (own + descendants), so a child that merely has no listings of
|
|
292
|
+
// its OWN but does further down is correctly kept.
|
|
293
|
+
const directChildrenWithListings = childCategories
|
|
294
|
+
.filter((c) => c.tier === (category?.tier ?? 0) + 1)
|
|
295
|
+
.filter((c) => listingCountFor(c) > 0);
|
|
296
|
+
/*
|
|
297
|
+
* The ancestor chain as cards, root first.
|
|
298
|
+
*
|
|
299
|
+
* ๐ `ancestor.id` is used as the slug, which is correct HERE and is not a
|
|
300
|
+
* general licence: categories are one of this codebase's "pure slug"
|
|
301
|
+
* collections (`id === slug`), so the two are interchangeable for a category
|
|
302
|
+
* row. `CategoryAncestor` carries only `{id, name, tier}` โ no slug field โ
|
|
303
|
+
* so the alternative would be a lookup per ancestor for a value already in
|
|
304
|
+
* hand.
|
|
305
|
+
*
|
|
306
|
+
* `tier` comes from the stored ancestor rather than the array index: the
|
|
307
|
+
* index is only the depth if the chain is complete, and a chain that skipped
|
|
308
|
+
* a tier would silently relabel every level below it (the closure-skip case
|
|
309
|
+
* CLAUDE.md records under the counters invariant).
|
|
310
|
+
*/
|
|
311
|
+
/*
|
|
312
|
+
* `CategoryAncestor` is `{id, name, tier}` and carries no display block, so
|
|
313
|
+
* the artwork is looked up in rows this page ALREADY fetched โ
|
|
314
|
+
* `rootSiblingCategories` spans the whole root subtree, which is where an
|
|
315
|
+
* ancestor lives. A miss simply yields no visual and the card falls back,
|
|
316
|
+
* which is why this is a plain lookup and not another read.
|
|
317
|
+
*/
|
|
318
|
+
const rowsById = new Map();
|
|
319
|
+
for (const c of [...rootSiblingCategories, ...childCategories]) {
|
|
320
|
+
if (c?.id)
|
|
321
|
+
rowsById.set(c.id, c);
|
|
322
|
+
}
|
|
323
|
+
/*
|
|
324
|
+
* ๐ The IMMEDIATE parent only โ one card, never the chain.
|
|
325
|
+
*
|
|
326
|
+
* `ancestors` is ordered root-first, so the last entry is the direct parent
|
|
327
|
+
* and everything before it is a grandparent. Rendering the whole ladder put
|
|
328
|
+
* four cards on a tier-4 page for a path the breadcrumb at the top of the
|
|
329
|
+
* page already spells out in full, and the uncles/grandparents are not
|
|
330
|
+
* places you step to from here โ the one move this row needs to afford is
|
|
331
|
+
* "up one".
|
|
332
|
+
*/
|
|
333
|
+
const immediateParent = (category?.ancestors ?? []).at(-1);
|
|
334
|
+
const parentLadder = immediateParent
|
|
335
|
+
? [
|
|
336
|
+
{
|
|
337
|
+
id: immediateParent.id,
|
|
338
|
+
name: immediateParent.name,
|
|
339
|
+
href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(immediateParent.id)),
|
|
340
|
+
level: immediateParent.tier,
|
|
341
|
+
...resolveTaxonomyVisual(rowsById.get(immediateParent.id) ?? null),
|
|
342
|
+
},
|
|
343
|
+
]
|
|
344
|
+
: [];
|
|
222
345
|
// Stores tab โ stores whose storeCategory is this category or ANY descendant.
|
|
223
346
|
//
|
|
224
347
|
// `storeCategory` is a single slug with no ancestor chain (unlike a product's
|
|
@@ -282,7 +405,16 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
282
405
|
* and it cast `createdAt` through `as unknown as string` where the adapter
|
|
283
406
|
* converts a Timestamp properly.
|
|
284
407
|
*/
|
|
285
|
-
.map((s) => toStoreListItem(s))
|
|
408
|
+
.map((s) => toStoreListItem(s))
|
|
409
|
+
// ๐ `storeCategory` is a one-time "primarily sells this" tag, not proof
|
|
410
|
+
// of current stock โ a store can match the category and still have
|
|
411
|
+
// nothing listed right now (the exact shape of the empty-store report
|
|
412
|
+
// this filter closes). Listing it here sends a visitor into a store page
|
|
413
|
+
// with nothing to buy, so only stores that actually have products are
|
|
414
|
+
// kept. `totalProducts` is the denormalised `stats.totalProducts`
|
|
415
|
+
// counter, incremented atomically on create/delete โ cheap to read here,
|
|
416
|
+
// unlike a per-store query.
|
|
417
|
+
.filter((s) => (s.totalProducts ?? 0) > 0);
|
|
286
418
|
const storeCount = categoryStores.length;
|
|
287
419
|
// `tabCounts` is keyed by tabSlug and already covers every listing type plus
|
|
288
420
|
// bundles; `stores` is added here because this page resolves its store list
|
|
@@ -430,7 +562,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
430
562
|
const ldFaq = category?.faqs && category.faqs.length > 0
|
|
431
563
|
? faqJsonLd(category.faqs.map((f) => ({ question: f.question, answer: f.answer })))
|
|
432
564
|
: null;
|
|
433
|
-
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
|
|
565
|
+
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
|
|
434
566
|
if (category?.tier !== 0)
|
|
435
567
|
return null;
|
|
436
568
|
const branches = childCategories.filter((c) => c.tier === 1);
|
|
@@ -455,10 +587,5 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
455
587
|
imageUrl: coverOf(b),
|
|
456
588
|
countLabel: countOf(b),
|
|
457
589
|
} }) }) }));
|
|
458
|
-
})(),
|
|
459
|
-
const directChildren = childCategories.filter((c) => c.tier === (category?.tier ?? 0) + 1);
|
|
460
|
-
if (directChildren.length === 0)
|
|
461
|
-
return null;
|
|
462
|
-
return (_jsxs(Div, { padding: "b-2xs", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", className: "mb-[var(--appkit-space-1-5)]", children: "Subcategories" }), _jsx(OverflowRow, { items: toSubcategoryItems(directChildren), ariaLabel: "Subcategories", variant: "nav", placeholder: "Browse subcategories", searchPlaceholder: "Search subcategories\u2026", noResultsText: "No matching subcategory" })] }));
|
|
463
|
-
})() }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
|
|
590
|
+
})(), directChildrenWithListings.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "inline", children: _jsxs(Div, { padding: "b-2xs", children: [parentLadder.length > 0 && (_jsx(Div, { padding: "b-2xs", children: _jsx(SubcategoryCardRow, { items: parentLadder, label: "Parent category" }) })), _jsx(SubcategoryCardRow, { items: toSubcategoryItems(directChildrenWithListings, childCategories) })] }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) }) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Categories" }), _jsx(CategoryGrid, { categories: relatedCategories, getHref: (c) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(c.slug)) })] }) }))] }));
|
|
464
591
|
}
|
|
@@ -37,6 +37,18 @@ const GENERIC_TAB_LISTING_TYPES = {
|
|
|
37
37
|
[pluginFor("live").tabSlug]: ["live"],
|
|
38
38
|
// The combined Art & Stickers tab spans both types under the `art` slug.
|
|
39
39
|
[pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
|
|
40
|
+
/*
|
|
41
|
+
* Spares render generically โ there is nothing type-specific to show beyond
|
|
42
|
+
* a product grid, unlike an auction (countdown, bid history) or a prize draw
|
|
43
|
+
* (entries remaining).
|
|
44
|
+
*
|
|
45
|
+
* ๐ This map is hand-written and a missing entry is a tab you can CLICK that
|
|
46
|
+
* renders an empty panel. It is covered โ `audit-tab-body-coverage` exists
|
|
47
|
+
* for precisely this, after `CategoryDetailTabs` showed a blank panel for
|
|
48
|
+
* four types while `BrandDetailTabs` silently dropped the same four โ so the
|
|
49
|
+
* eleventh type will be caught rather than discovered.
|
|
50
|
+
*/
|
|
51
|
+
[pluginFor("spares").tabSlug]: ["spares"],
|
|
40
52
|
};
|
|
41
53
|
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
42
54
|
const countFor = (id) => counts?.[id];
|
|
@@ -18,7 +18,16 @@ export function CategoryCard({ category, href, onClick, className = "", }) {
|
|
|
18
18
|
// own-node-only productCount so a parent category's card shows the same
|
|
19
19
|
// "N products" a visitor actually sees once they open the page.
|
|
20
20
|
const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? category.productCount ?? 0;
|
|
21
|
-
|
|
21
|
+
/*
|
|
22
|
+
* Is a real picture going to render in the image area? Drives the emoji
|
|
23
|
+
* overlay below, which must not paint over artwork. Deliberately mirrors the
|
|
24
|
+
* first two branches of the chain rather than being derived from it โ the
|
|
25
|
+
* `color` branch is a flat tint, which an emoji on top of is still the
|
|
26
|
+
* intended look.
|
|
27
|
+
*/
|
|
28
|
+
const hasArtwork = (category.display?.mosaicImages?.length ?? 0) >= MOSAIC_MIN ||
|
|
29
|
+
Boolean(category.display?.coverImage);
|
|
30
|
+
const inner = (_jsxs(Stack, { className: "h-full", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-[4/3] w-full ${__O.hidden} flex-shrink-0`, children: [(category.display?.mosaicImages?.length ?? 0) >= MOSAIC_MIN ? (_jsx(ImageMosaic, { images: category.display.mosaicImages, alt: category.name, className: "h-full w-full" })) : category.display?.coverImage ? (_jsx(MediaImage, { src: category.display.coverImage, alt: category.name, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : category.display?.color ? (_jsx(DynamicBgDiv, { color: category.display.color, className: "h-full w-full opacity-80" })) : null, category.display?.icon && !hasArtwork && (_jsx(Row, { textSize: "4xl", className: "absolute inset-0", align: "center", justify: "center", children: category.display.icon })), category.isFeatured && (_jsx(Span, { size: "xs", className: CLS_FEATURED_DOT, children: "\u2605" }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3}.5`, children: [_jsx(Text, { color: "inverse", className: `leading-snug text-[var(--appkit-color-text)] dark:`, truncate: 2, size: "sm", weight: "semibold", children: category.name }), category.description && (_jsx(Text, { className: `mt-1 flex-1`, color: "muted", truncate: 2, size: "xs", children: category.description })), _jsxs(Row, { className: "mt-2", align: "center", justify: "between", gap: "sm", children: [_jsxs(Text, { size: "xs", color: "faint", children: [productCount.toLocaleString(), " ", productCount === 1 ? "item" : "items"] }), _jsxs(Span, { layout: "inline-flex", gap: "xs", size: "xs", weight: "medium", border: "default", className: "group-hover:bg-primary group-hover:border-primary group-hover:text-white transition-colors", rounded: "md", padding: "pill-sm-tall", color: "muted", children: ["Browse ", _jsx(ArrowRight, { className: "h-3 w-3" })] })] })] })] }));
|
|
22
31
|
const cardClass = `group relative flex flex-col overflow-hidden rounded-xl border border-neutral-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] shadow-sm transition hover:shadow-md h-full ${className}`;
|
|
23
32
|
if (href) {
|
|
24
33
|
return (_jsx(Link, { href: href, className: cardClass, children: inner }));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PriceGuidanceHintProps {
|
|
3
|
+
/** Leaf category id/slug the listing is filed under. */
|
|
4
|
+
categoryId: string;
|
|
5
|
+
currency?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The seller-facing price hint `description-template/route.ts`'s own
|
|
10
|
+
* docstring names as "C1's job, from a surface that knows who is asking" โ
|
|
11
|
+
* renders the inherited competitor-derived market band beside the price
|
|
12
|
+
* field, on the SELLER'S own tax basis (the `/price-guidance` endpoint does
|
|
13
|
+
* that conversion server-side; this component only formats what it returns).
|
|
14
|
+
*
|
|
15
|
+
* Mounted via `ProductForm`'s `renderPriceGuidance` slot rather than fetched
|
|
16
|
+
* unconditionally inside the form โ `ProductForm` has no data-fetching of its
|
|
17
|
+
* own (every app-specific concern is a render-prop), and this component is
|
|
18
|
+
* seller/admin-only, not something the form itself should assume a caller
|
|
19
|
+
* wants.
|
|
20
|
+
*
|
|
21
|
+
* ๐ NEVER mount this on a public page (category/brand detail). A
|
|
22
|
+
* competitor-derived market band is exactly the "a neighbouring surface
|
|
23
|
+
* publishes the range" leak the guest price gate exists to close (CLAUDE.md
|
|
24
|
+
* ยง "Guest price gating"), even though the figure is advisory rather than a
|
|
25
|
+
* real listing price โ which is also why `/price-guidance` itself is
|
|
26
|
+
* seller/admin/moderator-gated, not a public, edge-cached endpoint like its
|
|
27
|
+
* `description-template` sibling.
|
|
28
|
+
*/
|
|
29
|
+
export declare function PriceGuidanceHint({ categoryId, currency, className, }: PriceGuidanceHintProps): React.JSX.Element | null;
|