@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
|
@@ -23,5 +23,5 @@ export function BottomNavItem({ href, label, icon, badge, isActive = false, clas
|
|
|
23
23
|
// Consumer projects may swap for next/link if preferred.
|
|
24
24
|
return (_jsx(Li, { className: "flex-1", children: _jsxs("a", { href: href, "aria-label": label, "aria-current": isActive ? "page" : undefined, className: `relative flex flex-col items-center justify-center gap-[var(--appkit-space-0-5)] h-full w-full min-h-[44px] transition-colors ${isActive
|
|
25
25
|
? "text-primary-600 dark:text-secondary-400"
|
|
26
|
-
: "text-[var(--appkit-color-text-muted)] hover:text-
|
|
26
|
+
: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)]"}${className ? ` ${className}` : ""}`, children: [_jsxs(Span, { className: "relative", children: [icon, badge != null && badge > 0 && (_jsx(Span, { layout: "flex-center", color: "inverse", "aria-label": `${badge} items`, className: "absolute -top-1.5 -right-1.5 min-w-[16px] h-4 px-[var(--appkit-space-1)] text-[10px] bg-secondary-500 leading-none", rounded: "full", weight: "bold", children: badge > 99 ? "99+" : badge }))] }), _jsx(Span, { weight: "medium", className: "text-[10px] leading-none", children: label })] }) }));
|
|
27
27
|
}
|
|
@@ -38,5 +38,5 @@ export function BottomSheet({ open, onClose, title, children, className, maxHeig
|
|
|
38
38
|
}, [open, handleEscape]);
|
|
39
39
|
if (!open)
|
|
40
40
|
return null;
|
|
41
|
-
return (_jsxs(Div, { className: "fixed inset-0 z-50 lg:hidden", role: "presentation", children: [_jsx(Div, { surface: "overlay-sm", className: "absolute inset-0 backdrop-blur-sm", "aria-hidden": "true", onClick: onClose }), _jsxs(Stack, { role: "dialog", "aria-modal": "true", "aria-label": title, className: `absolute bottom-0 inset-x-0 rounded-t-2xl border-t border-[var(--appkit-color-border-subtle)] shadow-2xl ${maxHeight} animate-slide-up${className ? ` ${className}` : ""}`, surface: "default", children: [_jsxs(Row, { border: "subtle", justify: "between", className: "border-b shrink-0", padding: "inline", children: [_jsx(Div, { surface: "skeleton", className: "absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-[var(--appkit-color-border)]", rounded: "full" }), title && (_jsx(Span, { size: "sm", weight: "semibold", className: "mt-2", color: "primary", children: title })), _jsx(Button, { rounded: "lg", type: "button", onClick: onClose, "aria-label": "Close", className: "ml-auto mt-2 p-[var(--appkit-space-1-5)] text-[var(--appkit-color-text-muted)] hover:text-
|
|
41
|
+
return (_jsxs(Div, { className: "fixed inset-0 z-50 lg:hidden", role: "presentation", children: [_jsx(Div, { surface: "overlay-sm", className: "absolute inset-0 backdrop-blur-sm", "aria-hidden": "true", onClick: onClose }), _jsxs(Stack, { role: "dialog", "aria-modal": "true", "aria-label": title, className: `absolute bottom-0 inset-x-0 rounded-t-2xl border-t border-[var(--appkit-color-border-subtle)] shadow-2xl ${maxHeight} animate-slide-up${className ? ` ${className}` : ""}`, surface: "default", children: [_jsxs(Row, { border: "subtle", justify: "between", className: "border-b shrink-0", padding: "inline", children: [_jsx(Div, { surface: "skeleton", className: "absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-[var(--appkit-color-border)]", rounded: "full" }), title && (_jsx(Span, { size: "sm", weight: "semibold", className: "mt-2", color: "primary", children: title })), _jsx(Button, { rounded: "lg", type: "button", onClick: onClose, "aria-label": "Close", className: "ml-auto mt-2 p-[var(--appkit-space-1-5)] text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] hover:bg-surface-hover transition-colors min-h-[44px] min-w-[44px] justify-center", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto} overscroll-contain pb-[env(safe-area-inset-bottom)]`, children: children })] })] }));
|
|
42
42
|
}
|
|
@@ -64,7 +64,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
64
64
|
_jsxs(Row, { gap: "xs", reverse: "hand", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-surface", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
|
|
65
65
|
const hasTb2 = !!(notificationsEl || messagesEl || wishlistEl || cartEl || profileEl);
|
|
66
66
|
// ββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
67
|
-
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { gap: "none", reverse: "hand", className: "relative h-14", children: [_jsx(Row, { gap: "3", reverse: "hand", className: "flex-1", children:
|
|
67
|
+
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { gap: "none", reverse: "hand", className: "relative h-14", children: [_jsx(Row, { gap: "3", reverse: "hand", className: "flex-1", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), _jsx(Row, { className: "flex flex-none", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandShortName ?? brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", reverse: "hand", justify: "end", className: "flex-1", children: [devSlot, navSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), messagesEl && _jsx(Div, { className: "hidden lg:flex", children: messagesEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
68
68
|
? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
|
|
69
69
|
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, _jsx(Div, { className: "hidden sm:flex", children: promotionsEl }), themeBtn, hamburgerBtn] })] }), hasTb2 && (
|
|
70
70
|
// `justify="end"` is CORRECT alongside `reverse="hand"` β do not
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link.js";
|
|
3
3
|
import { getLiveItemForDetail } from "../../../_internal/server/features/live/data.js";
|
|
4
|
-
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
|
|
4
|
+
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 dark:hover:text-primary-400 transition-colors";
|
|
5
5
|
const CLS_LIVE_ITEM_BADGE = "inline-block rounded-full bg-lime-100 dark:bg-lime-900/30 px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-lime-700 dark:text-lime-300";
|
|
6
6
|
const CLS_WARN_BOX = "rounded-lg border border-warning bg-warning-surface p-[var(--appkit-space-4)] text-[length:var(--appkit-text-sm)]";
|
|
7
7
|
const CLS_WARN_TITLE = "font-medium text-warning";
|
|
@@ -11,11 +11,11 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
|
11
11
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
12
12
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
13
13
|
import { safeDisplayName } from "../../../security/index.js";
|
|
14
|
-
import { Div, Container, GatedPrice, PricesOnly, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text,
|
|
14
|
+
import { Div, Container, GatedPrice, PricesOnly, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, SpecTable, } from "../../../ui/index.js";
|
|
15
15
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView.js";
|
|
16
16
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
|
|
17
17
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
|
|
18
|
-
import {
|
|
18
|
+
import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
|
|
19
19
|
import { ShareButton } from "../../products/components/ShareButton.js";
|
|
20
20
|
import { HistoryTracker } from "../../history/components/HistoryTracker.js";
|
|
21
21
|
import { RelatedItemsSection } from "../../products/components/RelatedItemsSection.js";
|
|
@@ -57,7 +57,6 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
57
57
|
const storeSlug = (typeof p.storeSlug === "string" ? p.storeSlug : null) || (typeof p.storeId === "string" ? p.storeId : null);
|
|
58
58
|
const storeHref = storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null;
|
|
59
59
|
const specs = Array.isArray(p.specifications) ? p.specifications : [];
|
|
60
|
-
const customSections = Array.isArray(p.customSections) ? p.customSections : [];
|
|
61
60
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
62
61
|
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
|
|
63
62
|
computeRelatedItems(product),
|
|
@@ -70,9 +69,5 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
70
69
|
price: price ?? undefined,
|
|
71
70
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
72
71
|
storeName: storeName ?? undefined,
|
|
73
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` Β· ${meta.sex}`, meta.ageMonths !== undefined && ` Β· ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", children: ["Method: ", transport.method, transport.handlingFee !== undefined && (_jsx(PricesOnly, { children: ` Β· Handling: ${formatCurrency(transport.handlingFee, "INR")}` })), transport.insuranceIncluded && " Β· Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(
|
|
74
|
-
id: s.id,
|
|
75
|
-
label: s.title,
|
|
76
|
-
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
77
|
-
})) })), renderBuyBar: () => _jsx(Div, { id: "live-item-buy-bar", children: renderActions?.(product) }), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
|
|
72
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` Β· ${meta.sex}`, meta.ageMonths !== undefined && ` Β· ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", children: ["Method: ", transport.method, transport.handlingFee !== undefined && (_jsx(PricesOnly, { children: ` Β· Handling: ${formatCurrency(transport.handlingFee, "INR")}` })), transport.insuranceIncluded && " Β· Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) })), renderBuyBar: () => _jsx(Div, { id: "live-item-buy-bar", children: renderActions?.(product) }), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
|
|
78
73
|
}
|
|
@@ -58,6 +58,23 @@ export function useMediaUpload(endpoints = {}) {
|
|
|
58
58
|
return apiClient.post(finalizeEndpoint, {
|
|
59
59
|
storagePath: signResponse.storagePath,
|
|
60
60
|
isPublic,
|
|
61
|
+
/*
|
|
62
|
+
* A7 β the ORIGINAL client filename, for the provenance heuristics.
|
|
63
|
+
*
|
|
64
|
+
* π It cannot be derived server-side. Our own `storagePath` is a
|
|
65
|
+
* content-derived SEO slug minted at sign time (Β§ Media Filename Slug
|
|
66
|
+
* Patterns), so by the time the server sees the upload the only name
|
|
67
|
+
* left is one we invented β and the signals A7 looks for (Amazon's
|
|
68
|
+
* `_AC_SL1500_`, a generator's `ChatGPT-Image-β¦`) live exclusively in
|
|
69
|
+
* the name the file arrived with.
|
|
70
|
+
*
|
|
71
|
+
* Advisory and client-supplied, therefore trivially spoofable. That is
|
|
72
|
+
* acceptable because nothing gates on it: it raises a flag for a human,
|
|
73
|
+
* and a seller who renames a file to avoid one has merely declined to
|
|
74
|
+
* volunteer information. Never treat it as evidence of anything but
|
|
75
|
+
* itself.
|
|
76
|
+
*/
|
|
77
|
+
originalFilename: file.name,
|
|
61
78
|
});
|
|
62
79
|
},
|
|
63
80
|
});
|
|
@@ -40,12 +40,58 @@ export interface MediaAssetDocument {
|
|
|
40
40
|
* unwatermarked image, or double-watermark it the other way round.
|
|
41
41
|
*/
|
|
42
42
|
watermarkedAt?: Date;
|
|
43
|
+
/**
|
|
44
|
+
* A7 β provenance concerns recorded for human review. See
|
|
45
|
+
* `features/media/trust-signals.ts` for the vocabulary and, more
|
|
46
|
+
* importantly, for what is deliberately NOT flagged.
|
|
47
|
+
*
|
|
48
|
+
* π ADVISORY. Nothing reads this to block an upload, hide a listing or
|
|
49
|
+
* refuse a seller; it exists so a moderator can look. A heuristic that
|
|
50
|
+
* auto-rejects refuses honest sellers on a filename.
|
|
51
|
+
*
|
|
52
|
+
* Absent means "not evaluated" (every row predating A7, and every non-image
|
|
53
|
+
* asset), which is NOT the same as "clean" β a reader must not treat the
|
|
54
|
+
* absence as a pass.
|
|
55
|
+
*/
|
|
56
|
+
trustFlags?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* A7 β 16-char hex dHash of the image, for near-duplicate detection.
|
|
59
|
+
*
|
|
60
|
+
* Perceptual, not cryptographic: stable across re-encoding and resizing, so
|
|
61
|
+
* it answers "is this the same photograph" rather than "is this the same
|
|
62
|
+
* file". Images only; absent when the hash could not be computed, which is
|
|
63
|
+
* deliberately non-fatal.
|
|
64
|
+
*/
|
|
65
|
+
dHash?: string;
|
|
43
66
|
createdAt: Date;
|
|
44
67
|
}
|
|
45
68
|
export type MediaAssetCreateInput = Omit<MediaAssetDocument, "id" | "createdAt">;
|
|
46
69
|
export declare class MediaAssetsRepository extends BaseRepository<MediaAssetDocument> {
|
|
47
70
|
constructor();
|
|
48
71
|
findByStoragePath(storagePath: string): Promise<MediaAssetDocument | null>;
|
|
72
|
+
/**
|
|
73
|
+
* A7 β assets sharing a dHash, for the cross-seller duplicate check.
|
|
74
|
+
*
|
|
75
|
+
* π BOUNDED, and exact-match only. An equality on `dHash` is served by
|
|
76
|
+
* Firestore's automatic single-field index, so this needs no composite β
|
|
77
|
+
* whereas a true nearest-neighbour search over 64-bit hashes would need
|
|
78
|
+
* either a full scan or a BK-tree, and a full collection scan on every
|
|
79
|
+
* upload is precisely the unbounded-read shape Rule #6 exists to prevent.
|
|
80
|
+
*
|
|
81
|
+
* π The trade is narrower than it first looks, and it was MEASURED rather
|
|
82
|
+
* than assumed. Against the same source image:
|
|
83
|
+
*
|
|
84
|
+
* resized to 220px wide -> distance 0 (exact match CATCHES it)
|
|
85
|
+
* re-encoded jpeg q55 -> distance 3 (exact match MISSES it)
|
|
86
|
+
* a different bey entirely -> distance 9
|
|
87
|
+
*
|
|
88
|
+
* So this online check reliably catches a RESIZE and will miss a re-encode.
|
|
89
|
+
* An earlier revision of this comment claimed both produce an identical
|
|
90
|
+
* hash; that was wrong. `dHashDistance` and `DHASH_DUPLICATE_MAX_DISTANCE`
|
|
91
|
+
* are exported for an offline sweep that can afford the pairwise
|
|
92
|
+
* comparison, which is where the distance-3 case gets caught.
|
|
93
|
+
*/
|
|
94
|
+
findByDHash(dHash: string, limit?: number): Promise<MediaAssetDocument[]>;
|
|
49
95
|
/**
|
|
50
96
|
* Create a new mediaAssets document with a collision-safe shortId.
|
|
51
97
|
* Tries `preferredShortId` first, then appends a random 4-char suffix up to
|
|
@@ -12,6 +12,37 @@ export class MediaAssetsRepository extends BaseRepository {
|
|
|
12
12
|
async findByStoragePath(storagePath) {
|
|
13
13
|
return this.findOneBy("storagePath", storagePath);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A7 β assets sharing a dHash, for the cross-seller duplicate check.
|
|
17
|
+
*
|
|
18
|
+
* π BOUNDED, and exact-match only. An equality on `dHash` is served by
|
|
19
|
+
* Firestore's automatic single-field index, so this needs no composite β
|
|
20
|
+
* whereas a true nearest-neighbour search over 64-bit hashes would need
|
|
21
|
+
* either a full scan or a BK-tree, and a full collection scan on every
|
|
22
|
+
* upload is precisely the unbounded-read shape Rule #6 exists to prevent.
|
|
23
|
+
*
|
|
24
|
+
* π The trade is narrower than it first looks, and it was MEASURED rather
|
|
25
|
+
* than assumed. Against the same source image:
|
|
26
|
+
*
|
|
27
|
+
* resized to 220px wide -> distance 0 (exact match CATCHES it)
|
|
28
|
+
* re-encoded jpeg q55 -> distance 3 (exact match MISSES it)
|
|
29
|
+
* a different bey entirely -> distance 9
|
|
30
|
+
*
|
|
31
|
+
* So this online check reliably catches a RESIZE and will miss a re-encode.
|
|
32
|
+
* An earlier revision of this comment claimed both produce an identical
|
|
33
|
+
* hash; that was wrong. `dHashDistance` and `DHASH_DUPLICATE_MAX_DISTANCE`
|
|
34
|
+
* are exported for an offline sweep that can afford the pairwise
|
|
35
|
+
* comparison, which is where the distance-3 case gets caught.
|
|
36
|
+
*/
|
|
37
|
+
async findByDHash(dHash, limit = 5) {
|
|
38
|
+
if (!dHash)
|
|
39
|
+
return [];
|
|
40
|
+
const snapshot = await this.getCollection()
|
|
41
|
+
.where("dHash", "==", dHash)
|
|
42
|
+
.limit(limit)
|
|
43
|
+
.get();
|
|
44
|
+
return snapshot.docs.map((d) => this.mapDoc(d));
|
|
45
|
+
}
|
|
15
46
|
/**
|
|
16
47
|
* Create a new mediaAssets document with a collision-safe shortId.
|
|
17
48
|
* Tries `preferredShortId` first, then appends a random 4-char suffix up to
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance signals on uploaded imagery β plan A7.
|
|
3
|
+
*
|
|
4
|
+
* ## π A FLAG, never a rejection
|
|
5
|
+
*
|
|
6
|
+
* Nothing here blocks an upload or hides a listing. Every signal is recorded
|
|
7
|
+
* on the `mediaAssets` row for a human to look at, because every one of them
|
|
8
|
+
* has a legitimate explanation: a seller may genuinely own the Amazon listing
|
|
9
|
+
* the photo came from, and an AI-generated image is a reasonable choice for an
|
|
10
|
+
* art listing's mock-up. Auto-rejecting on a heuristic means refusing honest
|
|
11
|
+
* sellers on a filename.
|
|
12
|
+
*
|
|
13
|
+
* ## What is grounded in measurement
|
|
14
|
+
*
|
|
15
|
+
* From the fifteen-site competitor crawl
|
|
16
|
+
* (`docs/research/competitor-crawl-2026-10/images.md`):
|
|
17
|
+
*
|
|
18
|
+
* bladekingbeys β **138 images named with Amazon's CDN pattern**
|
|
19
|
+
* (`..._AC_SL1500_.jpg`) plus `ChatGPT-Image-*.png` files.
|
|
20
|
+
* A storefront built from other people's product shots.
|
|
21
|
+
*
|
|
22
|
+
* ## π What is deliberately NOT a signal
|
|
23
|
+
*
|
|
24
|
+
* `rn-image_picker_lib_temp_*` (React Native's image picker) and
|
|
25
|
+
* `WhatsApp-Image-*` are what REAL sellers' uploads look like β measured,
|
|
26
|
+
* toycollectorsindia and beyyuniverse upload exactly that way, and
|
|
27
|
+
* beyyuniverse's entire catalogue is `WhatsApp-Image-*.jpeg`. Flagging them
|
|
28
|
+
* would put a flag on nearly every honest phone upload, which trains
|
|
29
|
+
* moderators to dismiss the queue β and a queue nobody reads is worse than no
|
|
30
|
+
* queue, because it looks like oversight.
|
|
31
|
+
*
|
|
32
|
+
* That asymmetry is the whole design: flag the patterns that indicate the
|
|
33
|
+
* image came from someone ELSE'S listing, never the ones that merely indicate
|
|
34
|
+
* it came from a phone.
|
|
35
|
+
*/
|
|
36
|
+
/** One recorded provenance concern. Stable strings β they are persisted. */
|
|
37
|
+
export declare const MEDIA_TRUST_FLAG: {
|
|
38
|
+
/** Filename carries Amazon's CDN size suffix, e.g. `..._AC_SL1500_.jpg`. */
|
|
39
|
+
readonly AMAZON_CDN_FILENAME: "amazon-cdn-filename";
|
|
40
|
+
/** Filename is an AI image generator's default output name. */
|
|
41
|
+
readonly AI_GENERATED_FILENAME: "ai-generated-filename";
|
|
42
|
+
/**
|
|
43
|
+
* Byte-level near-duplicate of an image a DIFFERENT uploader already has.
|
|
44
|
+
*
|
|
45
|
+
* Same-uploader duplicates are normal and are NOT flagged β a seller
|
|
46
|
+
* reusing one photo across their own variants is ordinary behaviour.
|
|
47
|
+
*/
|
|
48
|
+
readonly CROSS_SELLER_DUPLICATE: "cross-seller-duplicate";
|
|
49
|
+
};
|
|
50
|
+
export type MediaTrustFlag = (typeof MEDIA_TRUST_FLAG)[keyof typeof MEDIA_TRUST_FLAG];
|
|
51
|
+
/**
|
|
52
|
+
* Provenance flags derivable from the filename alone.
|
|
53
|
+
*
|
|
54
|
+
* Takes the ORIGINAL client filename, not the storage path β our own paths are
|
|
55
|
+
* content-derived slugs (Β§ Media Filename Slug Patterns) and carry none of
|
|
56
|
+
* this.
|
|
57
|
+
*/
|
|
58
|
+
export declare function filenameTrustFlags(originalFilename: string | undefined | null): MediaTrustFlag[];
|
|
59
|
+
/**
|
|
60
|
+
* Hamming distance between two hex dHashes β the number of differing bits.
|
|
61
|
+
*
|
|
62
|
+
* 0 is byte-identical-after-downscale; under ~5 is "the same photograph,
|
|
63
|
+
* re-encoded or lightly cropped". Returns `null` when the two cannot be
|
|
64
|
+
* compared (different lengths, non-hex), because a fabricated distance would
|
|
65
|
+
* feed a flag.
|
|
66
|
+
*/
|
|
67
|
+
export declare function dHashDistance(a: string | undefined, b: string | undefined): number | null;
|
|
68
|
+
/**
|
|
69
|
+
* How close two dHashes must be to count as the same photograph.
|
|
70
|
+
*
|
|
71
|
+
* π Deliberately TIGHT, and the number comes from measurement rather than
|
|
72
|
+
* from the usual "distance < 10" folklore. Against one real catalogue image:
|
|
73
|
+
*
|
|
74
|
+
* resized to 220px wide -> 0
|
|
75
|
+
* re-encoded jpeg q55 -> 3
|
|
76
|
+
* a DIFFERENT bey -> 9
|
|
77
|
+
*
|
|
78
|
+
* So the usable gap is 3-vs-9, which is real but not generous: product
|
|
79
|
+
* photography is self-similar (same item class, same white background, same
|
|
80
|
+
* angle), so a loose threshold here is a flag GENERATOR rather than a signal,
|
|
81
|
+
* and a queue full of false flags is worse than no queue because it looks like
|
|
82
|
+
* oversight. 5 sits above the re-encode and well below the different-item
|
|
83
|
+
* case.
|
|
84
|
+
*/
|
|
85
|
+
export declare const DHASH_DUPLICATE_MAX_DISTANCE = 5;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance signals on uploaded imagery β plan A7.
|
|
3
|
+
*
|
|
4
|
+
* ## π A FLAG, never a rejection
|
|
5
|
+
*
|
|
6
|
+
* Nothing here blocks an upload or hides a listing. Every signal is recorded
|
|
7
|
+
* on the `mediaAssets` row for a human to look at, because every one of them
|
|
8
|
+
* has a legitimate explanation: a seller may genuinely own the Amazon listing
|
|
9
|
+
* the photo came from, and an AI-generated image is a reasonable choice for an
|
|
10
|
+
* art listing's mock-up. Auto-rejecting on a heuristic means refusing honest
|
|
11
|
+
* sellers on a filename.
|
|
12
|
+
*
|
|
13
|
+
* ## What is grounded in measurement
|
|
14
|
+
*
|
|
15
|
+
* From the fifteen-site competitor crawl
|
|
16
|
+
* (`docs/research/competitor-crawl-2026-10/images.md`):
|
|
17
|
+
*
|
|
18
|
+
* bladekingbeys β **138 images named with Amazon's CDN pattern**
|
|
19
|
+
* (`..._AC_SL1500_.jpg`) plus `ChatGPT-Image-*.png` files.
|
|
20
|
+
* A storefront built from other people's product shots.
|
|
21
|
+
*
|
|
22
|
+
* ## π What is deliberately NOT a signal
|
|
23
|
+
*
|
|
24
|
+
* `rn-image_picker_lib_temp_*` (React Native's image picker) and
|
|
25
|
+
* `WhatsApp-Image-*` are what REAL sellers' uploads look like β measured,
|
|
26
|
+
* toycollectorsindia and beyyuniverse upload exactly that way, and
|
|
27
|
+
* beyyuniverse's entire catalogue is `WhatsApp-Image-*.jpeg`. Flagging them
|
|
28
|
+
* would put a flag on nearly every honest phone upload, which trains
|
|
29
|
+
* moderators to dismiss the queue β and a queue nobody reads is worse than no
|
|
30
|
+
* queue, because it looks like oversight.
|
|
31
|
+
*
|
|
32
|
+
* That asymmetry is the whole design: flag the patterns that indicate the
|
|
33
|
+
* image came from someone ELSE'S listing, never the ones that merely indicate
|
|
34
|
+
* it came from a phone.
|
|
35
|
+
*/
|
|
36
|
+
/** One recorded provenance concern. Stable strings β they are persisted. */
|
|
37
|
+
export const MEDIA_TRUST_FLAG = {
|
|
38
|
+
/** Filename carries Amazon's CDN size suffix, e.g. `..._AC_SL1500_.jpg`. */
|
|
39
|
+
AMAZON_CDN_FILENAME: "amazon-cdn-filename",
|
|
40
|
+
/** Filename is an AI image generator's default output name. */
|
|
41
|
+
AI_GENERATED_FILENAME: "ai-generated-filename",
|
|
42
|
+
/**
|
|
43
|
+
* Byte-level near-duplicate of an image a DIFFERENT uploader already has.
|
|
44
|
+
*
|
|
45
|
+
* Same-uploader duplicates are normal and are NOT flagged β a seller
|
|
46
|
+
* reusing one photo across their own variants is ordinary behaviour.
|
|
47
|
+
*/
|
|
48
|
+
CROSS_SELLER_DUPLICATE: "cross-seller-duplicate",
|
|
49
|
+
};
|
|
50
|
+
/*
|
|
51
|
+
* `_AC_` is Amazon's image-transform marker and `_SL<digits>_` its size
|
|
52
|
+
* directive; together they are unmistakable and do not occur in ordinary
|
|
53
|
+
* filenames. Matching `_AC_` alone would false-positive on anything with those
|
|
54
|
+
* letters between underscores.
|
|
55
|
+
*/
|
|
56
|
+
const AMAZON_CDN_RX = /_AC_[A-Z]{0,4}\d*_|_SL\d{3,4}_/;
|
|
57
|
+
/*
|
|
58
|
+
* Default output names from the common generators. Anchored to the START so a
|
|
59
|
+
* seller who writes "chatgpt-image-of-my-bey.jpg" by hand is not caught by a
|
|
60
|
+
* substring β the signal is "this is the tool's untouched default", not "the
|
|
61
|
+
* word appears".
|
|
62
|
+
*/
|
|
63
|
+
const AI_FILENAME_RX = /^(?:chatgpt-image-|dall[Β·e-]*e-|midjourney_|stable-?diffusion-|gemini-generated-image)/i;
|
|
64
|
+
/**
|
|
65
|
+
* Provenance flags derivable from the filename alone.
|
|
66
|
+
*
|
|
67
|
+
* Takes the ORIGINAL client filename, not the storage path β our own paths are
|
|
68
|
+
* content-derived slugs (Β§ Media Filename Slug Patterns) and carry none of
|
|
69
|
+
* this.
|
|
70
|
+
*/
|
|
71
|
+
export function filenameTrustFlags(originalFilename) {
|
|
72
|
+
if (!originalFilename)
|
|
73
|
+
return [];
|
|
74
|
+
const name = originalFilename.trim();
|
|
75
|
+
if (!name)
|
|
76
|
+
return [];
|
|
77
|
+
const flags = [];
|
|
78
|
+
if (AMAZON_CDN_RX.test(name))
|
|
79
|
+
flags.push(MEDIA_TRUST_FLAG.AMAZON_CDN_FILENAME);
|
|
80
|
+
if (AI_FILENAME_RX.test(name))
|
|
81
|
+
flags.push(MEDIA_TRUST_FLAG.AI_GENERATED_FILENAME);
|
|
82
|
+
return flags;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Hamming distance between two hex dHashes β the number of differing bits.
|
|
86
|
+
*
|
|
87
|
+
* 0 is byte-identical-after-downscale; under ~5 is "the same photograph,
|
|
88
|
+
* re-encoded or lightly cropped". Returns `null` when the two cannot be
|
|
89
|
+
* compared (different lengths, non-hex), because a fabricated distance would
|
|
90
|
+
* feed a flag.
|
|
91
|
+
*/
|
|
92
|
+
export function dHashDistance(a, b) {
|
|
93
|
+
if (!a || !b || a.length !== b.length)
|
|
94
|
+
return null;
|
|
95
|
+
let distance = 0;
|
|
96
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
97
|
+
const x = Number.parseInt(a[i], 16);
|
|
98
|
+
const y = Number.parseInt(b[i], 16);
|
|
99
|
+
if (Number.isNaN(x) || Number.isNaN(y))
|
|
100
|
+
return null;
|
|
101
|
+
let diff = x ^ y;
|
|
102
|
+
while (diff) {
|
|
103
|
+
distance += diff & 1;
|
|
104
|
+
diff >>= 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return distance;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* How close two dHashes must be to count as the same photograph.
|
|
111
|
+
*
|
|
112
|
+
* π Deliberately TIGHT, and the number comes from measurement rather than
|
|
113
|
+
* from the usual "distance < 10" folklore. Against one real catalogue image:
|
|
114
|
+
*
|
|
115
|
+
* resized to 220px wide -> 0
|
|
116
|
+
* re-encoded jpeg q55 -> 3
|
|
117
|
+
* a DIFFERENT bey -> 9
|
|
118
|
+
*
|
|
119
|
+
* So the usable gap is 3-vs-9, which is real but not generous: product
|
|
120
|
+
* photography is self-similar (same item class, same white background, same
|
|
121
|
+
* angle), so a loose threshold here is a flag GENERATOR rather than a signal,
|
|
122
|
+
* and a queue full of false flags is worse than no queue because it looks like
|
|
123
|
+
* oversight. 5 sits above the re-encode and well below the different-item
|
|
124
|
+
* case.
|
|
125
|
+
*/
|
|
126
|
+
export const DHASH_DUPLICATE_MAX_DISTANCE = 5;
|
|
@@ -41,8 +41,21 @@ export interface PartDetailPageViewProps {
|
|
|
41
41
|
listingsTruncated?: boolean;
|
|
42
42
|
/** Where "see all listings" goes β a real search, not a fabricated filter. */
|
|
43
43
|
allListingsHref: string;
|
|
44
|
+
/**
|
|
45
|
+
* Listings naming THIS exact glossary entry, via `partGlossaryId` β spares
|
|
46
|
+
* and any other individually-sold component carrying it.
|
|
47
|
+
*
|
|
48
|
+
* π A SEPARATE section from `listings` above, not a narrower version of it.
|
|
49
|
+
* `listings` is a fuzzy name search across the GENERAL catalogue
|
|
50
|
+
* (`GENERIC_PRODUCT_LISTING_TYPES`), which `spares` is deliberately excluded
|
|
51
|
+
* from (`inGeneralCatalogue: false`) β so a spare of this exact part could
|
|
52
|
+
* never appear there no matter how precisely it was titled. This section is
|
|
53
|
+
* the one place that gap is closed.
|
|
54
|
+
*/
|
|
55
|
+
exactListings?: readonly PartListingRow[];
|
|
56
|
+
exactListingsTruncated?: boolean;
|
|
44
57
|
/** A model's own page, when the composition resolved to a category. */
|
|
45
58
|
hrefForModel: (categoryId: string) => string;
|
|
46
59
|
hrefForClassification: (slot: string) => string;
|
|
47
60
|
}
|
|
48
|
-
export declare function PartDetailPageView({ part, usedBy, listings, listingsTruncated, allListingsHref, hrefForModel, hrefForClassification, }: PartDetailPageViewProps): React.JSX.Element;
|
|
61
|
+
export declare function PartDetailPageView({ part, usedBy, listings, listingsTruncated, allListingsHref, exactListings, exactListingsTruncated, hrefForModel, hrefForClassification, }: PartDetailPageViewProps): React.JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Badge, } from "../../../ui/index.js";
|
|
3
3
|
import { PART_SLOT_LABEL, } from "../schemas/firestore.js";
|
|
4
|
-
export function PartDetailPageView({ part, usedBy, listings, listingsTruncated, allListingsHref, hrefForModel, hrefForClassification, }) {
|
|
4
|
+
export function PartDetailPageView({ part, usedBy, listings, listingsTruncated, allListingsHref, exactListings = [], exactListingsTruncated, hrefForModel, hrefForClassification, }) {
|
|
5
5
|
const classification = PART_SLOT_LABEL[part.slot] ?? part.classification ?? part.slot;
|
|
6
6
|
/*
|
|
7
7
|
* π Only 23 of 753 parts carry a code, and that is a fact about the sources
|
|
@@ -16,5 +16,5 @@ export function PartDetailPageView({ part, usedBy, listings, listingsTruncated,
|
|
|
16
16
|
/* 49 of 259 compositions did not join to a category.
|
|
17
17
|
A link to a page that does not exist is worse than
|
|
18
18
|
plain text, so the name renders unlinked. */
|
|
19
|
-
_jsx(Text, { color: "primary", children: m.name })), m.system && (_jsxs(Span, { size: "xs", color: "muted", children: [" ", "\u00B7 ", m.system] }))] }), m.productCode && (_jsx(Text, { size: "xs", color: "faint", className: "flex-shrink-0", children: m.productCode }))] }, m.id))) }))] }), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", className: "min-w-0", children: [_jsxs(Heading, { level: 2, size: "lg", children: ["Listings", listingsTruncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", listings.length, "+)"] }))] }), listings.length > 0 && (_jsx(TextLink, { href: allListingsHref, children: "See all listings \u2192" }))] }), listings.length === 0 ? (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { color: "muted", size: "sm", children: "Nobody is selling this part right now." }), _jsx(TextLink, { href: allListingsHref, children: "Search the catalogue \u2192" })] })) : (_jsx(Stack, { gap: "xs", className: "min-w-0", children: listings.map((l) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", border: "subtle", className: "min-w-0", children: [_jsx(TextLink, { href: l.href, children: l.title }), l.condition && (_jsx(Text, { size: "xs", color: "muted", children: l.condition }))] }, l.id))) }))] }), part.source && (_jsxs(Text, { size: "xs", color: "faint", children: ["Source: ", part.source] }))] }) }) }));
|
|
19
|
+
_jsx(Text, { color: "primary", children: m.name })), m.system && (_jsxs(Span, { size: "xs", color: "muted", children: [" ", "\u00B7 ", m.system] }))] }), m.productCode && (_jsx(Text, { size: "xs", color: "faint", className: "flex-shrink-0", children: m.productCode }))] }, m.id))) }))] }), exactListings.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsxs(Heading, { level: 2, size: "lg", children: ["Buy this part", exactListingsTruncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", exactListings.length, "+)"] }))] }), _jsx(Stack, { gap: "xs", className: "min-w-0", children: exactListings.map((l) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", border: "subtle", className: "min-w-0", children: [_jsx(TextLink, { href: l.href, children: l.title }), l.condition && (_jsx(Text, { size: "xs", color: "muted", children: l.condition }))] }, l.id))) })] })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", className: "min-w-0", children: [_jsxs(Heading, { level: 2, size: "lg", children: ["Listings", listingsTruncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", listings.length, "+)"] }))] }), listings.length > 0 && (_jsx(TextLink, { href: allListingsHref, children: "See all listings \u2192" }))] }), listings.length === 0 ? (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { color: "muted", size: "sm", children: "Nobody is selling this part right now." }), _jsx(TextLink, { href: allListingsHref, children: "Search the catalogue \u2192" })] })) : (_jsx(Stack, { gap: "xs", className: "min-w-0", children: listings.map((l) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", border: "subtle", className: "min-w-0", children: [_jsx(TextLink, { href: l.href, children: l.title }), l.condition && (_jsx(Text, { size: "xs", color: "muted", children: l.condition }))] }, l.id))) }))] }), part.source && (_jsxs(Text, { size: "xs", color: "faint", children: ["Source: ", part.source] }))] }) }) }));
|
|
20
20
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D12 β the parts
|
|
2
|
+
* D12 β the Beyblade parts gallery index, and one classification's listing.
|
|
3
3
|
*
|
|
4
4
|
* ββ π THE INDEX COSTS ZERO FIRESTORE READS ββββββββββββββββββββββββββββββββ
|
|
5
5
|
*
|
|
@@ -20,8 +20,17 @@ import React from "react";
|
|
|
20
20
|
import { type PartGlossaryDocument, type PartSlot } from "../schemas/firestore";
|
|
21
21
|
export interface PartsIndexPageViewProps {
|
|
22
22
|
hrefForClassification: (slot: string) => string;
|
|
23
|
+
/**
|
|
24
|
+
* The search box, as a SLOT.
|
|
25
|
+
*
|
|
26
|
+
* π Not rendered directly, because this view is a Server Component and
|
|
27
|
+
* the box is a Client Component that fetches. Keeping it a slot also keeps
|
|
28
|
+
* the page's "ZERO Firestore reads" property literally true β the box
|
|
29
|
+
* reads a CDN object, never Firestore, and the index page stays static.
|
|
30
|
+
*/
|
|
31
|
+
renderSearch?: () => React.ReactNode;
|
|
23
32
|
}
|
|
24
|
-
export declare function PartsIndexPageView({ hrefForClassification }: PartsIndexPageViewProps): React.JSX.Element;
|
|
33
|
+
export declare function PartsIndexPageView({ hrefForClassification, renderSearch, }: PartsIndexPageViewProps): React.JSX.Element;
|
|
25
34
|
export interface PartClassificationPageViewProps {
|
|
26
35
|
slot: PartSlot;
|
|
27
36
|
parts: readonly PartGlossaryDocument[];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Section, Container, Stack, Row, Div, Grid, Heading, Text, Span, TextLink, Badge, } from "../../../ui/index.js";
|
|
3
3
|
import { ALL_PART_SLOTS, PART_SLOT_LABEL, } from "../schemas/firestore.js";
|
|
4
|
-
export function PartsIndexPageView({ hrefForClassification }) {
|
|
5
|
-
return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, children: "
|
|
4
|
+
export function PartsIndexPageView({ hrefForClassification, renderSearch, }) {
|
|
5
|
+
return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, children: "Beyblade parts gallery" }), _jsxs(Text, { color: "muted", children: ["Every component we have a record of, by what it is. A code like", " ", _jsx(Span, { weight: "semibold", children: "RF" }), " or ", _jsx(Span, { weight: "semibold", children: "105" }), " means different things in different generations, so parts are grouped by their classification rather than by code."] })] }), renderSearch?.(), _jsx(Grid, { cols: "navTiles", gap: "sm", className: "min-w-0", children: ALL_PART_SLOTS.map((slot) => (_jsx(Div, { surface: "card", padding: "sm", rounded: "md", border: "subtle", className: "min-w-0", children: _jsx(TextLink, { href: hrefForClassification(slot), children: PART_SLOT_LABEL[slot] }) }, slot))) })] }) }) }));
|
|
6
6
|
}
|
|
7
7
|
/** `performance-tip--rf` β `rf`. The id's tail IS the URL segment. */
|
|
8
8
|
function tailOf(partId) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PartsSearchBoxProps {
|
|
3
|
+
/** `siteSettings.search?.indexEnabled`. Read as `!== false`. */
|
|
4
|
+
indexEnabled?: boolean;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
/** Rows shown. A part search is a lookup, not a browse. */
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function PartsSearchBox({ indexEnabled, placeholder, limit, }: PartsSearchBoxProps): React.JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/*
|
|
4
|
+
* WHY: 752 parts were reachable only by drilling classification -> code.
|
|
5
|
+
* `/parts` had no search input anywhere.
|
|
6
|
+
* WHAT: A search box over the `part` slice of the local index. Zero Firestore
|
|
7
|
+
* reads, and it resolves the code ambiguity in the result row.
|
|
8
|
+
*
|
|
9
|
+
* ## π The ambiguity is the feature, not a problem to hide
|
|
10
|
+
*
|
|
11
|
+
* `route-map.ts` explains why a code alone cannot be a URL: "`105` is a Metal
|
|
12
|
+
* Fight spin-track height, a bare number in Burst is a disc, and `1-60` is a
|
|
13
|
+
* Beyblade X ratchet." So the honest answer to "105" is THREE results, each
|
|
14
|
+
* naming its classification and each linking to the right two-segment path β
|
|
15
|
+
* not one result picked by whatever sorted first.
|
|
16
|
+
*
|
|
17
|
+
* That is exactly what the index already carries: the part source puts the
|
|
18
|
+
* classification in `detail` for precisely this reason, and derives the slug
|
|
19
|
+
* by splitting the `${slot}--${code}` document id, which is the one
|
|
20
|
+
* derivation guaranteed to agree with the page that exists.
|
|
21
|
+
*
|
|
22
|
+
* ## It does not replace the drill-down
|
|
23
|
+
*
|
|
24
|
+
* The classification grid stays. Browsing by "what kind of thing is this" is
|
|
25
|
+
* a different question from "where is the part I can already name", and the
|
|
26
|
+
* grid is the only one of the two that works when you do not know the word.
|
|
27
|
+
*
|
|
28
|
+
* @tag domain:parts
|
|
29
|
+
* @tag layer:feature
|
|
30
|
+
* @tag pattern:none
|
|
31
|
+
* @tag access:client
|
|
32
|
+
* @tag consumers:app/[locale]/parts/page.tsx
|
|
33
|
+
* @tag sideEffects:network
|
|
34
|
+
*/
|
|
35
|
+
import React from "react";
|
|
36
|
+
import { Div, Input, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
37
|
+
import { useSiteSearch } from "../../search/hooks/useSiteSearch.js";
|
|
38
|
+
/** Only this slice of the index. See `resolveEntity`'s `types` parameter. */
|
|
39
|
+
const PART_TYPES = ["part"];
|
|
40
|
+
/** Stable identity β a `useMemo` dependency inside the hook. */
|
|
41
|
+
const partLabel = () => "Parts";
|
|
42
|
+
export function PartsSearchBox({ indexEnabled, placeholder = "Search parts by name or code β RF, 105, Rubber Flatβ¦", limit = 10, }) {
|
|
43
|
+
const [query, setQuery] = React.useState("");
|
|
44
|
+
const [active, setActive] = React.useState(false);
|
|
45
|
+
const { status, result } = useSiteSearch(query, {
|
|
46
|
+
active,
|
|
47
|
+
indexEnabled,
|
|
48
|
+
types: PART_TYPES,
|
|
49
|
+
label: partLabel,
|
|
50
|
+
});
|
|
51
|
+
const hits = result?.groups[0]?.hits ?? [];
|
|
52
|
+
const typed = query.trim().length > 0;
|
|
53
|
+
return (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Input, { type: "search", value: query, placeholder: placeholder, "aria-label": "Search parts", onFocus: () => setActive(true), onChange: (event) => {
|
|
54
|
+
setActive(true);
|
|
55
|
+
setQuery(event.target.value);
|
|
56
|
+
} }), typed && status === "loading" && _jsx(Text, { color: "muted", children: "Searching\u2026" }), typed && (status === "failed" || status === "disabled") && (_jsx(Text, { size: "sm", color: "muted", children: "Part search is unavailable right now \u2014 browse by classification below." })), typed && status === "ready" && hits.length === 0 && (_jsx(Text, { color: "muted", children: "No part matches that name or code." })), hits.length > 0 && (_jsx(Stack, { gap: "xs", className: "min-w-0", children: hits.slice(0, limit).map((hit) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", border: "subtle", children: [_jsx(TextLink, { href: hit.href, children: hit.title }), hit.detail && (_jsx(Text, { size: "xs", color: "muted", children: hit.detail }))] }, hit.id))) }))] }));
|
|
57
|
+
}
|