@mohasinac/appkit 4.37.1 → 4.38.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.
Files changed (126) hide show
  1. package/dist/_internal/server/features/bundles/og.d.ts +0 -1
  2. package/dist/_internal/server/features/bundles/og.js +2 -18
  3. package/dist/_internal/server/features/catalogue/og.d.ts +0 -1
  4. package/dist/_internal/server/features/catalogue/og.js +1 -9
  5. package/dist/_internal/server/features/classified/og.d.ts +0 -1
  6. package/dist/_internal/server/features/classified/og.js +1 -9
  7. package/dist/_internal/server/features/digital-code/og.d.ts +0 -1
  8. package/dist/_internal/server/features/digital-code/og.js +1 -9
  9. package/dist/_internal/server/features/grouped/og.d.ts +7 -4
  10. package/dist/_internal/server/features/grouped/og.js +7 -6
  11. package/dist/_internal/server/features/item-requests/og.d.ts +0 -1
  12. package/dist/_internal/server/features/item-requests/og.js +2 -18
  13. package/dist/_internal/server/features/live/og.d.ts +0 -1
  14. package/dist/_internal/server/features/live/og.js +1 -9
  15. package/dist/_internal/server/features/prize-draws/og.js +5 -10
  16. package/dist/_internal/server/features/products/og.js +5 -2
  17. package/dist/_internal/server/features/seo/sitemap.js +10 -2
  18. package/dist/_internal/server/features/shared/listing-adapters.d.ts +1 -2
  19. package/dist/_internal/server/features/shared/listing-adapters.js +5 -3
  20. package/dist/_internal/shared/listing-types/_registry.d.ts +31 -4
  21. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
  22. package/dist/_internal/shared/listing-types/art/config.js +2 -0
  23. package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
  24. package/dist/_internal/shared/listing-types/auction/config.js +3 -0
  25. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
  26. package/dist/_internal/shared/listing-types/classified/config.js +3 -0
  27. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
  28. package/dist/_internal/shared/listing-types/digital-code/config.js +3 -0
  29. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
  30. package/dist/_internal/shared/listing-types/live/config.js +3 -0
  31. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
  32. package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
  33. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
  34. package/dist/_internal/shared/listing-types/prize-draw/config.js +3 -0
  35. package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
  36. package/dist/_internal/shared/listing-types/standard/config.js +2 -0
  37. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
  38. package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
  39. package/dist/client/observability/reportClientError.js +11 -1
  40. package/dist/client.d.ts +4 -0
  41. package/dist/client.js +4 -0
  42. package/dist/core/hooks/useSyncManager.js +11 -1
  43. package/dist/features/account/hooks/useNotifications.js +25 -5
  44. package/dist/features/auctions/components/AuctionBottomActions.js +10 -2
  45. package/dist/features/auctions/components/AuctionDetailPageView.js +12 -3
  46. package/dist/features/auctions/components/LiveBidPrice.js +2 -2
  47. package/dist/features/auctions/components/LiveMinIncrement.d.ts +1 -1
  48. package/dist/features/auctions/components/LiveMinIncrement.js +8 -0
  49. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
  50. package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
  51. package/dist/features/auctions/hooks/useAuctions.js +12 -1
  52. package/dist/features/auctions/hooks/useRealtimeBids.d.ts +3 -0
  53. package/dist/features/auctions/hooks/useRealtimeBids.js +138 -36
  54. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +2 -2
  55. package/dist/features/catalogue/components/PublicCatalogueView.js +2 -2
  56. package/dist/features/categories/components/BundleCollage.js +10 -5
  57. package/dist/features/categories/components/BundleDetailView.js +7 -3
  58. package/dist/features/categories/components/CategoryProductsListing.js +4 -1
  59. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
  60. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
  61. package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -4
  62. package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
  63. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  64. package/dist/features/pre-orders/components/PreOrderActionsClient.js +2 -2
  65. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
  66. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +4 -1
  67. package/dist/features/products/actions/product-actions.d.ts +9 -0
  68. package/dist/features/products/actions/product-actions.js +11 -0
  69. package/dist/features/products/columns/productTableColumns.js +2 -2
  70. package/dist/features/products/components/AuctionsIndexListing.js +4 -1
  71. package/dist/features/products/components/BidHistory.js +2 -2
  72. package/dist/features/products/components/CompareOverlay.js +2 -2
  73. package/dist/features/products/components/GroupMemberPicker.js +5 -5
  74. package/dist/features/products/components/ListingBottomActions.js +12 -3
  75. package/dist/features/products/components/MakeOfferButton.js +7 -1
  76. package/dist/features/products/components/MarketplaceBundleCard.js +4 -4
  77. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
  78. package/dist/features/products/components/PrizeDrawCollage.js +16 -5
  79. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  80. package/dist/features/products/components/PrizeDrawsIndexListing.js +4 -1
  81. package/dist/features/products/components/PrizeRevealModal.js +2 -2
  82. package/dist/features/products/components/ProductDetailActions.js +10 -3
  83. package/dist/features/products/components/ProductFilters.js +2 -2
  84. package/dist/features/products/components/ProductGrid.js +5 -2
  85. package/dist/features/products/components/ProductsIndexListing.js +12 -1
  86. package/dist/features/products/components/ProductsIndexPageView.js +26 -0
  87. package/dist/features/products/components/ShowGroupSection.js +2 -2
  88. package/dist/features/products/components/SublistingCarouselSection.js +2 -2
  89. package/dist/features/products/constants/listing-tabs.d.ts +36 -12
  90. package/dist/features/products/constants/listing-tabs.js +38 -14
  91. package/dist/features/products/constants/sieve.d.ts +14 -0
  92. package/dist/features/products/constants/sieve.js +39 -0
  93. package/dist/features/tester/seed-data/authored/buying__bidding.js +5 -4
  94. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +85 -85
  95. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +2 -2
  96. package/dist/features/tester/seed-data/authored/buying__product-detail.js +53 -15
  97. package/dist/features/tester/seed-data/authored/content-discovery__search.js +6 -4
  98. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +4 -4
  99. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +2 -2
  100. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +4 -4
  101. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +8 -8
  102. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +8 -8
  103. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +16 -0
  104. package/dist/features/tester/seed-data/authored/seo__og-images.js +17 -0
  105. package/dist/features/tester/seed-data/stores-tester-seed-data.js +41 -2
  106. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +36 -6
  107. package/dist/features/wishlist/components/WishlistPage.js +2 -2
  108. package/dist/index.d.ts +2 -1
  109. package/dist/index.js +4 -1
  110. package/dist/react/contexts/SessionContext.js +40 -1
  111. package/dist/react/hooks/useCanSeePrices.d.ts +37 -0
  112. package/dist/react/hooks/useCanSeePrices.js +40 -0
  113. package/dist/seed/addresses-seed-data.js +36 -2
  114. package/dist/seed/users-seed-data.js +10 -2
  115. package/dist/seo/json-ld.d.ts +21 -0
  116. package/dist/seo/json-ld.js +45 -0
  117. package/dist/styles.css +41 -0
  118. package/dist/ui/components/GatedPrice.d.ts +66 -0
  119. package/dist/ui/components/GatedPrice.js +80 -0
  120. package/dist/ui/components/GatedPrice.style.css +40 -0
  121. package/dist/ui/components/PriceDisplay.d.ts +12 -0
  122. package/dist/ui/components/PriceDisplay.js +17 -17
  123. package/dist/ui/components/index.style.css +1 -0
  124. package/dist/ui/index.d.ts +2 -0
  125. package/dist/ui/index.js +1 -0
  126. package/package.json +1 -1
