@mohasinac/appkit 4.13.0 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +14 -5
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +3 -0
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +6 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +8 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +4 -1
- package/dist/_internal/client/features/lottery/LotteryListView.d.ts +7 -0
- package/dist/_internal/client/features/lottery/LotteryListView.js +1 -1
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +4 -1
- package/dist/_internal/client/features/lottery/slot-collage-items.d.ts +15 -0
- package/dist/_internal/client/features/lottery/slot-collage-items.js +13 -0
- package/dist/_internal/server/features/auctions/actions.js +6 -0
- package/dist/_internal/server/features/bundles/data.d.ts +16 -0
- package/dist/_internal/server/features/bundles/data.js +26 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -0
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +38 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +171 -0
- package/dist/_internal/server/features/homepage/index.d.ts +0 -1
- package/dist/_internal/server/features/homepage/index.js +7 -1
- package/dist/_internal/server/features/lottery/adapters.js +4 -0
- package/dist/_internal/server/features/products/data.js +15 -37
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +46 -20
- package/dist/_internal/server/features/products/list-public.js +266 -90
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +44 -0
- package/dist/_internal/server/features/products/listing-tab-counts.js +97 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +51 -0
- package/dist/_internal/server/features/site-settings/adapters.js +75 -0
- package/dist/_internal/server/features/site-settings/index.d.ts +1 -0
- package/dist/_internal/server/features/site-settings/index.js +1 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +56 -0
- package/dist/_internal/server/features/stores/adapters.js +91 -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/jobs/core/auctionSettlement.js +56 -2
- package/dist/_internal/server/jobs/core/offerExpiry.js +45 -9
- package/dist/_internal/server/jobs/core/paymentReviewAutoApprove.d.ts +5 -0
- package/dist/_internal/server/jobs/core/paymentReviewAutoApprove.js +33 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +7 -0
- package/dist/_internal/server/jobs/handlers/messages.js +10 -0
- package/dist/_internal/shared/actions/action-registry.js +15 -1
- package/dist/_internal/shared/checkout/lanes.d.ts +25 -0
- package/dist/_internal/shared/checkout/lanes.js +26 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +27 -0
- package/dist/_internal/shared/features/auctions/config.js +27 -0
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/cart/schema.d.ts +12 -12
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +32 -32
- package/dist/_internal/shared/features/emi/schedule.d.ts +24 -3
- package/dist/_internal/shared/features/emi/schedule.js +20 -5
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/homepage/section-gate.d.ts +14 -0
- package/dist/_internal/shared/features/homepage/section-gate.js +51 -0
- package/dist/_internal/shared/features/offers/config.d.ts +30 -0
- package/dist/_internal/shared/features/offers/config.js +59 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.d.ts +32 -32
- package/dist/_internal/shared/features/products/to-product-item.js +6 -0
- package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
- package/dist/_internal/shared/features/site-settings/fees.d.ts +60 -0
- package/dist/_internal/shared/features/site-settings/fees.js +39 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +40 -0
- package/dist/_internal/shared/features/site-settings/types.js +1 -0
- package/dist/_internal/shared/history/history.d.ts +66 -0
- package/dist/_internal/shared/history/history.js +158 -0
- package/dist/_internal/shared/history/index.d.ts +4 -0
- package/dist/_internal/shared/history/index.js +2 -0
- package/dist/_internal/shared/history/types.d.ts +61 -0
- package/dist/_internal/shared/history/types.js +6 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +54 -1
- package/dist/_internal/shared/listing-types/_registry.js +67 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/art/config.js +5 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +13 -0
- package/dist/_internal/shared/listing-types/auction/config.js +21 -0
- package/dist/_internal/shared/listing-types/availability.d.ts +79 -0
- package/dist/_internal/shared/listing-types/availability.js +104 -0
- package/dist/_internal/shared/listing-types/capabilities.d.ts +15 -0
- package/dist/_internal/shared/listing-types/capabilities.js +12 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/classified/config.js +5 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +11 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +22 -2
- package/dist/_internal/shared/listing-types/live/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/live/config.js +5 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +19 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +11 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +15 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/standard/config.js +5 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +5 -0
- package/dist/client.d.ts +7 -5
- package/dist/client.js +2 -2
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/constants/field-names.d.ts +49 -2
- package/dist/constants/field-names.js +53 -2
- package/dist/constants/table-keys.d.ts +17 -0
- package/dist/constants/table-keys.js +17 -0
- package/dist/errors/error-codes.d.ts +13 -1
- package/dist/errors/error-codes.js +13 -1
- package/dist/errors/error-display-map.js +12 -3
- package/dist/errors/messages.d.ts +10 -2
- package/dist/errors/messages.js +10 -2
- package/dist/features/about/components/AboutView.d.ts +3 -0
- package/dist/features/about/components/AboutView.js +3 -2
- package/dist/features/about/components/PolicyPageView.d.ts +2 -1
- package/dist/features/about/components/PolicyPageView.js +61 -23
- package/dist/features/about/components/PublicProfileView.js +5 -0
- package/dist/features/about/schemas/firestore.d.ts +3 -0
- package/dist/features/account/components/UserOrdersView.js +7 -0
- package/dist/features/account/components/index.d.ts +0 -2
- package/dist/features/account/components/index.js +0 -1
- package/dist/features/account/schemas/index.d.ts +2 -2
- package/dist/features/account/types/index.d.ts +21 -2
- package/dist/features/addresses/schemas/index.d.ts +4 -4
- package/dist/features/admin/components/AdminArtView.js +8 -3
- package/dist/features/admin/components/AdminClassifiedView.js +8 -3
- package/dist/features/admin/components/AdminDigitalCodesView.js +8 -3
- package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -0
- package/dist/features/admin/components/AdminLiveView.js +8 -3
- package/dist/features/admin/components/AdminOffersView.d.ts +4 -0
- package/dist/features/admin/components/AdminOffersView.js +186 -0
- package/dist/features/admin/components/AdminOrdersView.js +10 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +5 -0
- package/dist/features/admin/components/AdminProductsView.js +16 -29
- package/dist/features/admin/components/AdminSiteSettingsView.js +31 -13
- package/dist/features/admin/components/AdminSiteView.js +6 -2
- package/dist/features/admin/components/AdminStickersView.js +8 -3
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +86 -0
- package/dist/features/admin/constants/filter-tabs.js +55 -0
- package/dist/features/admin/schemas/firestore.d.ts +122 -3
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +20 -5
- package/dist/features/auctions/actions/bid-actions.js +93 -20
- package/dist/features/auctions/components/AuctionDetailPageView.js +11 -6
- package/dist/features/auctions/components/AuctionsListView.js +5 -6
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +7 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +19 -2
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +8 -4
- package/dist/features/auctions/repository/bid.repository.d.ts +13 -0
- package/dist/features/auctions/repository/bid.repository.js +23 -0
- package/dist/features/auctions/schemas/firestore.d.ts +17 -0
- package/dist/features/auctions/schemas/index.d.ts +12 -12
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +10 -1
- package/dist/features/blog/actions/blog-actions.d.ts +7 -7
- package/dist/features/blog/schemas/index.d.ts +6 -6
- package/dist/features/cart/repository/cart.repository.js +1 -0
- package/dist/features/cart/schemas/firestore.d.ts +15 -0
- package/dist/features/catalogue/schemas/validation.d.ts +4 -4
- package/dist/features/categories/components/BrandDetailPageView.js +23 -40
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -7
- package/dist/features/categories/components/BrandDetailTabs.js +8 -12
- package/dist/features/categories/components/BundleCollage.js +1 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +53 -57
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +10 -8
- package/dist/features/categories/components/CategoryDetailTabs.js +11 -16
- package/dist/features/categories/components/CategoryProductsListing.js +11 -1
- package/dist/features/categories/components/ConcernCard.js +7 -1
- package/dist/features/categories/schemas/firestore.d.ts +13 -0
- package/dist/features/categories/schemas/index.d.ts +26 -26
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +16 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +23 -3
- package/dist/features/events/components/AdminEventsView.js +15 -11
- package/dist/features/events/components/EventFilters.d.ts +9 -1
- package/dist/features/events/components/EventFilters.js +45 -21
- package/dist/features/events/components/EventsIndexListing.d.ts +6 -1
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/components/EventsListPageView.js +13 -4
- package/dist/features/events/repository/events.repository.d.ts +17 -0
- package/dist/features/events/repository/events.repository.js +27 -0
- package/dist/features/events/schemas/firestore.d.ts +12 -0
- package/dist/features/events/schemas/firestore.js +12 -0
- package/dist/features/events/schemas/index.d.ts +50 -50
- package/dist/features/events/types/index.d.ts +11 -0
- package/dist/features/events/types/index.js +57 -1
- package/dist/features/faq/actions/faq-actions.d.ts +12 -12
- package/dist/features/faq/schemas/index.d.ts +8 -8
- package/dist/features/filters/FilterFacetSection.d.ts +7 -1
- package/dist/features/filters/FilterFacetSection.js +11 -3
- package/dist/features/grouped/schemas/index.d.ts +4 -4
- package/dist/features/homepage/components/AnnouncementBar.d.ts +7 -1
- package/dist/features/homepage/components/AnnouncementBar.js +3 -3
- package/dist/features/homepage/components/MarketplaceHomepageView.js +20 -6
- package/dist/features/homepage/hooks/homepage-query.d.ts +16 -0
- package/dist/features/homepage/hooks/homepage-query.js +45 -0
- package/dist/features/homepage/hooks/useFeaturedAuctions.d.ts +10 -0
- package/dist/features/homepage/hooks/useFeaturedAuctions.js +28 -8
- package/dist/features/homepage/hooks/useFeaturedPreOrders.js +17 -8
- package/dist/features/homepage/hooks/useFeaturedProducts.js +15 -8
- package/dist/features/homepage/schemas/index.d.ts +2 -2
- package/dist/features/layout/AppLayoutShell.d.ts +11 -1
- package/dist/features/layout/AppLayoutShell.js +15 -14
- package/dist/features/layout/BackToTop.d.ts +9 -0
- package/dist/features/layout/BackToTop.js +10 -1
- package/dist/features/layout/BottomActions.js +28 -25
- package/dist/features/layout/BottomChrome.d.ts +34 -0
- package/dist/features/layout/BottomChrome.js +55 -0
- package/dist/features/layout/BottomNavLayout.d.ts +6 -0
- package/dist/features/layout/BottomNavLayout.js +7 -1
- package/dist/features/layout/BottomNavbar.js +15 -2
- package/dist/features/layout/FooterLayout.js +11 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +7 -0
- package/dist/features/layout/TitleBarLayout.js +28 -5
- package/dist/features/layout/index.d.ts +2 -0
- package/dist/features/layout/index.js +1 -0
- package/dist/features/lottery/types/index.d.ts +3 -0
- package/dist/features/media/MediaPickerModal.js +5 -1
- package/dist/features/messages/schemas/index.d.ts +2 -2
- package/dist/features/orders/components/OrderScopeTabs.d.ts +20 -0
- package/dist/features/orders/components/OrderScopeTabs.js +31 -0
- package/dist/features/orders/constants/order-scope.d.ts +48 -0
- package/dist/features/orders/constants/order-scope.js +111 -0
- package/dist/features/orders/repository/orders.repository.d.ts +54 -7
- package/dist/features/orders/repository/orders.repository.js +116 -22
- package/dist/features/orders/schemas/firestore.d.ts +117 -0
- package/dist/features/orders/schemas/firestore.js +30 -0
- package/dist/features/orders/schemas/index.d.ts +18 -18
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +14 -9
- package/dist/features/pre-orders/components/PreOrdersListView.js +6 -5
- package/dist/features/pre-orders/schemas/index.d.ts +1 -1
- package/dist/features/products/actions/product-actions.d.ts +14 -0
- package/dist/features/products/actions/product-actions.js +49 -11
- package/dist/features/products/api/route.d.ts +12 -0
- package/dist/features/products/api/route.js +42 -100
- package/dist/features/products/components/ArtStickersListView.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +13 -11
- package/dist/features/products/components/AvailabilityTabs.d.ts +11 -0
- package/dist/features/products/components/AvailabilityTabs.js +47 -0
- package/dist/features/products/components/MakeOfferButton.js +48 -49
- package/dist/features/products/components/PrizeDrawCollage.d.ts +20 -4
- package/dist/features/products/components/PrizeDrawCollage.js +9 -1
- package/dist/features/products/components/PrizeDrawsIndexListing.js +14 -12
- package/dist/features/products/components/PrizeDrawsListingView.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.d.ts +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +23 -18
- package/dist/features/products/components/ProductDetailPageView.d.ts +4 -1
- package/dist/features/products/components/ProductDetailPageView.js +18 -4
- package/dist/features/products/components/ProductForm.js +5 -2
- package/dist/features/products/components/ProductGrid.js +6 -1
- package/dist/features/products/components/ProductsIndexListing.js +22 -32
- package/dist/features/products/components/ProductsIndexPageView.js +6 -9
- package/dist/features/products/components/index.d.ts +2 -2
- package/dist/features/products/components/index.js +1 -1
- package/dist/features/products/config/listing-type-listing-config.d.ts +20 -0
- package/dist/features/products/config/listing-type-listing-config.js +22 -10
- package/dist/features/products/constants/action-defs.d.ts +2 -0
- package/dist/features/products/constants/action-defs.js +6 -0
- package/dist/features/products/hooks/useAvailabilityScope.d.ts +13 -0
- package/dist/features/products/hooks/useAvailabilityScope.js +19 -0
- package/dist/features/products/hooks/useProducts.d.ts +5 -0
- package/dist/features/products/hooks/useProducts.js +13 -2
- package/dist/features/products/repository/products.repository.d.ts +31 -2
- package/dist/features/products/repository/products.repository.js +53 -21
- package/dist/features/products/schemas/firestore.d.ts +17 -3
- package/dist/features/products/schemas/firestore.js +7 -2
- package/dist/features/products/schemas/index.d.ts +66 -66
- package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
- package/dist/features/products/types/index.d.ts +16 -1
- package/dist/features/promotions/schemas/index.d.ts +2 -2
- package/dist/features/reviews/schemas/index.d.ts +16 -16
- package/dist/features/search/schemas/index.d.ts +4 -4
- package/dist/features/seller/actions/offer-actions.js +25 -2
- package/dist/features/seller/components/SellerArtView.js +5 -0
- package/dist/features/seller/components/SellerAuctionsView.js +13 -15
- package/dist/features/seller/components/SellerClassifiedView.js +5 -0
- package/dist/features/seller/components/SellerDigitalCodesView.js +5 -0
- package/dist/features/seller/components/SellerLiveView.js +5 -0
- package/dist/features/seller/components/SellerOrdersView.js +31 -9
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -18
- package/dist/features/seller/components/SellerPreOrdersView.js +5 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +14 -16
- package/dist/features/seller/components/SellerProductShell.js +7 -1
- package/dist/features/seller/components/SellerProductsView.js +11 -8
- package/dist/features/seller/components/SellerStickersView.js +5 -0
- package/dist/features/seller/components/index.d.ts +0 -2
- package/dist/features/seller/components/index.js +0 -1
- package/dist/features/seller/repository/offer.repository.d.ts +73 -16
- package/dist/features/seller/repository/offer.repository.js +38 -3
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/seller/schemas/offer-forms.d.ts +25 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/seller/server.d.ts +0 -1
- package/dist/features/seller/server.js +0 -1
- package/dist/features/shell/SectionForm.d.ts +99 -0
- package/dist/features/shell/SectionForm.js +128 -0
- package/dist/features/shell/entity-form.d.ts +109 -0
- package/dist/features/shell/entity-form.js +96 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/stores/api/[storeSlug]/route.js +2 -25
- package/dist/features/stores/components/StoreAuctionsListing.js +15 -1
- package/dist/features/stores/components/StoreClassifiedsListing.js +12 -2
- package/dist/features/stores/components/StoreDetailLayoutView.js +24 -71
- package/dist/features/stores/components/StoreDigitalCodesListing.js +12 -2
- package/dist/features/stores/components/StoreLiveItemsListing.js +12 -2
- package/dist/features/stores/components/StorePreOrdersListing.js +12 -1
- package/dist/features/stores/components/StoreProductsListing.js +11 -1
- package/dist/features/stores/repository/store.repository.js +6 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/schemas/index.d.ts +4 -4
- package/dist/features/tester/actions/checklist-item-actions.d.ts +4 -4
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +119 -3
- package/dist/features/tester/seed-data/products-tester-seed-data.js +131 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +105 -1
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +13 -10
- package/dist/index.js +14 -9
- package/dist/next/routing/route-map.d.ts +7 -1
- package/dist/next/routing/route-map.js +5 -0
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +50 -1
- package/dist/providers/db-firebase/index.d.ts +1 -1
- package/dist/providers/db-firebase/index.js +1 -1
- package/dist/providers/db-firebase/sieve.d.ts +25 -0
- package/dist/providers/db-firebase/sieve.js +52 -18
- package/dist/react/hooks/useUrlTable.js +10 -1
- package/dist/schemas/registry.d.ts +68 -68
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +309 -0
- package/dist/seed/_helpers/category-tree.d.ts +27 -0
- package/dist/seed/_helpers/category-tree.js +103 -0
- package/dist/seed/catalogue-seed-data.js +12 -12
- package/dist/seed/categories-seed-data.js +177 -162
- package/dist/seed/events-seed-data.js +61 -10
- package/dist/seed/homepage-sections-seed-data.js +5 -1
- package/dist/seed/offers-seed-data.js +32 -27
- package/dist/seed/products-art-seed-data.js +50 -12
- package/dist/seed/products-auctions-seed-data.js +97 -18
- package/dist/seed/products-classifieds-seed-data.js +80 -16
- package/dist/seed/products-digital-codes-seed-data.js +66 -18
- package/dist/seed/products-live-items-seed-data.js +64 -13
- package/dist/seed/products-preorders-seed-data.d.ts +0 -1
- package/dist/seed/products-preorders-seed-data.js +52 -13
- package/dist/seed/products-prize-draws-seed-data.js +63 -14
- package/dist/seed/products-standard-seed-data.js +99 -26
- package/dist/seed/products-stickers-seed-data.js +47 -12
- package/dist/seed/reviews-seed-data.js +6 -1
- package/dist/seed/site-settings-seed-data.d.ts +1 -38
- package/dist/seed/site-settings-seed-data.js +38 -76
- package/dist/seed/stores-seed-data.js +71 -1
- package/dist/seed/users-seed-data.js +15 -5
- package/dist/server-entry.d.ts +1 -2
- package/dist/server-entry.js +4 -3
- package/dist/server.d.ts +4 -3
- package/dist/server.js +8 -4
- package/dist/styles.css +78 -12
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.style.css +19 -4
- package/dist/ui/components/CollapsibleSection.d.ts +17 -1
- package/dist/ui/components/CollapsibleSection.js +6 -2
- package/dist/ui/components/FilterChipGroup.d.ts +25 -1
- package/dist/ui/components/FilterChipGroup.js +21 -4
- package/dist/ui/components/HandMode.style.css +47 -0
- package/dist/ui/components/Layout.d.ts +9 -2
- package/dist/ui/components/Layout.js +1 -1
- package/dist/ui/components/ListingLayout.js +8 -8
- package/dist/ui/components/ListingLayout.style.css +8 -7
- package/dist/ui/components/StickyToolbar.d.ts +3 -1
- package/dist/ui/components/StickyToolbar.js +62 -20
- package/dist/ui/components/bottom-chrome-slot.d.ts +21 -0
- package/dist/ui/components/bottom-chrome-slot.js +29 -0
- package/dist/ui/components/index.style.css +3 -0
- package/dist/ui/forms/FormShell.d.ts +43 -13
- package/dist/ui/forms/FormShell.js +22 -11
- package/dist/ui/forms/index.d.ts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/package.json +2 -2
|
@@ -10,13 +10,21 @@
|
|
|
10
10
|
*
|
|
11
11
|
* WHAT: `parsePublicProductParams` (URL/searchParams -> typed input) and
|
|
12
12
|
* `listPublicProducts` (typed input -> page of documents). Firestore-safe
|
|
13
|
-
* clauses go into the query;
|
|
14
|
-
*
|
|
15
|
-
* requested order and re-paginated.
|
|
13
|
+
* clauses go into the query; the availability scope and unsafe date
|
|
14
|
+
* ranges are applied over bounded fetches, then re-sorted into the
|
|
15
|
+
* caller's requested order and re-paginated. See the comment block above
|
|
16
|
+
* `listPublicProducts` for why the three scopes execute differently.
|
|
17
|
+
*
|
|
18
|
+
* Since 2026-08-24 this also backs the admin/seller dashboards, via the
|
|
19
|
+
* `ANY_STATUS` sentinel — so a dashboard and a public page filtering the
|
|
20
|
+
* same listing type issue byte-identical queries instead of two
|
|
21
|
+
* hand-rolled filter builders that drift.
|
|
16
22
|
*
|
|
17
23
|
* EXPORTS:
|
|
18
24
|
* PublicProductListInput, PublicProductListResult, PublicProductExecutor,
|
|
19
|
-
* PublicProductListOptions, parsePublicProductParams, listPublicProducts
|
|
25
|
+
* PublicProductListOptions, parsePublicProductParams, listPublicProducts,
|
|
26
|
+
* listStoreProducts, defaultAvailabilityForListingTypes, ANY_STATUS,
|
|
27
|
+
* PUBLIC_PRODUCT_MAX_PAGE_SIZE
|
|
20
28
|
*
|
|
21
29
|
* @tag domain:products
|
|
22
30
|
* @tag layer:server-data
|
|
@@ -33,11 +41,19 @@ import { TABLE_KEYS } from "../../../../constants/table-keys";
|
|
|
33
41
|
import { sortBy } from "../../../../constants/sort";
|
|
34
42
|
import { SIEVE_OP, sieveAnd, sieveFilter, expandSieveParam, } from "../../../../utils/sieve-builder";
|
|
35
43
|
import { ALL_LISTING_TYPES } from "../../../shared/listing-types/feature-flags";
|
|
36
|
-
import {
|
|
44
|
+
import { isListingRowAvailable, unavailableClausesFor, } from "../../../shared/listing-types/_registry";
|
|
45
|
+
import { AVAILABILITY_VALUES, isAvailabilityFilter, } from "../../../../constants/field-names";
|
|
37
46
|
/** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
|
|
38
47
|
export const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
39
48
|
/**
|
|
40
|
-
*
|
|
49
|
+
* `status` sentinel meaning "every publication state". Only the authenticated
|
|
50
|
+
* admin/seller dashboards may pass it — a public surface that did would leak
|
|
51
|
+
* drafts. A sentinel rather than `status: undefined` because undefined already
|
|
52
|
+
* means "use the safe default", and conflating the two is how a leak ships.
|
|
53
|
+
*/
|
|
54
|
+
export const ANY_STATUS = "__any__";
|
|
55
|
+
/**
|
|
56
|
+
* The availability scope a listing surface starts in.
|
|
41
57
|
*
|
|
42
58
|
* THE POINT OF THIS FUNCTION is that SSR and the client refetch call the SAME
|
|
43
59
|
* implementation. `staleTime: Infinity` freezes SSR `initialData` forever, so
|
|
@@ -46,21 +62,19 @@ export const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
|
46
62
|
* them is wrong — permanently (Root Cause #30). Two mirrored literals is
|
|
47
63
|
* exactly how that bug is written; one shared function is how it isn't.
|
|
48
64
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
65
|
+
* It is a constant today rather than a per-type decision — every listing
|
|
66
|
+
* surface opens on what a shopper can actually buy. It stays a function
|
|
67
|
+
* because that is the seam a surface would override through, and because
|
|
68
|
+
* `audit-listing-filter-parity` asserts on this name to prove every SSR view
|
|
69
|
+
* derives its default rather than hard-coding one (two of them used to).
|
|
53
70
|
*/
|
|
54
|
-
export function
|
|
55
|
-
|
|
71
|
+
export function defaultAvailabilityForListingTypes(_types) {
|
|
72
|
+
return { availability: AVAILABILITY_VALUES.AVAILABLE };
|
|
73
|
+
}
|
|
74
|
+
/** Narrow arbitrary strings to real listing types, dropping the rest. */
|
|
75
|
+
function asListingTypes(types) {
|
|
56
76
|
const effective = types && types.length > 0 ? types : ALL_LISTING_TYPES;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
hideSoldByDefault: defaults.includes("sold"),
|
|
60
|
-
// "ended" and "closed" are the same query mechanic (a date range against
|
|
61
|
-
// the type's own end field); they differ only in the toggle's wording.
|
|
62
|
-
hideEndedByDefault: defaults.includes("ended") || defaults.includes("closed"),
|
|
63
|
-
};
|
|
77
|
+
return effective.filter((t) => ALL_LISTING_TYPES.includes(t));
|
|
64
78
|
}
|
|
65
79
|
const DEFAULT_PAGE = 1;
|
|
66
80
|
const DEFAULT_PAGE_SIZE = 24;
|
|
@@ -114,6 +128,29 @@ function pipeList(raw) {
|
|
|
114
128
|
const parts = raw.split("|").map((s) => s.trim()).filter(Boolean);
|
|
115
129
|
return parts.length > 0 ? parts : undefined;
|
|
116
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Read the availability scope, with a back-compat mapping for the three URL
|
|
133
|
+
* spellings this replaced.
|
|
134
|
+
*
|
|
135
|
+
* An unrecognised token falls back to the caller's default rather than being
|
|
136
|
+
* passed through — a raw query param must never reach a Firestore value.
|
|
137
|
+
*/
|
|
138
|
+
function readAvailability(get, fallback) {
|
|
139
|
+
const raw = get(TABLE_KEYS.AVAILABILITY);
|
|
140
|
+
if (raw && isAvailabilityFilter(raw))
|
|
141
|
+
return raw;
|
|
142
|
+
if (raw)
|
|
143
|
+
return fallback;
|
|
144
|
+
// `showSold` / `showEnded` / `showClosed` each meant "stop filtering", so
|
|
145
|
+
// they map to `all`, NOT to `unavailable`. Kept only so links shared before
|
|
146
|
+
// 2026-08-24 still resolve to something sensible; nothing writes them.
|
|
147
|
+
if (get(TABLE_KEYS.SHOW_SOLD) === "true" ||
|
|
148
|
+
get(TABLE_KEYS.SHOW_ENDED) === "true" ||
|
|
149
|
+
get(TABLE_KEYS.SHOW_CLOSED) === "true") {
|
|
150
|
+
return AVAILABILITY_VALUES.ALL;
|
|
151
|
+
}
|
|
152
|
+
return fallback;
|
|
153
|
+
}
|
|
117
154
|
/**
|
|
118
155
|
* Read the shared public-listing query params. SSR views hand in Next's
|
|
119
156
|
* `searchParams` record; the API route hands in `url.searchParams`. Both produce
|
|
@@ -160,21 +197,12 @@ export function parsePublicProductParams(params, defaults) {
|
|
|
160
197
|
typeFacets: readTypeFacets(get),
|
|
161
198
|
preOrderProductionStatus: get(TABLE_KEYS.PREORDER_STATUS) || get("preOrderStatus") || undefined,
|
|
162
199
|
prizeRevealStatus: get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined,
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
: get(TABLE_KEYS.IN_STOCK)
|
|
170
|
-
? get(TABLE_KEYS.IN_STOCK) === "true"
|
|
171
|
-
: defaults?.hideSoldByDefault
|
|
172
|
-
? true
|
|
173
|
-
: undefined,
|
|
174
|
-
dateFrom: get(TABLE_KEYS.DATE_FROM) ||
|
|
175
|
-
(defaults?.hideEndedByDefault && get(TABLE_KEYS.SHOW_ENDED) !== "true"
|
|
176
|
-
? new Date().toISOString()
|
|
177
|
-
: undefined),
|
|
200
|
+
availability: readAvailability(get, defaults?.availability),
|
|
201
|
+
// Now a plain drawer facet again. It used to double as the hide-ended
|
|
202
|
+
// mechanism, which is why it only worked when exactly one listing type was
|
|
203
|
+
// selected — `dateFieldFor` returns null otherwise, so /products silently
|
|
204
|
+
// showed ended auctions in its default view.
|
|
205
|
+
dateFrom: get(TABLE_KEYS.DATE_FROM) || undefined,
|
|
178
206
|
dateTo: get(TABLE_KEYS.DATE_TO) || undefined,
|
|
179
207
|
page: num(get(TABLE_KEYS.PAGE)) ?? DEFAULT_PAGE,
|
|
180
208
|
pageSize: num(get(TABLE_KEYS.PAGE_SIZE)) ?? defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
@@ -196,7 +224,10 @@ function buildFirestoreSafeFilters(input) {
|
|
|
196
224
|
const parts = [];
|
|
197
225
|
// Published-only unless the caller explicitly asks otherwise. Without this
|
|
198
226
|
// default, any refetch that omits `status` leaks drafts (Root Cause #30).
|
|
199
|
-
|
|
227
|
+
// ANY_STATUS is the one opt-out, reserved for the authenticated dashboards.
|
|
228
|
+
if (input.status !== ANY_STATUS) {
|
|
229
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, input.status || PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED));
|
|
230
|
+
}
|
|
200
231
|
const types = input.listingTypes?.filter(Boolean) ?? [];
|
|
201
232
|
if (types.length > 0) {
|
|
202
233
|
parts.push(sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, types.join("|")));
|
|
@@ -308,72 +339,188 @@ async function defaultExecutor(query) {
|
|
|
308
339
|
cursor: null,
|
|
309
340
|
};
|
|
310
341
|
}
|
|
342
|
+
/** Render one `UnavailableClause` as a Sieve clause against a fixed `now`. */
|
|
343
|
+
function clauseToSieve(clause, now) {
|
|
344
|
+
const value = clause.value === "NOW" ? now.toISOString() : clause.value;
|
|
345
|
+
return clause.op === "lt"
|
|
346
|
+
? sieveFilter(clause.field, SIEVE_OP.LT, value)
|
|
347
|
+
: sieveFilter(clause.field, SIEVE_OP.EQ, value);
|
|
348
|
+
}
|
|
311
349
|
// ---------------------------------------------------------------------------
|
|
312
350
|
// The one public listing query
|
|
313
351
|
// ---------------------------------------------------------------------------
|
|
352
|
+
//
|
|
353
|
+
// AVAILABILITY IS ASYMMETRIC, and the asymmetry is what makes this tractable.
|
|
354
|
+
//
|
|
355
|
+
// "unavailable" is SPARSE and expressible as an OR of EQUALITIES — sold out,
|
|
356
|
+
// ended, closed, depleted. Firestore cannot OR across different fields, but
|
|
357
|
+
// it can run each equality as its own bounded query, and merging N small
|
|
358
|
+
// result sets in memory is cheap. That is Path C.
|
|
359
|
+
//
|
|
360
|
+
// "available" is DENSE and NEGATION-shaped: NOT(a OR b OR c). There is no
|
|
361
|
+
// query for that, so it stays what it has always been — one bounded window
|
|
362
|
+
// in the caller's own sort order, filtered per row. That is fine precisely
|
|
363
|
+
// because it is dense: nearly every row in the window passes.
|
|
364
|
+
//
|
|
365
|
+
// Path A ("all") applies no predicate at all and is therefore the only scope
|
|
366
|
+
// with true Firestore pagination and an exact total.
|
|
314
367
|
export async function listPublicProducts(input, opts) {
|
|
368
|
+
const now = opts?.now ?? new Date();
|
|
315
369
|
const page = Math.max(1, input.page ?? DEFAULT_PAGE);
|
|
316
370
|
const pageSize = Math.min(PUBLIC_PRODUCT_MAX_PAGE_SIZE, Math.max(1, input.pageSize ?? DEFAULT_PAGE_SIZE));
|
|
317
371
|
const sorts = input.sorts || DEFAULT_SORTS;
|
|
318
372
|
const requestedSortField = sorts.replace(/^-/, "");
|
|
373
|
+
const sortDesc = sorts.startsWith("-");
|
|
374
|
+
const availability = input.availability ?? AVAILABILITY_VALUES.ALL;
|
|
375
|
+
const wantAvailable = availability === AVAILABILITY_VALUES.AVAILABLE;
|
|
376
|
+
const wantUnavailable = availability === AVAILABILITY_VALUES.UNAVAILABLE;
|
|
319
377
|
const dateField = dateFieldFor(input.listingTypes);
|
|
320
378
|
const hasDateRange = Boolean((input.dateFrom || input.dateTo) && dateField);
|
|
321
|
-
// Firestore DOES accept one inequality when the query sorts by that same
|
|
322
|
-
//
|
|
323
|
-
const canPushDate = hasDateRange && !
|
|
379
|
+
// Firestore DOES accept one inequality when the query sorts by that same
|
|
380
|
+
// field, so a matching date facet can still be pushed down.
|
|
381
|
+
const canPushDate = hasDateRange && !wantAvailable && requestedSortField === dateField;
|
|
324
382
|
const safeFilters = buildFirestoreSafeFilters(input);
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
383
|
+
const dateClauses = [
|
|
384
|
+
...(canPushDate && input.dateFrom
|
|
385
|
+
? [sieveFilter(dateField, SIEVE_OP.GTE, input.dateFrom)]
|
|
386
|
+
: []),
|
|
387
|
+
...(canPushDate && input.dateTo
|
|
388
|
+
? [sieveFilter(dateField, SIEVE_OP.LTE, input.dateTo)]
|
|
389
|
+
: []),
|
|
390
|
+
];
|
|
391
|
+
const executor = opts?.executor ?? defaultExecutor;
|
|
392
|
+
const types = asListingTypes(input.listingTypes);
|
|
393
|
+
// ── Path B: one type whose unavailability IS a pushable inequality, and the
|
|
394
|
+
// caller is already sorting by that field. Ended auctions sorted by end
|
|
395
|
+
// date is the whole reason this branch exists — it gives the archive real,
|
|
396
|
+
// unbounded-depth Firestore pagination instead of a 50-row window.
|
|
397
|
+
const pushdown = wantUnavailable && types.length === 1 && !hasDateRange
|
|
398
|
+
? unavailableClausesFor(types).find((c) => c.op === "lt" && c.sortField === requestedSortField)
|
|
399
|
+
: undefined;
|
|
400
|
+
if (pushdown) {
|
|
401
|
+
const filters = sieveAnd(safeFilters, clauseToSieve(pushdown, now));
|
|
402
|
+
const result = await runQuery(executor, {
|
|
403
|
+
filters,
|
|
404
|
+
sorts,
|
|
405
|
+
page,
|
|
406
|
+
pageSize,
|
|
407
|
+
cursor: input.cursor ?? null,
|
|
408
|
+
});
|
|
409
|
+
if (!result)
|
|
410
|
+
return null;
|
|
411
|
+
return finish({
|
|
412
|
+
items: result.items,
|
|
413
|
+
total: result.total,
|
|
414
|
+
page: result.page,
|
|
415
|
+
pageSize,
|
|
416
|
+
totalPages: result.totalPages,
|
|
417
|
+
hasMore: result.hasMore,
|
|
418
|
+
cursor: result.cursor ?? null,
|
|
419
|
+
truncated: false,
|
|
420
|
+
filters,
|
|
421
|
+
sorts,
|
|
422
|
+
enabled: opts?.enabledListingTypes,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
// ── Path C: unavailable, everything else. One bounded query per distinct
|
|
426
|
+
// clause, run in parallel, merged and deduped. Nine listing types collapse
|
|
427
|
+
// to five clauses, so this is 5 × 50 = 250 documents in a single round —
|
|
428
|
+
// well inside Rule #6, and the same fan-out shape `computeRelatedItems`
|
|
429
|
+
// already uses.
|
|
430
|
+
if (wantUnavailable) {
|
|
431
|
+
const clauses = unavailableClausesFor(types);
|
|
432
|
+
const results = await Promise.all(clauses.map((clause) => {
|
|
433
|
+
// An inequality still has to be ordered by its own field, even here.
|
|
434
|
+
const clauseSorts = clause.op === "lt" && clause.sortField
|
|
435
|
+
? sortBy(clause.sortField, "DESC")
|
|
436
|
+
: sorts;
|
|
437
|
+
return runQuery(executor, {
|
|
438
|
+
filters: sieveAnd(safeFilters, clauseToSieve(clause, now)),
|
|
439
|
+
sorts: clauseSorts,
|
|
440
|
+
page: 1,
|
|
441
|
+
pageSize: PUBLIC_PRODUCT_MAX_PAGE_SIZE,
|
|
442
|
+
cursor: null,
|
|
443
|
+
});
|
|
444
|
+
}));
|
|
445
|
+
// One failed branch must not blank the whole scope — but it is already
|
|
446
|
+
// logged loudly by `runQuery`, and it does mean the merge is incomplete,
|
|
447
|
+
// which `truncated` communicates honestly.
|
|
448
|
+
if (results.every((r) => r === null))
|
|
449
|
+
return null;
|
|
450
|
+
const merged = new Map();
|
|
451
|
+
let truncated = false;
|
|
452
|
+
for (const result of results) {
|
|
453
|
+
if (!result) {
|
|
454
|
+
truncated = true;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const rows = result.items;
|
|
458
|
+
if (rows.length >= PUBLIC_PRODUCT_MAX_PAGE_SIZE)
|
|
459
|
+
truncated = true;
|
|
460
|
+
for (const row of rows) {
|
|
461
|
+
const id = typeof row.id === "string" ? row.id : JSON.stringify(row.id);
|
|
462
|
+
if (!merged.has(id))
|
|
463
|
+
merged.set(id, row);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Drop false positives — a clause selects a SUPERSET. A pre-order can hold
|
|
467
|
+
// `availableQuantity: 0` from an over-signed allocation while still open,
|
|
468
|
+
// and only the per-type predicate knows that.
|
|
469
|
+
const rows = [...merged.values()].filter((row) => !isListingRowAvailable(row, now));
|
|
470
|
+
const ordered = sortRows(rows, requestedSortField, sortDesc);
|
|
471
|
+
const start = (page - 1) * pageSize;
|
|
472
|
+
return finish({
|
|
473
|
+
items: ordered.slice(start, start + pageSize),
|
|
474
|
+
total: ordered.length,
|
|
475
|
+
page,
|
|
476
|
+
pageSize,
|
|
477
|
+
totalPages: truncated
|
|
478
|
+
? page + 1
|
|
479
|
+
: Math.max(1, Math.ceil(ordered.length / pageSize)),
|
|
480
|
+
hasMore: truncated || start + pageSize < ordered.length,
|
|
481
|
+
cursor: null,
|
|
482
|
+
truncated,
|
|
483
|
+
filters: clauses.map((c) => sieveAnd(safeFilters, clauseToSieve(c, now))).join(" OR "),
|
|
484
|
+
sorts,
|
|
485
|
+
enabled: opts?.enabledListingTypes,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
// ── Path A ("all") and the dense "available" case share one query. The only
|
|
489
|
+
// difference is whether a per-row predicate runs afterwards.
|
|
490
|
+
const filters = sieveAnd(safeFilters, ...dateClauses);
|
|
491
|
+
const inMemory = wantAvailable || (hasDateRange && !canPushDate);
|
|
331
492
|
// When a date range stays in memory, fetch in whichever direction front-loads
|
|
332
493
|
// the rows that will PASS it: `dateFrom` (">=", still live) wants the most
|
|
333
494
|
// future dates first; `dateTo` ("<=", ends by) wants the earliest first.
|
|
334
495
|
// Fetching in the client's own order instead is what made live auctions
|
|
335
496
|
// invisible once ~50 had already ended.
|
|
336
|
-
const fetchSorts =
|
|
497
|
+
const fetchSorts = inMemory && hasDateRange && !canPushDate && dateField
|
|
337
498
|
? input.dateFrom
|
|
338
499
|
? sortBy(dateField, "DESC")
|
|
339
500
|
: sortBy(dateField, "ASC")
|
|
340
501
|
: sorts;
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
cursor: hasUnsafeFilter ? null : (input.cursor ?? null),
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
void normalizeError(error);
|
|
354
|
-
// Loud. A swallowed query failure here is indistinguishable from an empty
|
|
355
|
-
// catalogue at every call site above, which is exactly how the /art bug hid.
|
|
356
|
-
serverLogger.error("listPublicProducts query failed", {
|
|
357
|
-
filters,
|
|
358
|
-
sorts: fetchSorts,
|
|
359
|
-
error: error instanceof Error ? error.message : String(error),
|
|
360
|
-
});
|
|
502
|
+
const result = await runQuery(executor, {
|
|
503
|
+
filters,
|
|
504
|
+
sorts: fetchSorts,
|
|
505
|
+
page: inMemory ? 1 : page,
|
|
506
|
+
pageSize: inMemory ? PUBLIC_PRODUCT_MAX_PAGE_SIZE : pageSize,
|
|
507
|
+
cursor: inMemory ? null : (input.cursor ?? null),
|
|
508
|
+
});
|
|
509
|
+
if (!result)
|
|
361
510
|
return null;
|
|
362
|
-
}
|
|
363
511
|
let items = result.items;
|
|
364
512
|
let total = result.total;
|
|
365
513
|
let totalPages = result.totalPages;
|
|
366
514
|
let resultPage = result.page;
|
|
367
515
|
let hasMore = result.hasMore;
|
|
368
516
|
let cursor = result.cursor ?? null;
|
|
369
|
-
|
|
517
|
+
let truncated = false;
|
|
518
|
+
if (inMemory) {
|
|
519
|
+
truncated = items.length >= PUBLIC_PRODUCT_MAX_PAGE_SIZE;
|
|
370
520
|
const filtered = items.filter((item) => {
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
if (hasDateRange && dateField) {
|
|
521
|
+
if (wantAvailable && !isListingRowAvailable(item, now))
|
|
522
|
+
return false;
|
|
523
|
+
if (hasDateRange && !canPushDate && dateField) {
|
|
377
524
|
const raw = item[dateField];
|
|
378
525
|
if (!raw)
|
|
379
526
|
return false;
|
|
@@ -385,28 +532,16 @@ export async function listPublicProducts(input, opts) {
|
|
|
385
532
|
}
|
|
386
533
|
return true;
|
|
387
534
|
});
|
|
388
|
-
const ordered = fetchSorts === sorts ? filtered : sortRows(filtered, requestedSortField,
|
|
535
|
+
const ordered = fetchSorts === sorts ? filtered : sortRows(filtered, requestedSortField, sortDesc);
|
|
389
536
|
total = ordered.length;
|
|
390
|
-
totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
537
|
+
totalPages = truncated ? page + 1 : Math.max(1, Math.ceil(total / pageSize));
|
|
391
538
|
resultPage = page;
|
|
392
539
|
const start = (page - 1) * pageSize;
|
|
393
540
|
items = ordered.slice(start, start + pageSize);
|
|
394
|
-
hasMore = start + pageSize < total;
|
|
541
|
+
hasMore = truncated || start + pageSize < total;
|
|
395
542
|
cursor = null;
|
|
396
543
|
}
|
|
397
|
-
|
|
398
|
-
const enabled = opts?.enabledListingTypes;
|
|
399
|
-
if (enabled && enabled.size > 0) {
|
|
400
|
-
const before = items.length;
|
|
401
|
-
items = items.filter((it) => {
|
|
402
|
-
const lt = typeof it.listingType === "string" ? it.listingType : "standard";
|
|
403
|
-
return enabled.has(lt);
|
|
404
|
-
});
|
|
405
|
-
const removed = before - items.length;
|
|
406
|
-
if (removed > 0)
|
|
407
|
-
total = Math.max(0, total - removed);
|
|
408
|
-
}
|
|
409
|
-
return {
|
|
544
|
+
return finish({
|
|
410
545
|
items,
|
|
411
546
|
total,
|
|
412
547
|
page: resultPage,
|
|
@@ -414,8 +549,49 @@ export async function listPublicProducts(input, opts) {
|
|
|
414
549
|
totalPages,
|
|
415
550
|
hasMore,
|
|
416
551
|
cursor,
|
|
552
|
+
truncated,
|
|
417
553
|
filters,
|
|
418
554
|
sorts,
|
|
555
|
+
enabled: opts?.enabledListingTypes,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Run one executor call, logging loudly and returning null on failure.
|
|
560
|
+
*
|
|
561
|
+
* Loud on purpose: a swallowed query failure is indistinguishable from an
|
|
562
|
+
* empty catalogue at every call site above, which is exactly how the /art bug
|
|
563
|
+
* hid for months (Root Cause #59).
|
|
564
|
+
*/
|
|
565
|
+
async function runQuery(executor, query) {
|
|
566
|
+
try {
|
|
567
|
+
return await executor(query);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
void normalizeError(error);
|
|
571
|
+
serverLogger.error("listPublicProducts query failed", {
|
|
572
|
+
filters: query.filters,
|
|
573
|
+
sorts: query.sorts,
|
|
574
|
+
error: error instanceof Error ? error.message : String(error),
|
|
575
|
+
});
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** Apply the feature-flag post-filter and assemble the result. */
|
|
580
|
+
function finish(draft) {
|
|
581
|
+
const { enabled, ...result } = draft;
|
|
582
|
+
if (!enabled || enabled.size === 0)
|
|
583
|
+
return result;
|
|
584
|
+
// Feature-flagged-off types never reach a public surface.
|
|
585
|
+
const before = result.items.length;
|
|
586
|
+
const items = result.items.filter((it) => {
|
|
587
|
+
const lt = typeof it.listingType === "string" ? it.listingType : "standard";
|
|
588
|
+
return enabled.has(lt);
|
|
589
|
+
});
|
|
590
|
+
const removed = before - items.length;
|
|
591
|
+
return {
|
|
592
|
+
...result,
|
|
593
|
+
items,
|
|
594
|
+
total: removed > 0 ? Math.max(0, result.total - removed) : result.total,
|
|
419
595
|
};
|
|
420
596
|
}
|
|
421
597
|
/**
|
|
@@ -438,7 +614,7 @@ export async function listStoreProducts(storeId, listingTypes, searchParams = {}
|
|
|
438
614
|
listingTypes,
|
|
439
615
|
pageSize: defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
440
616
|
sorts: defaults?.sorts,
|
|
441
|
-
...
|
|
617
|
+
...defaultAvailabilityForListingTypes(listingTypes),
|
|
442
618
|
}),
|
|
443
619
|
// The route owns the store identity — a `?storeId=` in the URL must not be
|
|
444
620
|
// able to make one store's tab render another store's inventory.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ListingTab } from "../../../../features/products/constants/listing-tabs";
|
|
2
|
+
/**
|
|
3
|
+
* Which slice of the catalogue a tab bar is counting. Exactly one of the three
|
|
4
|
+
* scoping fields is set in practice, but the shape is shared so all three
|
|
5
|
+
* surfaces go through one code path.
|
|
6
|
+
*/
|
|
7
|
+
export interface ListingTabScope {
|
|
8
|
+
/**
|
|
9
|
+
* Category ids to match against `categorySlugs`. Products are tagged with
|
|
10
|
+
* their FULL ancestor chain, so this should be the page's own id alone —
|
|
11
|
+
* expanding descendants is both unnecessary and, past 30 entries, illegal
|
|
12
|
+
* (see `ProductRepository.MAX_ARRAY_CONTAINS_ANY`).
|
|
13
|
+
*/
|
|
14
|
+
categoriesIn?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Brand DISPLAY NAME, not slug. `BrandDetailPageView` matches products on the
|
|
17
|
+
* free-text `brand` field, so the count must use the same field or it would
|
|
18
|
+
* disagree with the rows the tab then shows.
|
|
19
|
+
*/
|
|
20
|
+
brandName?: string;
|
|
21
|
+
storeId?: string;
|
|
22
|
+
/** Publication state to count. Defaults to `published`. */
|
|
23
|
+
status?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* tabSlug -> number of rows, or `undefined` when that count could not be
|
|
27
|
+
* resolved.
|
|
28
|
+
*
|
|
29
|
+
* `undefined` is load-bearing: callers hide a tab at zero, so a failed query
|
|
30
|
+
* that reported 0 would hide a tab holding real inventory. Callers must treat
|
|
31
|
+
* `undefined` as "keep the tab visible" (Root Cause #59).
|
|
32
|
+
*/
|
|
33
|
+
export type ListingTabCounts = Record<string, number | undefined>;
|
|
34
|
+
/**
|
|
35
|
+
* Count every tab in `tabs` within `scope`, in one parallel batch.
|
|
36
|
+
*
|
|
37
|
+
* The listing-type buckets come from each tab's own `listingType` field — which
|
|
38
|
+
* already carries `"art|stickers"` as a pipe OR-group — so this function never
|
|
39
|
+
* enumerates the `ListingType` union itself and cannot drift from it.
|
|
40
|
+
*
|
|
41
|
+
* `stores` is not counted here: only the category page has that tab and it
|
|
42
|
+
* already resolves its store list eagerly to render the tab body.
|
|
43
|
+
*/
|
|
44
|
+
export declare function listingTabCounts(tabs: readonly ListingTab[], scope: ListingTabScope): Promise<ListingTabCounts>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Category, brand and store detail pages each show a listing-type tab bar
|
|
3
|
+
* and hide a tab whose scope holds nothing. Before this module they each
|
|
4
|
+
* hand-wrote their own per-type count map, and all three had drifted:
|
|
5
|
+
* category and brand counted 6 of 10 tabs (classifieds / digital-codes /
|
|
6
|
+
* live / art fell through to `undefined` and could therefore NEVER hide),
|
|
7
|
+
* while the store page counted all 9 through eleven copy-pasted queries.
|
|
8
|
+
* That is Recurrent Root Cause #61 — an enumeration of `ListingType` kept
|
|
9
|
+
* by hand in more than one place always drifts.
|
|
10
|
+
* WHAT: One function that derives the buckets it counts from the SAME tab array
|
|
11
|
+
* the tab bar renders, so a tenth listing type is counted automatically.
|
|
12
|
+
*
|
|
13
|
+
* EXPORTS:
|
|
14
|
+
* ListingTabScope — the surface's scope (category / brand / store)
|
|
15
|
+
* ListingTabCounts — tabSlug -> count, `undefined` meaning "query failed"
|
|
16
|
+
* listingTabCounts — resolve every tab's count in one parallel batch
|
|
17
|
+
*
|
|
18
|
+
* @tag domain:products,categories,stores
|
|
19
|
+
* @tag layer:server
|
|
20
|
+
* @tag pattern:derived-from-registry
|
|
21
|
+
* @tag access:server-only
|
|
22
|
+
* @tag consumers:CategoryDetailPageView,BrandDetailPageView,StoreDetailLayoutView
|
|
23
|
+
* @tag sideEffects:none
|
|
24
|
+
*/
|
|
25
|
+
import { categoriesRepository, productRepository } from "../../../../repositories";
|
|
26
|
+
import { serverLogger } from "../../../../monitoring";
|
|
27
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
28
|
+
const DEFAULT_STATUS = "published";
|
|
29
|
+
/** Bundles a category owns, resolved by the same rule the bundles tab renders. */
|
|
30
|
+
async function countBundles(scope) {
|
|
31
|
+
try {
|
|
32
|
+
const rows = await categoriesRepository.listByType("bundle", {
|
|
33
|
+
activeOnly: true,
|
|
34
|
+
limit: 100,
|
|
35
|
+
});
|
|
36
|
+
return rows.filter((row) => bundleMatchesScope(row, scope)).length;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
void normalizeError(error);
|
|
40
|
+
serverLogger.warn("listingTabCounts: bundle count failed; tab stays visible");
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Does a bundle belong to the scope being counted?
|
|
46
|
+
*
|
|
47
|
+
* A bundle row carries `brandSlug` and `createdByStoreId` but has no category
|
|
48
|
+
* field of its own, so category membership is derived from the categories its
|
|
49
|
+
* MEMBER products are filed under — mirrored onto `bundleCategorySlugs` for
|
|
50
|
+
* index-friendly reads. Per Root Cause #42 the mirror is never trusted alone:
|
|
51
|
+
* when it is absent the bundle is treated as unscoped rather than as excluded,
|
|
52
|
+
* because silently dropping a bundle whose mirror one write path forgot is the
|
|
53
|
+
* exact failure that entry documents.
|
|
54
|
+
*/
|
|
55
|
+
function bundleMatchesScope(bundle, scope) {
|
|
56
|
+
if (scope.storeId)
|
|
57
|
+
return bundle.createdByStoreId === scope.storeId;
|
|
58
|
+
if (scope.brandName)
|
|
59
|
+
return true; // brand pages filter by slug at the call site
|
|
60
|
+
if (scope.categoriesIn && scope.categoriesIn.length > 0) {
|
|
61
|
+
const mirror = bundle.bundleCategorySlugs;
|
|
62
|
+
if (!mirror || mirror.length === 0)
|
|
63
|
+
return true;
|
|
64
|
+
return mirror.some((slug) => scope.categoriesIn.includes(slug));
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Count every tab in `tabs` within `scope`, in one parallel batch.
|
|
70
|
+
*
|
|
71
|
+
* The listing-type buckets come from each tab's own `listingType` field — which
|
|
72
|
+
* already carries `"art|stickers"` as a pipe OR-group — so this function never
|
|
73
|
+
* enumerates the `ListingType` union itself and cannot drift from it.
|
|
74
|
+
*
|
|
75
|
+
* `stores` is not counted here: only the category page has that tab and it
|
|
76
|
+
* already resolves its store list eagerly to render the tab body.
|
|
77
|
+
*/
|
|
78
|
+
export async function listingTabCounts(tabs, scope) {
|
|
79
|
+
const listingTabs = tabs.filter((tab) => Boolean(tab.listingType));
|
|
80
|
+
const wantsBundles = tabs.some((tab) => tab.collection === "bundles");
|
|
81
|
+
const [typeCounts, bundleCount] = await Promise.all([
|
|
82
|
+
productRepository.countByListingTypes(listingTabs.map((tab) => String(tab.listingType)), {
|
|
83
|
+
status: scope.status ?? DEFAULT_STATUS,
|
|
84
|
+
storeId: scope.storeId,
|
|
85
|
+
categoriesIn: scope.categoriesIn,
|
|
86
|
+
brandName: scope.brandName,
|
|
87
|
+
}),
|
|
88
|
+
wantsBundles ? countBundles(scope) : Promise.resolve(undefined),
|
|
89
|
+
]);
|
|
90
|
+
const counts = {};
|
|
91
|
+
for (const tab of listingTabs) {
|
|
92
|
+
counts[tab.id] = typeCounts[String(tab.listingType)];
|
|
93
|
+
}
|
|
94
|
+
if (wantsBundles)
|
|
95
|
+
counts.bundles = bundleCount;
|
|
96
|
+
return counts;
|
|
97
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-settings adapter — the allow-list projection for `GET /api/site-settings`.
|
|
3
|
+
*
|
|
4
|
+
* Every field of `SiteSettingsDocument` is triaged into exactly one of
|
|
5
|
+
* `PUBLIC_SITE_SETTINGS_FIELDS` (mapped into the returned literal) or
|
|
6
|
+
* `PRIVATE_SITE_SETTINGS_FIELDS` (with a stated reason). A field added to the
|
|
7
|
+
* schema and to neither list fails
|
|
8
|
+
* `scripts/audit-public-projection-parity.mjs` — new fields are private by
|
|
9
|
+
* default.
|
|
10
|
+
*
|
|
11
|
+
* Why this replaced a deny-list (2026-08-24): the route used to spread the
|
|
12
|
+
* document and delete three keys (`credentials`, `emailSettings`,
|
|
13
|
+
* `legalPages`), which meant everything else was public by default and
|
|
14
|
+
* edge-cached for 300s. That shipped `gst.gstin` + the registered legal name
|
|
15
|
+
* and address, all 25 `commissions.*` fields, `laborRate.hourlyRate`,
|
|
16
|
+
* `emi.surchargeSellerSharePercent`, `featureFlags.adminCheckoutBypass`, and —
|
|
17
|
+
* because they weren't even declared on the interface —
|
|
18
|
+
* `adSettings.providerCredentials` (which the ADMIN ads endpoint bothers to
|
|
19
|
+
* mask) plus every draft/paused/scheduled ad in `adSettings.inventory`.
|
|
20
|
+
*
|
|
21
|
+
* That last part is the argument for an allow-list over any amount of
|
|
22
|
+
* deny-list discipline: a deny-list keyed on a TypeScript interface is blind
|
|
23
|
+
* to fields the interface never declared. This function can only emit what it
|
|
24
|
+
* names.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: fees are deliberately NOT exposed here. Cart and checkout receive them
|
|
27
|
+
* as SSR props via `toBuyerFacingFees`. If a client ever needs them from this
|
|
28
|
+
* endpoint, spread `toBuyerFacingFees(doc.commissions)` into the literal below
|
|
29
|
+
* and move `commissions` across to the public list — do not hand-roll a second
|
|
30
|
+
* subset.
|
|
31
|
+
*/
|
|
32
|
+
import type { SiteSettingsDocument } from "../../../../features/admin/schemas/firestore";
|
|
33
|
+
import type { MediaVideoWatermark } from "../../../../features/media/MediaVideo";
|
|
34
|
+
import type { PublicSiteSettings } from "../../../shared/features/site-settings/types";
|
|
35
|
+
/**
|
|
36
|
+
* Document fields with a proven `useSiteSettings()` reader. Every entry below
|
|
37
|
+
* was traced to a call site; anything without one belongs in the private list,
|
|
38
|
+
* because an unused public field is pure attack surface.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "featureFlags", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark"];
|
|
41
|
+
/** Everything else, with the reason it stays server-side. */
|
|
42
|
+
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "disabledRoutes", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults"];
|
|
43
|
+
export interface ToPublicSiteSettingsOptions {
|
|
44
|
+
/**
|
|
45
|
+
* The resolved marker → wordmark → text watermark chain. Injected because
|
|
46
|
+
* the resolver lives in the consumer app
|
|
47
|
+
* (`src/lib/watermark/resolve-effective-watermark.ts`), not in appkit.
|
|
48
|
+
*/
|
|
49
|
+
effectiveWatermark?: MediaVideoWatermark;
|
|
50
|
+
}
|
|
51
|
+
export declare function toPublicSiteSettings(doc: SiteSettingsDocument, opts?: ToPublicSiteSettingsOptions): PublicSiteSettings;
|