@mohasinac/appkit 4.37.0 → 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.
- package/dist/_internal/server/features/bundles/og.d.ts +0 -1
- package/dist/_internal/server/features/bundles/og.js +2 -18
- package/dist/_internal/server/features/catalogue/og.d.ts +0 -1
- package/dist/_internal/server/features/catalogue/og.js +1 -9
- package/dist/_internal/server/features/classified/og.d.ts +0 -1
- package/dist/_internal/server/features/classified/og.js +1 -9
- package/dist/_internal/server/features/digital-code/og.d.ts +0 -1
- package/dist/_internal/server/features/digital-code/og.js +1 -9
- package/dist/_internal/server/features/grouped/og.d.ts +7 -4
- package/dist/_internal/server/features/grouped/og.js +7 -6
- package/dist/_internal/server/features/item-requests/og.d.ts +0 -1
- package/dist/_internal/server/features/item-requests/og.js +2 -18
- package/dist/_internal/server/features/live/og.d.ts +0 -1
- package/dist/_internal/server/features/live/og.js +1 -9
- package/dist/_internal/server/features/prize-draws/og.js +5 -10
- package/dist/_internal/server/features/products/og.js +5 -2
- package/dist/_internal/server/features/seo/sitemap.js +10 -2
- package/dist/_internal/server/features/shared/listing-adapters.d.ts +1 -2
- package/dist/_internal/server/features/shared/listing-adapters.js +5 -3
- package/dist/_internal/shared/listing-types/_registry.d.ts +31 -4
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/config.js +2 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/config.js +3 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/classified/config.js +3 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +3 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/live/config.js +3 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +3 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/config.js +2 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
- package/dist/client/observability/reportClientError.js +11 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.js +4 -0
- package/dist/core/hooks/useSyncManager.js +11 -1
- package/dist/features/account/hooks/useNotifications.js +25 -5
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +12 -3
- package/dist/features/auctions/components/LiveBidPrice.js +2 -2
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +1 -1
- package/dist/features/auctions/components/LiveMinIncrement.js +8 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useAuctions.js +12 -1
- package/dist/features/auctions/hooks/useRealtimeBids.d.ts +3 -0
- package/dist/features/auctions/hooks/useRealtimeBids.js +138 -36
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +2 -2
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -2
- package/dist/features/categories/components/BundleCollage.js +10 -5
- package/dist/features/categories/components/BundleDetailView.js +7 -3
- package/dist/features/categories/components/CategoryProductsListing.js +4 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -4
- package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +2 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +4 -1
- package/dist/features/products/actions/product-actions.d.ts +9 -0
- package/dist/features/products/actions/product-actions.js +11 -0
- package/dist/features/products/columns/productTableColumns.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +4 -1
- package/dist/features/products/components/BidHistory.js +2 -2
- package/dist/features/products/components/CompareOverlay.js +2 -2
- package/dist/features/products/components/GroupMemberPicker.js +5 -5
- package/dist/features/products/components/ListingBottomActions.js +12 -3
- package/dist/features/products/components/MakeOfferButton.js +7 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +4 -4
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
- package/dist/features/products/components/PrizeDrawCollage.js +16 -5
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +4 -1
- package/dist/features/products/components/PrizeRevealModal.js +2 -2
- package/dist/features/products/components/ProductDetailActions.js +10 -3
- package/dist/features/products/components/ProductFilters.js +2 -2
- package/dist/features/products/components/ProductGrid.js +5 -2
- package/dist/features/products/components/ProductsIndexListing.js +12 -1
- package/dist/features/products/components/ProductsIndexPageView.js +26 -0
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -2
- package/dist/features/products/constants/listing-tabs.d.ts +36 -12
- package/dist/features/products/constants/listing-tabs.js +38 -14
- package/dist/features/products/constants/sieve.d.ts +14 -0
- package/dist/features/products/constants/sieve.js +39 -0
- package/dist/features/tester/seed-data/authored/buying__bidding.js +5 -4
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +85 -85
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +2 -2
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +53 -15
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +6 -4
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +8 -8
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +8 -8
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +16 -0
- package/dist/features/tester/seed-data/authored/seo__og-images.js +17 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +41 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +36 -6
- package/dist/features/wishlist/components/WishlistPage.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/react/contexts/SessionContext.js +40 -1
- package/dist/react/hooks/useCanSeePrices.d.ts +37 -0
- package/dist/react/hooks/useCanSeePrices.js +40 -0
- package/dist/seed/addresses-seed-data.js +36 -2
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +6 -1
- package/dist/seed/users-seed-data.js +10 -2
- package/dist/seo/json-ld.d.ts +21 -0
- package/dist/seo/json-ld.js +45 -0
- package/dist/styles.css +41 -0
- package/dist/ui/components/GatedPrice.d.ts +66 -0
- package/dist/ui/components/GatedPrice.js +80 -0
- package/dist/ui/components/GatedPrice.style.css +40 -0
- package/dist/ui/components/PriceDisplay.d.ts +12 -0
- package/dist/ui/components/PriceDisplay.js +17 -17
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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: [
|
|
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,
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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,
|
|
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 })),
|
|
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,
|
|
@@ -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:
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
17
|
+
/** Maps the shared fields from a raw Firestore ProductDocument. */
|
|
19
18
|
export declare function mapBaseListingFields(doc: ProductDocument): BaseListingClientShape;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
@@ -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.
|
|
@@ -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`
|
|
@@ -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.
|