@@ -17,7 +17,6 @@ export interface BundleOgData {
17
17
  name: string;
18
18
  description?: string | null;
19
19
  coverImageUrl?: string | null;
20
- priceLabel?: string | null;
21
20
  itemCount?: number | null;
22
21
  stockStatus?: "in_stock" | "partial" | "out_of_stock" | null;
23
22
  }
@@ -1,19 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { resolveOgImageUrl } from "../seo/og";
3
- function formatPriceInr(amount) {
4
- return `₹${amount.toLocaleString("en-IN")}`;
5
- }
6
3
  export function renderBundleOg(doc, opts) {
7
4
  const name = doc?.name ?? "Bundle";
8
- const priceLabel = typeof doc?.bundlePrice === "number" && doc.bundlePrice > 0
9
- ? formatPriceInr(doc.bundlePrice)
10
- : null;
11
5
  return renderBundleOgImage({
12
6
  name,
13
7
  description: doc?.description?.slice(0, 140) ??
14
8
  `Curated multi-product bundle on ${opts.siteName}.`,
15
9
  coverImageUrl: resolveOgImageUrl(doc?.display?.coverImage ?? null, opts.baseUrl),
16
- priceLabel,
17
10
  itemCount: doc?.bundleProductIds?.length ?? null,
18
11
  stockStatus: doc?.bundleStockStatus ?? null,
19
12
  }, opts.siteName);
@@ -34,7 +27,7 @@ const STOCK_BADGE = {
34
27
  },
35
28
  };
