@mohasinac/appkit 4.42.18 → 4.44.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/lottery/LotteryDetailView.js +22 -1
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/components/ProductsIndexPageView.js +1 -1
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/orders-seed-data.js +5 -3
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +11 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-owned content — description templates and product defaults.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this lives on the CATEGORY and not on a listing template
|
|
5
|
+
*
|
|
6
|
+
* `ListingTemplateDocument` is store-scoped. The ask is that the description
|
|
7
|
+
* lives with the *category*, and the crawl proves the model: worldhobbyshop
|
|
8
|
+
* reuses TWO bodies across 1,195 listings, and tcgindia generates five
|
|
9
|
+
* templated FAQs per product. Nobody writes 1,195 descriptions by hand — our
|
|
10
|
+
* sellers do, because no category→description mechanism exists anywhere in
|
|
11
|
+
* this codebase.
|
|
12
|
+
*
|
|
13
|
+
* ## Why it costs ZERO extra reads
|
|
14
|
+
*
|
|
15
|
+
* Both hot paths already hold the category document: `deriveTaxonomy` does
|
|
16
|
+
* `cats.doc(leaf).get()`, and `CategoryDetailPageView` does `getCategoryBySlug`
|
|
17
|
+
* plus seven parallel reads — already at Rule #6's ~3-round-trip ceiling. A
|
|
18
|
+
* sidecar collection would add an eighth read to the second-most-crawled page
|
|
19
|
+
* on the site.
|
|
20
|
+
*
|
|
21
|
+
* ## Why three fields rather than one blob
|
|
22
|
+
*
|
|
23
|
+
* Each has exactly one reader: `contentBody` → the category page;
|
|
24
|
+
* `descriptionTemplates` → the seller form plus the narrow template endpoint;
|
|
25
|
+
* `productDefaults` → `deriveTaxonomy`. A single `content` object would make
|
|
26
|
+
* every one of those read the other two.
|
|
27
|
+
*/
|
|
28
|
+
import type { ProductAuthenticity } from "../../products/schemas/firestore";
|
|
29
|
+
/**
|
|
30
|
+
* Which condition a template is written for.
|
|
31
|
+
*
|
|
32
|
+
* 🛑 A KEYED SET, not one body, because worldhobbyshop demonstrably reuses
|
|
33
|
+
* **two** per category — a pre-owned body and a new-in-box body — and a single
|
|
34
|
+
* string field cannot express that. The variants are the conditions that
|
|
35
|
+
* change what the copy must SAY, not every `CONDITION_OPTIONS` value: a
|
|
36
|
+
* "pre-owned" and a "used" listing want the same disclosure paragraph.
|
|
37
|
+
*/
|
|
38
|
+
export type CategoryTemplateVariant = "default" | "new_in_box" | "new_in_packet" | "pre_owned" | "replica";
|
|
39
|
+
export interface CategoryDescriptionTemplate {
|
|
40
|
+
id: string;
|
|
41
|
+
/** What the picker shows, e.g. "Pre-owned — condition as pictured". */
|
|
42
|
+
label: string;
|
|
43
|
+
variant: CategoryTemplateVariant;
|
|
44
|
+
/** `{{placeholder}}` slots; HTML permitted, rendered via RichTextRenderer. */
|
|
45
|
+
body: string;
|
|
46
|
+
/**
|
|
47
|
+
* Slot names found in `body`, derived at write time as a convenience.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 ABSENT means "re-parse", never "this body has no slots". A reader that
|
|
50
|
+
* treats `undefined` as an empty list will skip interpolation entirely on
|
|
51
|
+
* every template written before this field existed — and the failure is a
|
|
52
|
+
* published description containing a literal `{{series}}`, which is the most
|
|
53
|
+
* visible way this feature can break. Use `extractPlaceholders(body)` when
|
|
54
|
+
* it is absent.
|
|
55
|
+
*/
|
|
56
|
+
placeholders?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* An admin has published a real listing from this template.
|
|
59
|
+
*
|
|
60
|
+
* Exists because of raikages, the crawl's counter-example: 114-char bodies
|
|
61
|
+
* lifted from the Beyblade Wiki, one of which ships with the literal UI
|
|
62
|
+
* string "Add to Wishlist Add to Wishlist" welded into the product body. An
|
|
63
|
+
* unreviewed template is how that happens, so the flag records review rather
|
|
64
|
+
* than assuming it.
|
|
65
|
+
*/
|
|
66
|
+
isTested?: boolean;
|
|
67
|
+
version?: number;
|
|
68
|
+
}
|
|
69
|
+
/** A spec row as the product page renders it. */
|
|
70
|
+
export interface CategorySpecification {
|
|
71
|
+
name: string;
|
|
72
|
+
value: string;
|
|
73
|
+
unit?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What a category hands down to a product filed under it.
|
|
77
|
+
*
|
|
78
|
+
* Three behaviours, and the distinction matters more than the field list:
|
|
79
|
+
*
|
|
80
|
+
* WRITTEN tax, specifications, inTheBox, features — written server-side
|
|
81
|
+
* by `deriveTaxonomy` when the product omits them.
|
|
82
|
+
* FILL-BLANK seo* — fills only an empty field, never overwrites.
|
|
83
|
+
* ADVISORY priceGuidance — NEVER written to `price`. It renders as a hint
|
|
84
|
+
* beside the price input and nothing else.
|
|
85
|
+
*/
|
|
86
|
+
export interface CategoryProductDefaults {
|
|
87
|
+
/**
|
|
88
|
+
* Points at a `taxCodes` row. `deriveTaxonomy` resolves it and writes
|
|
89
|
+
* `gstRate`/`hsnCode` onto the product; the order item then snapshots from
|
|
90
|
+
* the product. That layering is what stops a tax-code edit retroactively
|
|
91
|
+
* re-rating an invoice already issued.
|
|
92
|
+
*/
|
|
93
|
+
taxCodeId?: string;
|
|
94
|
+
specifications?: CategorySpecification[];
|
|
95
|
+
/**
|
|
96
|
+
* Feature ids merged onto the product, never replacing what it already has.
|
|
97
|
+
* (The old `defaultTags` name went with `ProductDocument.tags`.)
|
|
98
|
+
*/
|
|
99
|
+
defaultFeatures?: string[];
|
|
100
|
+
defaultCondition?: string;
|
|
101
|
+
defaultAuthenticity?: ProductAuthenticity;
|
|
102
|
+
/** "What's in the box?" — becomes `ProductDocument.features[]` prose. */
|
|
103
|
+
inTheBox?: string[];
|
|
104
|
+
priceGuidance?: CategoryPriceGuidance;
|
|
105
|
+
seoTitleTemplate?: string;
|
|
106
|
+
seoDescriptionTemplate?: string;
|
|
107
|
+
/** Merged with whatever the product already has. */
|
|
108
|
+
seoKeywords?: string[];
|
|
109
|
+
weightG?: number;
|
|
110
|
+
dimensionsCm?: {
|
|
111
|
+
l: number;
|
|
112
|
+
w: number;
|
|
113
|
+
h: number;
|
|
114
|
+
};
|
|
115
|
+
shippingPaidBy?: string;
|
|
116
|
+
fragile?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Rendered at READ time, so fixing a wrong answer fixes every listing at
|
|
119
|
+
* once. The tcgindia model — five per product, three interpolating the
|
|
120
|
+
* name — and nobody else in the crawl of fifteen does it.
|
|
121
|
+
*/
|
|
122
|
+
faqTemplates?: {
|
|
123
|
+
question: string;
|
|
124
|
+
answer: string;
|
|
125
|
+
}[];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Two price views, never blended.
|
|
129
|
+
*
|
|
130
|
+
* 🛑 `market` and `sold` are rendered SIDE BY SIDE — "Market ₹799–₹1,499 ·
|
|
131
|
+
* Sold here ₹850–₹1,200 (7 sales)" — and the gap between them is the most
|
|
132
|
+
* useful thing we can tell a seller, given 76–93% of competitor listings never
|
|
133
|
+
* sell at their ask. Averaging the two would launder an inference into a
|
|
134
|
+
* measurement.
|
|
135
|
+
*/
|
|
136
|
+
export interface CategoryPriceGuidance {
|
|
137
|
+
currency: "INR";
|
|
138
|
+
/**
|
|
139
|
+
* Day-1 baseline from the competitor crawl: a TYPICAL MARKET LISTING PRICE.
|
|
140
|
+
*
|
|
141
|
+
* 🛑 NOT evidence of a transaction. A competitor's sold-out row proves the
|
|
142
|
+
* item is GONE, not that it sold at that price. Seed-owned — the rollup job
|
|
143
|
+
* must never write here, or a quiet night would erase the baseline.
|
|
144
|
+
*/
|
|
145
|
+
market?: {
|
|
146
|
+
/**
|
|
147
|
+
* The one figure every measured band has. Required for exactly that
|
|
148
|
+
* reason — a band with no central value is not a band.
|
|
149
|
+
*/
|
|
150
|
+
median: number;
|
|
151
|
+
/**
|
|
152
|
+
* 🛑 OPTIONAL, and that is load-bearing. The crawl recorded quartiles for
|
|
153
|
+
* parts, launchers, TCG and figures and only a MEDIAN AND A RANGE for Hot
|
|
154
|
+
* Wheels (n=7,418, ₹99–36,500). Requiring p25/p75 would have forced a
|
|
155
|
+
* range to be retyped as quartiles, which is a different statistic
|
|
156
|
+
* presented as the one the field is named after. Absent means "not
|
|
157
|
+
* measured", and the hint renders as "around ₹850" rather than a span.
|
|
158
|
+
*/
|
|
159
|
+
p25?: number;
|
|
160
|
+
p75?: number;
|
|
161
|
+
/**
|
|
162
|
+
* 🛑 Also optional, for the same reason: the crawl's Beyblade-top table
|
|
163
|
+
* carries no per-leaf sample size at all, and the one place two sources
|
|
164
|
+
* disagreed (HMS: beybladeshopindia n=39 against worldhobbyshop n=2) is
|
|
165
|
+
* recorded precisely because an n=2 median is a coin flip presented as
|
|
166
|
+
* data. Absent must render as absent — never as a confident figure, and
|
|
167
|
+
* never as a number somebody filled in to satisfy the type.
|
|
168
|
+
*/
|
|
169
|
+
sampleSize?: number;
|
|
170
|
+
asOf: Date;
|
|
171
|
+
soldOutShare?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Which crawl this came from, and any caveat about it — "worldhobbyshop",
|
|
174
|
+
* "raikages n=68 (the only component-level pricing anywhere)",
|
|
175
|
+
* "toycollectorsindia 1:64 POOLED across mainline and premium".
|
|
176
|
+
*
|
|
177
|
+
* Carried so the question "where did this number come from" is answerable
|
|
178
|
+
* from the data rather than from a markdown file nobody has open, and so a
|
|
179
|
+
* source-priority decision can be applied without re-deriving the bands.
|
|
180
|
+
*/
|
|
181
|
+
source?: string;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* OUR completed sales only, including won auctions and accepted offers.
|
|
185
|
+
*
|
|
186
|
+
* Absent until there are some — **never zeroed**. A zero would render as a
|
|
187
|
+
* real figure meaning "things sell here for ₹0", which is worse than silence.
|
|
188
|
+
*/
|
|
189
|
+
sold?: {
|
|
190
|
+
min: number;
|
|
191
|
+
median: number;
|
|
192
|
+
max: number;
|
|
193
|
+
sampleSize: number;
|
|
194
|
+
windowDays: number;
|
|
195
|
+
byPath?: {
|
|
196
|
+
standard?: number;
|
|
197
|
+
auction?: number;
|
|
198
|
+
offer?: number;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
lastSold?: {
|
|
202
|
+
amount: number;
|
|
203
|
+
at: Date;
|
|
204
|
+
via?: "standard" | "auction" | "offer";
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-owned content — description templates and product defaults.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this lives on the CATEGORY and not on a listing template
|
|
5
|
+
*
|
|
6
|
+
* `ListingTemplateDocument` is store-scoped. The ask is that the description
|
|
7
|
+
* lives with the *category*, and the crawl proves the model: worldhobbyshop
|
|
8
|
+
* reuses TWO bodies across 1,195 listings, and tcgindia generates five
|
|
9
|
+
* templated FAQs per product. Nobody writes 1,195 descriptions by hand — our
|
|
10
|
+
* sellers do, because no category→description mechanism exists anywhere in
|
|
11
|
+
* this codebase.
|
|
12
|
+
*
|
|
13
|
+
* ## Why it costs ZERO extra reads
|
|
14
|
+
*
|
|
15
|
+
* Both hot paths already hold the category document: `deriveTaxonomy` does
|
|
16
|
+
* `cats.doc(leaf).get()`, and `CategoryDetailPageView` does `getCategoryBySlug`
|
|
17
|
+
* plus seven parallel reads — already at Rule #6's ~3-round-trip ceiling. A
|
|
18
|
+
* sidecar collection would add an eighth read to the second-most-crawled page
|
|
19
|
+
* on the site.
|
|
20
|
+
*
|
|
21
|
+
* ## Why three fields rather than one blob
|
|
22
|
+
*
|
|
23
|
+
* Each has exactly one reader: `contentBody` → the category page;
|
|
24
|
+
* `descriptionTemplates` → the seller form plus the narrow template endpoint;
|
|
25
|
+
* `productDefaults` → `deriveTaxonomy`. A single `content` object would make
|
|
26
|
+
* every one of those read the other two.
|
|
27
|
+
*/
|
|
28
|
+
export {};
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CategoryAncestor, CategoryMetrics, CategoryDisplay } from "../types";
|
|
8
8
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
9
|
+
import type { CategoryDescriptionTemplate, CategoryProductDefaults } from "./category-content";
|
|
10
|
+
export type { CategoryDescriptionTemplate, CategoryProductDefaults, CategoryPriceGuidance, CategorySpecification, CategoryTemplateVariant, } from "./category-content";
|
|
9
11
|
export type { CategoryAncestor, CategoryMetrics };
|
|
10
12
|
/**
|
|
11
13
|
* How a bundle resolves its member products.
|
|
@@ -82,9 +84,9 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
82
84
|
name: string;
|
|
83
85
|
slug: string;
|
|
84
86
|
description?: string;
|
|
85
|
-
/**
|
|
87
|
+
/** Test-data flag — set on shared admin-seeded test categories/brands/bundles; swept by testDataCleanup. */
|
|
86
88
|
isTestData?: boolean;
|
|
87
|
-
/** When isTestData, the cutoff after which
|
|
89
|
+
/** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
|
|
88
90
|
testDataExpiresAt?: Date;
|
|
89
91
|
rootId: string;
|
|
90
92
|
parentIds: string[];
|
|
@@ -135,6 +137,38 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
135
137
|
question: string;
|
|
136
138
|
answer: string;
|
|
137
139
|
}[];
|
|
140
|
+
/**
|
|
141
|
+
* Alternate names this category is searched by — indexed into `searchTxt`
|
|
142
|
+
* and rendered nowhere.
|
|
143
|
+
*
|
|
144
|
+
* B5. A named-model leaf is the case that needs it: *Lost Longinus* is sold
|
|
145
|
+
* in the west as *Lost Luinor L2* and its product code is *B-66*, and a
|
|
146
|
+
* buyer types whichever one they learned first. `name` can only hold one of
|
|
147
|
+
* the three.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 Watch the 600-token ceiling in `buildSearchTxt`, which truncates
|
|
150
|
+
* SILENTLY. A category's own tokens (name, slug, description, path, every
|
|
151
|
+
* ancestor name) come first in the array, so an unbounded alias list would
|
|
152
|
+
* evict the lineage tokens that are the whole reason ancestors are indexed.
|
|
153
|
+
* The generator caps a model leaf at the Hasbro name plus the product code.
|
|
154
|
+
*/
|
|
155
|
+
aliases?: string[];
|
|
156
|
+
/**
|
|
157
|
+
* 400–900 chars of keyword-rich HTML with sibling/parent cross-links,
|
|
158
|
+
* rendered via `<RichTextRenderer>`.
|
|
159
|
+
*
|
|
160
|
+
* 🛑 DISTINCT from `description`, which is a <=230-char blurb that also
|
|
161
|
+
* feeds `seo.description` and every card. Conflating them would put a
|
|
162
|
+
* 900-char body in a card and a 230-char stub on the page.
|
|
163
|
+
*
|
|
164
|
+
* 36 of worldhobbyshop's 46 categories carry one and it is their actual
|
|
165
|
+
* ranking mechanism; no Shopify competitor in the crawl has any.
|
|
166
|
+
*/
|
|
167
|
+
contentBody?: string;
|
|
168
|
+
/** Condition-keyed description bodies. A SET, because WHS reuses two per category. */
|
|
169
|
+
descriptionTemplates?: CategoryDescriptionTemplate[];
|
|
170
|
+
/** What this category hands down to a product filed under it. */
|
|
171
|
+
productDefaults?: CategoryProductDefaults;
|
|
138
172
|
/**
|
|
139
173
|
* "special" — admin-curated bundle; writes reverse `partOfBundleIds`
|
|
140
174
|
* pointers on member products and shows the "Bundled" badge on cards.
|
|
@@ -194,6 +228,19 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
194
228
|
isActive: boolean;
|
|
195
229
|
isSearchable: boolean;
|
|
196
230
|
showOnHomepage?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Prefix-expanded search tokens, derived on every write by
|
|
233
|
+
* `CategoriesRepository.buildSearchTxtFor` → `buildCategorySearchTxt`.
|
|
234
|
+
*
|
|
235
|
+
* 🛑 Distinct from `isSearchable`, which is an admin visibility flag. This is
|
|
236
|
+
* the index that makes a category findable AT ALL: before it existed,
|
|
237
|
+
* `CATEGORY_SEARCH_SCAN_LIMIT` was capped at 100 by `SIEVE_DEFAULTS.maxPageSize`
|
|
238
|
+
* — so raising that number did nothing — and a tier-4 model like
|
|
239
|
+
* "Lost Longinus" was unfindable in every picker.
|
|
240
|
+
*
|
|
241
|
+
* Never fed any PII: no `createdBy` (a raw uid) and no `createdByStoreName`.
|
|
242
|
+
*/
|
|
243
|
+
searchTxt?: string[];
|
|
197
244
|
createdBy: string;
|
|
198
245
|
/** Whether this category was created by admin or a store owner. */
|
|
199
246
|
createdByType?: "admin" | "store";
|
|
@@ -204,12 +251,12 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
204
251
|
ancestors: CategoryAncestor[];
|
|
205
252
|
}
|
|
206
253
|
export declare const CATEGORIES_COLLECTION: "categories";
|
|
207
|
-
export declare const CATEGORIES_INDEXED_FIELDS: readonly ["slug", "rootId", "tier", "parentIds", "isLeaf", "isFeatured", "featuredPriority", "isBrand", "categoryType", "isActive", "isSearchable", "showOnHomepage", "createdBy", "createdByType", "createdByStoreId", "createdAt"];
|
|
254
|
+
export declare const CATEGORIES_INDEXED_FIELDS: readonly ["slug", "rootId", "tier", "parentIds", "isLeaf", "isFeatured", "featuredPriority", "isBrand", "categoryType", "isActive", "isSearchable", "showOnHomepage", "searchTxt", "createdBy", "createdByType", "createdByStoreId", "createdAt"];
|
|
208
255
|
export declare const MIN_ITEMS_FOR_FEATURED: 8;
|
|
209
256
|
export declare const MAX_FEATURED_CATEGORIES: 4;
|
|
210
257
|
export declare const DEFAULT_CATEGORY_DATA: Partial<CategoryDocument>;
|
|
211
|
-
export declare const CATEGORIES_PUBLIC_FIELDS: readonly ["id", "name", "slug", "description", "rootId", "parentIds", "childrenIds", "tier", "path", "order", "position", "subtreeSize", "isLeaf", "metrics.totalProductCount", "metrics.totalAuctionCount", "metrics.totalItemCount", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "ancestors"];
|
|
212
|
-
export declare const CATEGORIES_UPDATABLE_FIELDS: readonly ["name", "slug", "description", "order", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "isSearchable"];
|
|
258
|
+
export declare const CATEGORIES_PUBLIC_FIELDS: readonly ["id", "name", "slug", "description", "rootId", "parentIds", "childrenIds", "tier", "path", "order", "position", "subtreeSize", "isLeaf", "metrics.totalProductCount", "metrics.totalAuctionCount", "metrics.totalItemCount", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "ancestors", "highlights", "faqs", "contentBody", "descriptionTemplates", "productDefaults", "aliases"];
|
|
259
|
+
export declare const CATEGORIES_UPDATABLE_FIELDS: readonly ["name", "slug", "description", "order", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "isSearchable", "highlights", "faqs", "contentBody", "descriptionTemplates", "productDefaults", "aliases"];
|
|
213
260
|
export type CategoryCreateInput = Omit<CategoryDocument, "id" | "createdAt" | "updatedAt" | "metrics" | "isLeaf" | "ancestors"> & {
|
|
214
261
|
parentIds?: string[];
|
|
215
262
|
parentId?: string | null;
|
|
@@ -285,6 +332,11 @@ export declare const CATEGORY_FIELDS: {
|
|
|
285
332
|
readonly VIEW_COUNT: "viewCount";
|
|
286
333
|
readonly IS_ACTIVE: "isActive";
|
|
287
334
|
readonly IS_SEARCHABLE: "isSearchable";
|
|
335
|
+
readonly SEARCH_TXT: "searchTxt";
|
|
336
|
+
readonly CONTENT_BODY: "contentBody";
|
|
337
|
+
readonly DESCRIPTION_TEMPLATES: "descriptionTemplates";
|
|
338
|
+
readonly PRODUCT_DEFAULTS: "productDefaults";
|
|
339
|
+
readonly PRODUCT_DEFAULTS_TAX_CODE_ID: "productDefaults.taxCodeId";
|
|
288
340
|
readonly CATEGORY_TYPE: "categoryType";
|
|
289
341
|
readonly CREATED_BY: "createdBy";
|
|
290
342
|
readonly CREATED_AT: "createdAt";
|
|
@@ -293,5 +345,22 @@ export declare const CATEGORY_FIELDS: {
|
|
|
293
345
|
};
|
|
294
346
|
/**
|
|
295
347
|
* Build a category tree from a flat list of CategoryDocument objects.
|
|
348
|
+
*
|
|
349
|
+
* 🛑 ONE INDEXING PASS, then recursion — not a `.filter()` per node.
|
|
350
|
+
* This used to scan the whole array once per category to find its children,
|
|
351
|
+
* which is O(n²): 2,209 comparisons at today's 47 nodes, ~109,000 at the ~330
|
|
352
|
+
* this plan seeds, and **2.25 million** at 1,500. It runs inside
|
|
353
|
+
* `categoriesRepository.buildTree()`, i.e. on a request path. Grouping by
|
|
354
|
+
* parent id first makes it O(n log n) (the sorts) and the output is
|
|
355
|
+
* byte-identical: same roots, same child order, same depths.
|
|
356
|
+
*
|
|
357
|
+
* 🛑 The `seen` set is not defensive padding. A root is picked by
|
|
358
|
+
* `tier === 0`, and nothing stops a corrupt row from being tier 0 *and* a
|
|
359
|
+
* descendant of its own child (`parentIds` is a denormalised chain that four
|
|
360
|
+
* different write paths maintain). The old code would recurse forever on that,
|
|
361
|
+
* and a hung SSR render is worse than a failed one — it holds a function open
|
|
362
|
+
* for its whole timeout and reports nothing. A revisited node is dropped from
|
|
363
|
+
* the second position it appears in rather than throwing: a malformed edge
|
|
364
|
+
* should cost one tile, not the category page.
|
|
296
365
|
*/
|
|
297
366
|
export declare function buildCategoryTree(categories: CategoryDocument[], rootId?: string): CategoryTreeNode[];
|
|
@@ -20,6 +20,7 @@ export const CATEGORIES_INDEXED_FIELDS = [
|
|
|
20
20
|
"isActive",
|
|
21
21
|
"isSearchable",
|
|
22
22
|
"showOnHomepage",
|
|
23
|
+
"searchTxt",
|
|
23
24
|
"createdBy",
|
|
24
25
|
"createdByType",
|
|
25
26
|
"createdByStoreId",
|
|
@@ -76,6 +77,31 @@ export const CATEGORIES_PUBLIC_FIELDS = [
|
|
|
76
77
|
"display",
|
|
77
78
|
"isActive",
|
|
78
79
|
"ancestors",
|
|
80
|
+
/*
|
|
81
|
+
* B3. `highlights` and `faqs` were MISSING from both this list and the
|
|
82
|
+
* updatable one, which is the more interesting half: `CategoryUpdateInput`
|
|
83
|
+
* derives from UPDATABLE_FIELDS, so a field absent from it is not even
|
|
84
|
+
* TYPE-LEGAL to update — and both had shipped with no editor at all.
|
|
85
|
+
* Precedent-by-omission gets closed, not copied.
|
|
86
|
+
*
|
|
87
|
+
* `productDefaults` is public because its advisory half (priceGuidance,
|
|
88
|
+
* specifications, inTheBox) is rendered to buyers and sellers. Its
|
|
89
|
+
* `taxCodeId` is a reference to an admin-only row, not a secret.
|
|
90
|
+
*/
|
|
91
|
+
"highlights",
|
|
92
|
+
"faqs",
|
|
93
|
+
"contentBody",
|
|
94
|
+
"descriptionTemplates",
|
|
95
|
+
"productDefaults",
|
|
96
|
+
/*
|
|
97
|
+
* B5. `aliases` is public because it is already published in a form nobody
|
|
98
|
+
* can withhold: every alias is tokenised into `searchTxt`, which is what the
|
|
99
|
+
* search endpoint matches on, so the facts are inferable from search results
|
|
100
|
+
* whether or not the array is in the payload. Withholding it would hide a
|
|
101
|
+
* useful "also known as" line from the page while changing nothing about
|
|
102
|
+
* what is reachable.
|
|
103
|
+
*/
|
|
104
|
+
"aliases",
|
|
79
105
|
];
|
|
80
106
|
export const CATEGORIES_UPDATABLE_FIELDS = [
|
|
81
107
|
"name",
|
|
@@ -89,6 +115,14 @@ export const CATEGORIES_UPDATABLE_FIELDS = [
|
|
|
89
115
|
"display",
|
|
90
116
|
"isActive",
|
|
91
117
|
"isSearchable",
|
|
118
|
+
// B3 — see the note in CATEGORIES_PUBLIC_FIELDS. Without these five,
|
|
119
|
+
// CategoryUpdateInput cannot express them and no editor can save them.
|
|
120
|
+
"highlights",
|
|
121
|
+
"faqs",
|
|
122
|
+
"contentBody",
|
|
123
|
+
"descriptionTemplates",
|
|
124
|
+
"productDefaults",
|
|
125
|
+
"aliases",
|
|
92
126
|
];
|
|
93
127
|
export const categoryQueryHelpers = {
|
|
94
128
|
bySlug: (slug) => ["slug", "==", slug],
|
|
@@ -179,6 +213,12 @@ export const CATEGORY_FIELDS = {
|
|
|
179
213
|
VIEW_COUNT: "viewCount",
|
|
180
214
|
IS_ACTIVE: "isActive",
|
|
181
215
|
IS_SEARCHABLE: "isSearchable",
|
|
216
|
+
SEARCH_TXT: "searchTxt",
|
|
217
|
+
CONTENT_BODY: "contentBody",
|
|
218
|
+
DESCRIPTION_TEMPLATES: "descriptionTemplates",
|
|
219
|
+
PRODUCT_DEFAULTS: "productDefaults",
|
|
220
|
+
/* Dot-path, so a referrer query is one equality and needs no index. */
|
|
221
|
+
PRODUCT_DEFAULTS_TAX_CODE_ID: "productDefaults.taxCodeId",
|
|
182
222
|
CATEGORY_TYPE: "categoryType",
|
|
183
223
|
CREATED_BY: "createdBy",
|
|
184
224
|
CREATED_AT: "createdAt",
|
|
@@ -187,12 +227,43 @@ export const CATEGORY_FIELDS = {
|
|
|
187
227
|
};
|
|
188
228
|
/**
|
|
189
229
|
* Build a category tree from a flat list of CategoryDocument objects.
|
|
230
|
+
*
|
|
231
|
+
* 🛑 ONE INDEXING PASS, then recursion — not a `.filter()` per node.
|
|
232
|
+
* This used to scan the whole array once per category to find its children,
|
|
233
|
+
* which is O(n²): 2,209 comparisons at today's 47 nodes, ~109,000 at the ~330
|
|
234
|
+
* this plan seeds, and **2.25 million** at 1,500. It runs inside
|
|
235
|
+
* `categoriesRepository.buildTree()`, i.e. on a request path. Grouping by
|
|
236
|
+
* parent id first makes it O(n log n) (the sorts) and the output is
|
|
237
|
+
* byte-identical: same roots, same child order, same depths.
|
|
238
|
+
*
|
|
239
|
+
* 🛑 The `seen` set is not defensive padding. A root is picked by
|
|
240
|
+
* `tier === 0`, and nothing stops a corrupt row from being tier 0 *and* a
|
|
241
|
+
* descendant of its own child (`parentIds` is a denormalised chain that four
|
|
242
|
+
* different write paths maintain). The old code would recurse forever on that,
|
|
243
|
+
* and a hung SSR render is worse than a failed one — it holds a function open
|
|
244
|
+
* for its whole timeout and reports nothing. A revisited node is dropped from
|
|
245
|
+
* the second position it appears in rather than throwing: a malformed edge
|
|
246
|
+
* should cost one tile, not the category page.
|
|
190
247
|
*/
|
|
191
248
|
export function buildCategoryTree(categories, rootId) {
|
|
249
|
+
const byParent = new Map();
|
|
250
|
+
for (const cat of categories) {
|
|
251
|
+
const parentId = cat.parentIds[cat.parentIds.length - 1];
|
|
252
|
+
if (!parentId)
|
|
253
|
+
continue;
|
|
254
|
+
const bucket = byParent.get(parentId);
|
|
255
|
+
if (bucket)
|
|
256
|
+
bucket.push(cat);
|
|
257
|
+
else
|
|
258
|
+
byParent.set(parentId, [cat]);
|
|
259
|
+
}
|
|
260
|
+
for (const bucket of byParent.values())
|
|
261
|
+
bucket.sort((a, b) => a.order - b.order);
|
|
262
|
+
const seen = new Set();
|
|
192
263
|
function buildTree(category, depth) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.
|
|
264
|
+
seen.add(category.id);
|
|
265
|
+
const children = (byParent.get(category.id) ?? [])
|
|
266
|
+
.filter((child) => !seen.has(child.id))
|
|
196
267
|
.map((child) => buildTree(child, depth + 1));
|
|
197
268
|
return { category, children, depth };
|
|
198
269
|
}
|