36
29
  export function renderBundleOgImage(data, siteName) {
37
- const { name, description, coverImageUrl, priceLabel, itemCount, stockStatus } = data;
30
+ const { name, description, coverImageUrl, itemCount, stockStatus } = data;
38
31
  const itemLabel = typeof itemCount === "number" && itemCount > 0
39
32
  ? `${itemCount} item${itemCount === 1 ? "" : "s"}`
40
33
  : null;
@@ -91,16 +84,7 @@ export function renderBundleOgImage(data, siteName) {
91
84
  gap: 12,
92
85
  marginTop: 8,
93
86
  flexWrap: "wrap",
94
- }, children: [priceLabel && (_jsx("div", { style: {
95
- display: "inline-flex",
96
- padding: "10px 22px",
97
- borderRadius: 999,
98
- background: "rgba(56, 189, 248, 0.18)",
99
- color: "#e0f2fe",
100
- fontSize: 26,
101
- fontWeight: 700,
102
- border: "1px solid rgba(56, 189, 248, 0.4)",
103
- }, children: priceLabel })), itemLabel && (_jsx("div", { style: {
87
+ }, children: [itemLabel && (_jsx("div", { style: {
104
88
  display: "inline-flex",
105
89
  padding: "10px 22px",
106
90
  borderRadius: 999,
@@ -2,7 +2,6 @@ import type { ReactElement } from "react";
2
2
  export interface CatalogueItemOgData {
3
3
  title: string;
4
4
  ownerName?: string | null;
5
- priceLabel?: string | null;
6
5
  imageUrl?: string | null;
7
6
  }
8
7
  interface CatalogueItemDocLike {
@@ -1,17 +1,9 @@
1
1
  import { renderOgLayout } from "../seo/og-layout";
2
2
  import { resolveOgImageUrl } from "../seo/og";
3
3
  export function renderCatalogueItemOg(doc, opts) {
4
- const priceLabel = doc?.price != null && doc.price > 0
5
- ? new Intl.NumberFormat("en-IN", {
6
- style: "currency",
7
- currency: "INR",
8
- maximumFractionDigits: 0,
9
- }).format(doc.price)
10
- : null;
11
4
  return renderCatalogueItemOgImage({
12
5
  title: doc?.title ?? "Catalogue Item",
13
6
  ownerName: opts.ownerName,
14
- priceLabel,
15
7
  imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
16
8
  }, opts.siteName);
17
9
  }
@@ -21,7 +13,7 @@ export function renderCatalogueItemOgImage(data, siteName) {
21
13
  subtitle: data.ownerName ? `Owned by ${data.ownerName}` : undefined,
22
14
  imageUrl: data.imageUrl,
23
15
  siteName: `${siteName} · Catalogue`,
24
- accentSlot: data.priceLabel,
16
+ accentSlot: null,
25
17
  theme: { accentColor: "#a78bfa" },
26
18
  });
27
19
  }
@@ -2,7 +2,6 @@ import type { ReactElement } from "react";
2
2
  import type { ProductDocument } from "../../../../features/products/schemas/firestore";
3
3
  export interface ClassifiedOgData {
4
4
  title: string;
5
- priceLabel?: string | null;
6
5
  location?: string | null;
7
6
  imageUrl?: string | null;
8
7
  }
@@ -5,16 +5,8 @@ export function renderClassifiedOg(doc, opts) {
5
5
  const location = meta?.meetupArea
6
6
  ? [meta.meetupArea.locality, meta.meetupArea.city].filter(Boolean).join(", ")
7
7
  : null;
8
- const priceLabel = doc?.price != null
9
- ? new Intl.NumberFormat("en-IN", {
10
- style: "currency",
11
- currency: doc.currency ?? "INR",
12
- maximumFractionDigits: 0,
13
- }).format(doc.price)
14
- : null;
15
8
  return renderClassifiedOgImage({
16
9
  title: doc?.title ?? "Classified Listing",
17
- priceLabel,
18
10
  location,
19
11
  imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
20
12
  }, opts.siteName);
@@ -25,7 +17,7 @@ export function renderClassifiedOgImage(data, siteName) {
25
17
  subtitle: data.location ?? undefined,
26
18
  imageUrl: data.imageUrl,
27
19
  siteName: `${siteName} · Classified`,
28
- accentSlot: data.priceLabel,
20
+ accentSlot: null,
29
21
  theme: { accentColor: "#38bdf8" },
30
22
  });
31
23
  }
@@ -2,7 +2,6 @@ import type { ReactElement } from "react";
2
2
  import type { ProductDocument } from "../../../../features/products/schemas/firestore";
3
3
  export interface DigitalCodeOgData {
4
4
  title: string;
5
- priceLabel?: string | null;
6
5
  deliveryMethod?: string | null;
7
6
  imageUrl?: string | null;
8
7
  }
@@ -1,13 +1,6 @@
1
1
  import { renderOgLayout } from "../seo/og-layout";
2
2
  import { resolveOgImageUrl } from "../seo/og";
3
3
  export function renderDigitalCodeOg(doc, opts) {
4
- const priceLabel = doc?.price != null
5
- ? new Intl.NumberFormat("en-IN", {
6
- style: "currency",
7
- currency: doc.currency ?? "INR",
8
- maximumFractionDigits: 0,
9
- }).format(doc.price)
10
- : null;
11
4
  const deliveryMethod = doc?.digitalCode?.codeDeliveryMethod === "auto-claim"
12
5
  ? "Instant delivery"
13
6
  : doc?.digitalCode?.codeDeliveryMethod === "manual-email"
@@ -15,7 +8,6 @@ export function renderDigitalCodeOg(doc, opts) {
15
8
  : null;
16
9
  return renderDigitalCodeOgImage({
17
10
  title: doc?.title ?? "Digital Code",
18
- priceLabel,
19
11
  deliveryMethod,
20
12
  imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
21
13
  }, opts.siteName);
@@ -26,7 +18,7 @@ export function renderDigitalCodeOgImage(data, siteName) {
26
18
  subtitle: data.deliveryMethod ?? undefined,
27
19
  imageUrl: data.imageUrl,
28
20
  siteName: `${siteName} · Digital Code`,
29
- accentSlot: data.priceLabel,
21
+ accentSlot: null,
30
22
  theme: { background: "#0c0a1e", accentColor: "#a78bfa" },
31
23
  });
32
24
  }
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Deliberately delegates to `renderBundleOgImage` rather than re-implementing
5
5
  * the layout: the card is the same shape (title, description, cover, item
6
- * count) minus the price, and `BundleOgData.priceLabel` / `.stockStatus` are
7
- * already nullable for exactly that case. Forking it would give two ~150-line
8
- * OG layouts to keep visually in step — the Duplication Framework's
9
- * "same prop surface, same output" consolidate rule.
6
+ * count), and `BundleOgData.stockStatus` is already nullable for the fields a
7
+ * group has no value for. Forking it would give two ~150-line OG layouts to
8
+ * keep visually in step — the Duplication Framework's "same prop surface, same
9
+ * output" consolidate rule.
10
+ *
11
+ * Neither card carries a price any more: an OG image is a public URL with the
12
+ * amount burned into the pixels, which no sign-in gate reaches.
10
13
  */
11
14
  import type { ReactElement } from "react";
12
15
  import type { GroupedListingDocument } from "../../../../features/grouped/schemas/firestore";
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Deliberately delegates to `renderBundleOgImage` rather than re-implementing
5
5
  * the layout: the card is the same shape (title, description, cover, item
6
- * count) minus the price, and `BundleOgData.priceLabel` / `.stockStatus` are
7
- * already nullable for exactly that case. Forking it would give two ~150-line
8
- * OG layouts to keep visually in step — the Duplication Framework's
9
- * "same prop surface, same output" consolidate rule.
6
+ * count), and `BundleOgData.stockStatus` is already nullable for the fields a
7
+ * group has no value for. Forking it would give two ~150-line OG layouts to
8
+ * keep visually in step — the Duplication Framework's "same prop surface, same
9
+ * output" consolidate rule.
10
+ *
11
+ * Neither card carries a price any more: an OG image is a public URL with the
12
+ * amount burned into the pixels, which no sign-in gate reaches.
10
13
  */
11
14
  import { renderBundleOgImage } from "../bundles/og";
12
15
  import { resolveOgImageUrl } from "../seo/og";
@@ -16,8 +19,6 @@ export function renderGroupedListingOg(doc, opts) {
16
19
  description: doc?.description?.slice(0, 140) ??
17
20
  `A curated group of related listings on ${opts.siteName}.`,
18
21
  coverImageUrl: resolveOgImageUrl(doc?.coverImage ?? null, opts.baseUrl),
19
- // A group has no price of its own — the buyer's selection is the price.
20
- priceLabel: null,
21
22
  itemCount: doc?.productIds?.length ?? null,
22
23
  stockStatus: null,
23
24
  }, opts.siteName);
@@ -17,7 +17,6 @@ import type { ReactElement } from "react";
17
17
  export interface ItemRequestOgData {
18
18
  title: string;
19
19
  opDisplayName?: string | null;
20
- budgetLabel?: string | null;
21
20
  imageUrl?: string | null;
22
21
  status?: "open" | "closed" | "removed" | "approved" | null;
23
22
  replyCount?: number | null;
@@ -1,21 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { resolveOgImageUrl } from "../seo/og";
3
- function formatPriceInr(amount) {
4
- return `≤ ₹${amount.toLocaleString("en-IN")}`;
5
- }
6
3
  function normalizeStatus(s) {
7
4
  if (s === "open" || s === "closed" || s === "removed" || s === "approved")
8
5
  return s;
9
6
  return null;
10
7
  }
11
8
  export function renderItemRequestOg(doc, opts) {
12
- const budgetLabel = typeof doc?.maxBudget === "number" && doc.maxBudget > 0
13
- ? formatPriceInr(doc.maxBudget)
14
- : null;
15
9
  return renderItemRequestOgImage({
16
10
  title: doc?.title ?? "Item Request",
17
11
  opDisplayName: doc?.opDisplayName ?? null,
18
- budgetLabel,
19
12
  imageUrl: resolveOgImageUrl(doc?.imageUrls?.[0] ?? null, opts.baseUrl),
20
13
  status: normalizeStatus(doc?.status),
21
14
  replyCount: doc?.replyCount ?? null,
@@ -49,7 +42,7 @@ const STATUS_BADGE = {
49
42
  },
50
43
  };
51
44
  export function renderItemRequestOgImage(data, siteName) {
52
- const { title, opDisplayName, budgetLabel, imageUrl, status, replyCount, category } = data;
45
+ const { title, opDisplayName, imageUrl, status, replyCount, category } = data;
53
46
  const statusBadge = status ? STATUS_BADGE[status] : null;
54
47
  const replyLabel = typeof replyCount === "number" && replyCount >= 0
55
48
  ? `${replyCount} repl${replyCount === 1 ? "y" : "ies"}`
@@ -113,16 +106,7 @@ export function renderItemRequestOgImage(data, siteName) {
113
106
  fontSize: 22,
114
107
  fontWeight: 700,
115
108
  border: `1px solid ${statusBadge.border}`,
116
- }, children: statusBadge.text })), budgetLabel && (_jsx("div", { style: {
117
- display: "inline-flex",
118
- padding: "10px 22px",
119
- borderRadius: 999,
120
- background: "rgba(34, 211, 238, 0.18)",
121
- color: "#cffafe",
122
- fontSize: 22,
123
- fontWeight: 700,
124
- border: "1px solid rgba(34, 211, 238, 0.4)",
125
- }, children: budgetLabel })), replyLabel && (_jsx("div", { style: {
109
+ }, children: statusBadge.text })), replyLabel && (_jsx("div", { style: {
126
110
  display: "inline-flex",
127
111
  padding: "10px 22px",
128
112
  borderRadius: 999,
@@ -3,7 +3,6 @@ import type { ProductDocument } from "../../../../features/products/schemas/fire
3
3
  export interface LiveItemOgData {
4
4
  title: string;
5
5
  species?: string | null;
6
- priceLabel?: string | null;
7
6
  imageUrl?: string | null;
8
7
  }
9
8
  interface LiveItemDocLike {
@@ -1,17 +1,9 @@
1
1
  import { renderOgLayout } from "../seo/og-layout";
2
2
  import { resolveOgImageUrl } from "../seo/og";
3
3
  export function renderLiveItemOg(doc, opts) {
4
- const priceLabel = doc?.price != null
5
- ? new Intl.NumberFormat("en-IN", {
6
- style: "currency",
7
- currency: doc.currency ?? "INR",
8
- maximumFractionDigits: 0,
9
- }).format(doc.price)
10
- : null;
11
4
  return renderLiveItemOgImage({
12
5
  title: doc?.title ?? "Live Listing",
13
6
  species: doc?.liveItem?.species ?? null,
14
- priceLabel,
15
7
  imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
16
8
  }, opts.siteName);
17
9
  }
@@ -21,7 +13,7 @@ export function renderLiveItemOgImage(data, siteName) {
21
13
  subtitle: data.species ?? undefined,
22
14
  imageUrl: data.imageUrl,
23
15
  siteName: `${siteName} · Live Listing`,
24
- accentSlot: data.priceLabel,
16
+ accentSlot: null,
25
17
  theme: { background: "#052e16", accentColor: "#86efac" },
26
18
  });
27
19
  }
@@ -4,18 +4,13 @@
4
4
  */
5
5
  import { renderOgLayout } from "../seo/og-layout";
6
6
  import { resolveOgImageUrl } from "../seo/og";
7
- function formatPriceInr(amount, currency) {
8
- return new Intl.NumberFormat("en-IN", {
9
- style: "currency",
10
- currency,
11
- maximumFractionDigits: 0,
12
- }).format(amount);
13
- }
14
7
  export function renderPrizeDrawOg(doc, opts) {
15
8
  const entryPrice = doc?.pricePerEntry ?? doc?.price ?? null;
16
- const pricePerEntryLabel = typeof entryPrice === "number" && entryPrice > 0
17
- ? `${formatPriceInr(entryPrice, doc?.currency ?? "INR")} / entry`
18
- : "Free entry";
9
+ // "Free entry" is kept — it is a fact about the draw, not a disclosed amount,
10
+ // and it is the half of this chip that actually earns a click. The priced case
11
+ // renders nothing: an OG image is a public URL with the value burned into the
12
+ // pixels, which no sign-in gate or paywall markup can reach.
13
+ const pricePerEntryLabel = typeof entryPrice === "number" && entryPrice > 0 ? null : "Free entry";
19
14
  const max = doc?.prizeMaxEntries ?? null;
20
15
  const current = doc?.prizeCurrentEntries ?? 0;
21
16
  const entriesLabel = typeof max === "number" && max > 0
@@ -9,11 +9,14 @@ export function renderProductOg(doc, opts) {
9
9
  }, opts.siteName);
10
10
  }
11
11
  export function renderProductOgImage(data, siteName) {
12
- const priceLabel = data.price ? `₹${data.price.toLocaleString("en-IN")}` : null;
12
+ // 🛑 No money in the accent slot. An OG image is a PUBLIC image URL anyone can
13
+ // fetch directly, with the amount burned into the pixels — no gate reaches it,
14
+ // and no paywall markup describes it. The auction and pre-order OGs already
15
+ // use a non-money accent (end date / release date); this matches them.
13
16
  return renderOgLayout({
14
17
  title: data.title,
15
18
  imageUrl: data.imageUrl,
16
19
  siteName,
17
- accentSlot: priceLabel,
20
+ accentSlot: null,
18
21
  });
19
22
  }
@@ -62,8 +62,16 @@ function staticPages(baseUrl) {
62
62
  const page = (path, changeFreq, priority) => ({ url: `${baseUrl}${path}`, lastModified: new Date(), changeFrequency: changeFreq, priority });
63
63
  return [
64
64
  page(String(ROUTES.HOME), "daily", 1.0),
65
- page(String(ROUTES.PUBLIC.PRODUCTS), "hourly", 0.9),
66
- page(String(ROUTES.PUBLIC.AUCTIONS), "hourly", 0.9),
65
+ // "daily", not "hourly". These two are the highest-priority URLs in the
66
+ // sitemap AND the entry points into the deepest faceted URL space
67
+ // (tab x sortKey x page), so an hourly hint invites recrawl of that whole
68
+ // surface at a rate a catalogue this size cannot justify — new listings
69
+ // arrive daily, not hourly. `changeFrequency` is only a hint and Google has
70
+ // said it largely disregards it, so treat this as removing a bad signal
71
+ // rather than as a lever: priority is unchanged and neither page loses
72
+ // indexability. 2026-09-14, Vercel usage reduction.
73
+ page(String(ROUTES.PUBLIC.PRODUCTS), "daily", 0.9),
74
+ page(String(ROUTES.PUBLIC.AUCTIONS), "daily", 0.9),
67
75
  page(String(ROUTES.PUBLIC.CATEGORIES), "weekly", 0.8),
68
76
  page(String(ROUTES.PUBLIC.BRANDS), "weekly", 0.7),
69
77
  page(String(ROUTES.PUBLIC.BLOG), "daily", 0.7),
@@ -4,7 +4,6 @@ export interface BaseListingClientShape {
4
4
  title: string;
5
5
  description: string;
6
6
  price: number;
7
- priceLabel: string;
8
7
  currency: string;
9
8
  mainImage: string | null;
10
9
  images: string[];
@@ -15,5 +14,5 @@ export interface BaseListingClientShape {
15
14
  status: ProductDocument["status"];
16
15
  createdAt: string;
17
16
  }
18
- /** Maps the 11 shared fields from a raw Firestore ProductDocument. */
17
+ /** Maps the shared fields from a raw Firestore ProductDocument. */
19
18
  export declare function mapBaseListingFields(doc: ProductDocument): BaseListingClientShape;
@@ -1,12 +1,14 @@
1
- import { formatCurrency } from "../../../../utils/number.formatter";
2
- /** Maps the 11 shared fields from a raw Firestore ProductDocument. */
1
+ /** Maps the shared fields from a raw Firestore ProductDocument. */
3
2
  export function mapBaseListingFields(doc) {
4
3
  return {
5
4
  id: doc.id,
6
5
  title: doc.title,
7
6
  description: doc.description,
8
7
  price: doc.price,
9
- priceLabel: formatCurrency(doc.price, doc.currency ?? "INR"),
8
+ // 🛑 No pre-formatted money string here. A `priceLabel` baked server-side
9
+ // is a ready-to-print amount that any component can render directly,
10
+ // bypassing <GatedPrice> entirely — and it had zero readers, so it was
11
+ // pure exposure. Clients format from `price` through the gate.
10
12
  currency: doc.currency ?? "INR",
11
13
  mainImage: doc.mainImage || doc.images?.[0] || null,
12
14
  images: doc.images ?? [],
@@ -74,12 +74,39 @@ export interface ListingTypePlugin {
74
74
  /** Short label for the dense type-chip row on /products and /admin/products. */
75
75
  chipLabel: string;
76
76
  /**
77
- * Dedicated public browse page for this type, or `null` when it browses on
78
- * `/products` (art and stickers — see CLAUDE.md's Listing Types Reference).
79
- * A non-null value also drives the "full <type> filters →" link shown on
80
- * /products when exactly this type is selected.
77
+ * Dedicated public browse page for this type.
78
+ *
79
+ * EVERY type has one today, including `standard` (→ `/products`) and
80
+ * `art`/`stickers` (→ the combined `/art`). This field is therefore NOT a
81
+ * "does it have its own page" discriminator — use `inGeneralCatalogue` for
82
+ * that. (This comment used to claim `null` meant "browses on /products",
83
+ * which no config has ever matched.)
84
+ *
85
+ * Also drives the "full <type> filters →" link shown on /products when
86
+ * exactly this type is selected — suppressed when it points at /products
87
+ * itself, which would be a self-link.
81
88
  */
82
89
  browseRoute: string | null;
90
+ /**
91
+ * Does this type belong in the GENERAL catalogue at `/products`?
92
+ *
93
+ * `/products` is the general catalogue, not an index of everything. A type
94
+ * whose dedicated page is reachable from `MAIN_NAV_ITEMS` lives only on that
95
+ * page, so an item is never reachable from two places with two different
96
+ * chromes. A type with a dedicated page but NO nav entry
97
+ * (classified / digital-code / live) stays here, because `/products` is its
98
+ * only discovery path — dropping those is how Root Cause #61 happened.
99
+ *
100
+ * Required, not optional, on purpose: `LISTING_TYPE_REGISTRY` is a
101
+ * `Record<ListingType, ListingTypePlugin>`, so a tenth listing type cannot
102
+ * compile without answering this. A hand-written list of "types allowed on
103
+ * /products" is exactly the drift Root Cause #61 documents.
104
+ *
105
+ * 🛑 If you set this `false` for a type, confirm its dedicated page is
106
+ * actually linked from the main nav first — otherwise the type becomes
107
+ * reachable only by typing the URL.
108
+ */
109
+ inGeneralCatalogue: boolean;
83
110
  /**
84
111
  * Which "Show X" default this type's browse surfaces apply. Since 2026-08-24
85
112
  * this drives the LABEL of the availability tab bar's middle tab
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "sold";
21
22
  isAvailable: typeof alwaysAvailable;
22
23
  unavailableClauses: readonly import("../availability").UnavailableClause[];
@@ -20,6 +20,8 @@ export const config = {
20
20
  // detail pages use the standard product route — so this is a real
21
21
  // dedicated browse surface even though detailRoute points at /products.
22
22
  browseRoute: String(ROUTES.PUBLIC.ART),
23
+ // /art ("Art & Stickers") is in MAIN_NAV_ITEMS and spans art + stickers.
24
+ inGeneralCatalogue: false,
23
25
  hideDefault: "sold",
24
26
  // Runs out rather than ending — no time dimension, so the shared
25
27
  // stock-only rules are the whole story for this type.
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "ended";
21
22
  isAvailable: (row: AvailabilityRow, now: Date) => boolean;
22
23
  unavailableClauses: ({
@@ -19,6 +19,9 @@ export const config = {
19
19
  pluralLabel: "Auctions",
20
20
  chipLabel: "Auctions",
21
21
  browseRoute: String(ROUTES.PUBLIC.AUCTIONS),
22
+ // /auctions is in MAIN_NAV_ITEMS. An auction card in the general grid renders
23
+ // Buy-Now chrome rather than bid affordances, so it belongs on its own page.
24
+ inGeneralCatalogue: false,
22
25
  // An auction is never "sold out" — it ends. Hiding by stock would hide
23
26
  // every live auction, since auctions don't carry meaningful stock.
24
27
  hideDefault: "ended",
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "sold";
21
22
  isAvailable: typeof alwaysAvailable;
22
23
  unavailableClauses: readonly import("../availability").UnavailableClause[];
@@ -18,6 +18,9 @@ export const config = {
18
18
  pluralLabel: "Classifieds",
19
19
  chipLabel: "Classifieds",
20
20
  browseRoute: String(ROUTES.PUBLIC.CLASSIFIED),
21
+ // Has a dedicated page but NO MAIN_NAV_ITEMS entry, so /products is its only
22
+ // discovery path. Dropping it here would orphan the type (Root Cause #61).
23
+ inGeneralCatalogue: true,
21
24
  hideDefault: "sold",
22
25
  // Runs out rather than ending — no time dimension, so the shared
23
26
  // stock-only rules are the whole story for this type.
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "sold";
21
22
  isAvailable: (row: AvailabilityRow) => boolean;
22
23
  unavailableClauses: ({
@@ -31,6 +31,9 @@ export const config = {
31
31
  pluralLabel: "Digital Codes",
32
32
  chipLabel: "Digital Codes",
33
33
  browseRoute: String(ROUTES.PUBLIC.DIGITAL_CODES),
34
+ // Has a dedicated page but NO MAIN_NAV_ITEMS entry, so /products is its only
35
+ // discovery path. Dropping it here would orphan the type (Root Cause #61).
36
+ inGeneralCatalogue: true,
34
37
  // "Sold out" for a code pool means codesAvailable hit 0. That was ASSUMED to
35
38
  // be mirrored by stockQuantity, and it isn't — seed and production rows exist
36
39
  // with a drained pool and non-zero stock. The pool count is authoritative.
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "sold";
21
22
  isAvailable: typeof alwaysAvailable;
22
23
  unavailableClauses: readonly import("../availability").UnavailableClause[];
@@ -18,6 +18,9 @@ export const config = {
18
18
  pluralLabel: "Live Items",
19
19
  chipLabel: "Live Items",
20
20
  browseRoute: String(ROUTES.PUBLIC.LIVE),
21
+ // Has a dedicated page but NO MAIN_NAV_ITEMS entry, so /products is its only
22
+ // discovery path. Dropping it here would orphan the type (Root Cause #61).
23
+ inGeneralCatalogue: true,
21
24
  hideDefault: "sold",
22
25
  // Runs out rather than ending — no time dimension, so the shared
23
26
  // stock-only rules are the whole story for this type.
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "closed";
21
22
  isAvailable: (row: AvailabilityRow) => boolean;
22
23
  unavailableClauses: readonly import("../availability").UnavailableClause[];
@@ -19,6 +19,8 @@ export const config = {
19
19
  pluralLabel: "Pre-Orders",
20
20
  chipLabel: "Pre-Orders",
21
21
  browseRoute: String(ROUTES.PUBLIC.PRE_ORDERS),
22
+ // /pre-orders is in MAIN_NAV_ITEMS.
23
+ inGeneralCatalogue: false,
22
24
  hideDefault: "closed",
23
25
  // A pre-order closes when its allocation fills, NOT when its delivery date
24
26
  // passes — a shipped-soon pre-order is still orderable. `preOrderClosed`
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "closed";
21
22
  isAvailable: (row: AvailabilityRow) => boolean;
22
23
  unavailableClauses: ({
@@ -18,6 +18,9 @@ export const config = {
18
18
  pluralLabel: "Prize Draws",
19
19
  chipLabel: "Prize Draws",
20
20
  browseRoute: String(ROUTES.PUBLIC.PRIZE_DRAWS),
21
+ // /prize-draws is in MAIN_NAV_ITEMS. Entry-based pricing reads wrong beside
22
+ // ordinary unit prices in the general grid.
23
+ inGeneralCatalogue: false,
21
24
  hideDefault: "closed",
22
25
  // `prizeRevealStatus`, NOT remaining entries: a draw can close with slots
23
26
  // unsold (the seller ends it early, or the reveal window lapses), and it can
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "sold";
21
22
  isAvailable: typeof alwaysAvailable;
22
23
  unavailableClauses: readonly import("../availability").UnavailableClause[];
@@ -17,6 +17,8 @@ export const config = {
17
17
  pluralLabel: "Products",
18
18
  chipLabel: "Standard",
19
19
  browseRoute: String(ROUTES.PUBLIC.PRODUCTS),
20
+ // The general catalogue IS this type's page.
21
+ inGeneralCatalogue: true,
20
22
  hideDefault: "sold",
21
23
  // Runs out rather than ending — no time dimension, so the shared
22
24
  // stock-only rules are the whole story for this type.
@@ -17,6 +17,7 @@ export declare const config: {
17
17
  pluralLabel: string;
18
18
  chipLabel: string;
19
19
  browseRoute: string;
20
+ inGeneralCatalogue: boolean;
20
21
  hideDefault: "sold";
21
22
  isAvailable: typeof alwaysAvailable;
22
23
  unavailableClauses: readonly import("../availability").UnavailableClause[];
@@ -20,6 +20,8 @@ export const config = {
20
20
  // have no page of their own, which is why this points at ART rather than
21
21
  // a `/stickers` route that does not exist.
22
22
  browseRoute: String(ROUTES.PUBLIC.ART),
23
+ // /art ("Art & Stickers") is in MAIN_NAV_ITEMS and spans art + stickers.
24
+ inGeneralCatalogue: false,
23
25
  hideDefault: "sold",
24
26
  // Runs out rather than ending — no time dimension, so the shared
25
27
  // stock-only rules are the whole story for this type.