@mohasinac/appkit 4.37.1 → 4.39.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/digital-code/CodeRevealPanel.d.ts +20 -1
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +21 -1
- package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.d.ts +40 -0
- package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.js +67 -0
- 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/checkout/actions.js +18 -0
- 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/download.d.ts +60 -0
- package/dist/_internal/server/features/digital-code/download.js +128 -0
- 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/digital-code/pool.d.ts +101 -0
- package/dist/_internal/server/features/digital-code/pool.js +208 -0
- package/dist/_internal/server/features/digital-code/upload.d.ts +51 -0
- package/dist/_internal/server/features/digital-code/upload.js +144 -0
- 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/server/jobs/core/cleanupRtdbEvents.js +40 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +153 -40
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +33 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +93 -25
- package/dist/_internal/server/notifications/send-recorder.d.ts +79 -0
- package/dist/_internal/server/notifications/send-recorder.js +126 -0
- 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 +6 -0
- package/dist/client.js +10 -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/blog/components/BlogIndexPageView.js +10 -1
- package/dist/features/blog/repository/blog.repository.d.ts +14 -0
- package/dist/features/blog/repository/blog.repository.js +23 -1
- 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/CategoryDetailPageView.js +1 -1
- package/dist/features/categories/components/CategoryProductsListing.js +4 -1
- package/dist/features/categories/components/CategoryTree.js +1 -1
- package/dist/features/categories/repository/categories.repository.d.ts +34 -2
- package/dist/features/categories/repository/categories.repository.js +31 -9
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
- package/dist/features/contact/email.js +36 -1
- 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/orders/repository/orders.repository.d.ts +21 -1
- package/dist/features/orders/repository/orders.repository.js +26 -9
- 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/products/repository/products.repository.d.ts +35 -0
- package/dist/features/products/repository/products.repository.js +116 -3
- package/dist/features/products/schemas/firestore.d.ts +37 -0
- package/dist/features/seller/actions/seller-actions.js +4 -13
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -1
- package/dist/features/seller/components/SellerProductShell.js +11 -2
- package/dist/features/stores/api/[storeSlug]/reviews/route.d.ts +6 -1
- package/dist/features/stores/api/[storeSlug]/reviews/route.js +89 -30
- package/dist/features/stores/components/StoresIndexPageView.js +17 -1
- package/dist/features/tester/seed-data/authored/admin__users-trust.js +1 -1
- 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__offers.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +53 -15
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +6 -6
- 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/tester/utils/phases.js +78 -12
- package/dist/features/wishlist/components/WishlistPage.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/providers/db-firebase/rtdb-paths.d.ts +7 -0
- package/dist/providers/db-firebase/rtdb-paths.js +7 -0
- 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/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/server.d.ts +3 -0
- package/dist/server.js +12 -0
- package/dist/styles.css +41 -0
- package/dist/ui/components/FilePickerButton.d.ts +32 -0
- package/dist/ui/components/FilePickerButton.js +23 -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 +4 -0
- package/dist/ui/index.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record what the mailer decided, so the tester can assert on it without an inbox.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* 21 checklist cases are about email. Reading a real mailbox over IMAP can answer
|
|
7
|
+
* some of them, but it is the wrong instrument for most:
|
|
8
|
+
*
|
|
9
|
+
* - **Six of them assert an email should NOT arrive** (kill switch spares staff,
|
|
10
|
+
* opt-out honoured, no employee blast, losing bidders get no mail). Over IMAP,
|
|
11
|
+
* "nothing arrived" is unprovable — you wait N seconds and hope, and a slow
|
|
12
|
+
* inbox is indistinguishable from a working suppression. Here it is one read.
|
|
13
|
+
* - Delivery latency makes every other case slow and flaky for no added truth:
|
|
14
|
+
* whether Resend accepted the handoff says nothing about whether the app made
|
|
15
|
+
* the right decision, which is what the cases are actually about.
|
|
16
|
+
*
|
|
17
|
+
* So this records the DECISION and its inputs. A real inbox is still the only
|
|
18
|
+
* proof of real delivery, and the two or three cases that assert that keep using
|
|
19
|
+
* one (`tester/scripts/lib/inbox.mjs`).
|
|
20
|
+
*
|
|
21
|
+
* ## Why here and not in `guardSend`
|
|
22
|
+
*
|
|
23
|
+
* `SendGuardContext` carries channel/feature/audience and deliberately no
|
|
24
|
+
* recipient — the guard's own docstring explains it must not become the transport.
|
|
25
|
+
* `sendEmail()` is the one place that holds the recipient, the rendered body AND
|
|
26
|
+
* the guard's verdict, which is exactly the tuple a case needs.
|
|
27
|
+
*
|
|
28
|
+
* ## 🛑 Gated, because recording real people's mail is a privacy problem
|
|
29
|
+
*
|
|
30
|
+
* Recording recipients and bodies for actual users would be a new PII store with
|
|
31
|
+
* no retention story. So this writes ONLY when the recipient is the harness
|
|
32
|
+
* mailbox (`TESTER_EMAIL_ID`), or when `EMAIL_RECORDER_ALL` is explicitly set for
|
|
33
|
+
* local debugging. Absent that env var, this function is a no-op and costs one
|
|
34
|
+
* string comparison.
|
|
35
|
+
*
|
|
36
|
+
* Best-effort throughout: a recorder that can fail a send is worse than no
|
|
37
|
+
* recorder, so every path swallows. Same discipline as `recordAdminAction`.
|
|
38
|
+
*
|
|
39
|
+
* @tag domain:notifications,tester
|
|
40
|
+
* @tag layer:server
|
|
41
|
+
* @tag access:server-only
|
|
42
|
+
*/
|
|
43
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
44
|
+
import { serverLogger } from "../../../monitoring";
|
|
45
|
+
import { RTDB_PATHS } from "../../../providers/db-firebase/rtdb-paths";
|
|
46
|
+
/** Firestore collection holding the canonical record. */
|
|
47
|
+
export const EMAIL_EVENTS_COLLECTION = "emailEvents";
|
|
48
|
+
/** Normalise `to` for comparison and storage. */
|
|
49
|
+
function recipients(to) {
|
|
50
|
+
return (Array.isArray(to) ? to : [to]).map((t) => String(t).trim().toLowerCase()).filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Should this send be recorded at all?
|
|
54
|
+
*
|
|
55
|
+
* Exported so the audit and tests can exercise the gate directly rather than
|
|
56
|
+
* inferring it from behaviour.
|
|
57
|
+
*/
|
|
58
|
+
export function shouldRecordSend(to) {
|
|
59
|
+
if (process.env.EMAIL_RECORDER_ALL === "true")
|
|
60
|
+
return true;
|
|
61
|
+
const mailbox = process.env.TESTER_EMAIL_ID?.trim().toLowerCase();
|
|
62
|
+
if (!mailbox)
|
|
63
|
+
return false;
|
|
64
|
+
return recipients(to).includes(mailbox);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write the record. Never throws, never blocks the send.
|
|
68
|
+
*
|
|
69
|
+
* Firestore is canonical and RTDB is only a ping — the same split the messaging
|
|
70
|
+
* feature already uses. The body does NOT go into RTDB: `bulk_events` is
|
|
71
|
+
* uncapped and a newsletter CSV already goes there, which is the mistake not to
|
|
72
|
+
* repeat on a channel that fires on every email.
|
|
73
|
+
*/
|
|
74
|
+
export async function recordSendAttempt(record) {
|
|
75
|
+
if (!shouldRecordSend(record.to))
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
const { getAdminDb } = await import("../../../providers/db-firebase/admin");
|
|
79
|
+
const db = getAdminDb();
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const doc = {
|
|
82
|
+
to: recipients(record.to),
|
|
83
|
+
subject: record.subject,
|
|
84
|
+
feature: record.feature,
|
|
85
|
+
audience: record.audience,
|
|
86
|
+
channel: record.channel,
|
|
87
|
+
outcome: record.outcome,
|
|
88
|
+
reason: record.reason ?? null,
|
|
89
|
+
html: record.html ?? null,
|
|
90
|
+
createdAt: now,
|
|
91
|
+
};
|
|
92
|
+
const ref = await db.collection(EMAIL_EVENTS_COLLECTION).add(doc);
|
|
93
|
+
/*
|
|
94
|
+
* The ping. Tiny by design: an id, the outcome and the feature are enough for
|
|
95
|
+
* a poller to know something happened and go read the Firestore row.
|
|
96
|
+
* Best-effort separately from the write above — losing the ping costs a
|
|
97
|
+
* poller one extra second, losing the record costs the case its evidence.
|
|
98
|
+
*/
|
|
99
|
+
try {
|
|
100
|
+
const { getAdminRealtimeDb } = await import("../../../providers/db-firebase/admin");
|
|
101
|
+
await getAdminRealtimeDb()
|
|
102
|
+
.ref(`${RTDB_PATHS.EMAIL_EVENTS}/${ref.id}`)
|
|
103
|
+
.set({
|
|
104
|
+
id: ref.id,
|
|
105
|
+
feature: record.feature,
|
|
106
|
+
outcome: record.outcome,
|
|
107
|
+
at: now.toISOString(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// Deliberately unreported: the Firestore row above is the record, and it
|
|
112
|
+
// already succeeded by the time this runs. Losing the ping costs a poller
|
|
113
|
+
// one extra second of latency and loses no evidence, so logging it would
|
|
114
|
+
// add noise to every transient RTDB blip for no recoverable information.
|
|
115
|
+
void normalizeError(err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const n = normalizeError(err);
|
|
120
|
+
serverLogger.warn("recordSendAttempt: could not record email event", {
|
|
121
|
+
feature: record.feature,
|
|
122
|
+
outcome: record.outcome,
|
|
123
|
+
message: n.message,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -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.
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
import { normalizeError } from "../../errors/normalize";
|
|
3
3
|
import { CLIENT_ERROR_ENDPOINTS } from "../../constants/api-endpoints";
|
|
4
4
|
const SEEN = new Map();
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* How long an identical error is suppressed before it is reported again.
|
|
7
|
+
*
|
|
8
|
+
* This is a rate limit on a beacon that costs a function invocation per send,
|
|
9
|
+
* so the window has to be sized against the worst case rather than the typical
|
|
10
|
+
* one: a component stuck in a render loop re-throws the SAME error forever, and
|
|
11
|
+
* at the original 5s that was 12 invocations/minute/tab, indefinitely, from a
|
|
12
|
+
* single broken page. The first occurrence is still reported immediately —
|
|
13
|
+
* widening this loses no distinct error, only repeats of one already recorded.
|
|
14
|
+
*/
|
|
15
|
+
const DEDUPE_WINDOW_MS = 60000;
|
|
6
16
|
const MAX_SEEN = 32;
|
|
7
17
|
function topFrame(stack) {
|
|
8
18
|
if (!stack)
|
package/dist/client.d.ts
CHANGED
|
@@ -61,6 +61,10 @@ export { SideModal } from "./ui/components/SideModal";
|
|
|
61
61
|
export { UnsavedChangesModal } from "./ui/components/UnsavedChangesModal";
|
|
62
62
|
export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
|
|
63
63
|
export type { LoginRequiredModalProps } from "./ui/components/LoginRequiredModal";
|
|
64
|
+
export { GatedPrice, PricesOnly, GATED_PRICE_CLASS } from "./ui/components/GatedPrice";
|
|
65
|
+
export type { GatedPriceProps } from "./ui/components/GatedPrice";
|
|
66
|
+
export { useCanSeePrices } from "./react/hooks/useCanSeePrices";
|
|
67
|
+
export type { CanSeePricesState } from "./react/hooks/useCanSeePrices";
|
|
64
68
|
export { isAuthError } from "./utils/auth-error";
|
|
65
69
|
export { useToast } from "./ui/components/Toast";
|
|
66
70
|
export { StickyToolbar } from "./ui/components/StickyToolbar";
|
|
@@ -410,6 +414,8 @@ export { NavPermissionsManager } from "./features/site-settings/components/NavPe
|
|
|
410
414
|
export type { NavPermissionsManagerProps, NavGroup as NavPermissionsGroup, NavItem as NavPermissionsItem } from "./features/site-settings/components/NavPermissionsManager";
|
|
411
415
|
export { CodeRevealPanel } from "./_internal/client/features/digital-code/CodeRevealPanel";
|
|
412
416
|
export type { CodeRevealPanelProps, RevealedCode } from "./_internal/client/features/digital-code/CodeRevealPanel";
|
|
417
|
+
export { DigitalContentPoolManager } from "./_internal/client/features/digital-code/DigitalContentPoolManager";
|
|
418
|
+
export type { DigitalContentPoolManagerProps, PoolEntry } from "./_internal/client/features/digital-code/DigitalContentPoolManager";
|
|
413
419
|
export { PhysicalLocationModal } from "./features/seller/components/PhysicalLocationModal";
|
|
414
420
|
export type { PhysicalLocation } from "./features/seller/components/PhysicalLocationModal";
|
|
415
421
|
export { Toggle } from "./ui/components/Toggle";
|
package/dist/client.js
CHANGED
|
@@ -89,6 +89,10 @@ export { UnsavedChangesModal } from "./ui/components/UnsavedChangesModal";
|
|
|
89
89
|
// [CLIENT-ONLY]-Cannot run in SSR mode — uses browser-only APIs (window.location).
|
|
90
90
|
// LoginRequiredModal - Modal prompting unauthenticated users to log in.
|
|
91
91
|
export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
|
|
92
|
+
// GatedPrice - a money amount only signed-in visitors may read. GATED_PRICE_CLASS
|
|
93
|
+
// is a contract with the JSON-LD paywall markup; see the component.
|
|
94
|
+
export { GatedPrice, PricesOnly, GATED_PRICE_CLASS } from "./ui/components/GatedPrice";
|
|
95
|
+
export { useCanSeePrices } from "./react/hooks/useCanSeePrices";
|
|
92
96
|
// isAuthError - Detects auth/authorization errors from server actions or fetch responses.
|
|
93
97
|
export { isAuthError } from "./utils/auth-error";
|
|
94
98
|
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
@@ -420,6 +424,12 @@ export { ActionPermissionsManager } from "./features/site-settings/components/Ac
|
|
|
420
424
|
export { NavPermissionsManager } from "./features/site-settings/components/NavPermissionsManager";
|
|
421
425
|
// ── Digital-code code-reveal panel (still used directly on order pages) ───────
|
|
422
426
|
export { CodeRevealPanel } from "./_internal/client/features/digital-code/CodeRevealPanel";
|
|
427
|
+
/*
|
|
428
|
+
* The SELLER half. Without a surface the pool routes are unreachable, which is
|
|
429
|
+
* the same "finished feature nobody can get to" gap that left the pool empty in
|
|
430
|
+
* the first place (its only writer answered 501).
|
|
431
|
+
*/
|
|
432
|
+
export { DigitalContentPoolManager } from "./_internal/client/features/digital-code/DigitalContentPoolManager";
|
|
423
433
|
export { PhysicalLocationModal } from "./features/seller/components/PhysicalLocationModal";
|
|
424
434
|
// [CLIENT-ONLY] — Toggle switch primitive.
|
|
425
435
|
export { Toggle } from "./ui/components/Toggle";
|
|
@@ -18,7 +18,17 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
18
18
|
import { apiClient } from "../../http";
|
|
19
19
|
import { CART_ENDPOINTS, ACCOUNT_ENDPOINTS } from "../../constants/api-endpoints";
|
|
20
20
|
import { getCartOps, clearCartOps, getWishlistOps, clearWishlistOps, } from "../../features/cart/utils/pending-ops";
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Cadence for replaying queued cart/wishlist ops.
|
|
23
|
+
*
|
|
24
|
+
* A tick with no pending ops is nearly free, but a tick WITH ops also
|
|
25
|
+
* invalidates the cart and wishlist queries, so each one can cascade into
|
|
26
|
+
* refetches. At 30s that ran for every signed-in tab for as long as it stayed
|
|
27
|
+
* open. 2 minutes is ample for an offline-replay backstop — the queue is also
|
|
28
|
+
* flushed eagerly on `visibilitychange` and `pagehide` (see below), which is
|
|
29
|
+
* what actually catches the tab-closing case that matters.
|
|
30
|
+
*/
|
|
31
|
+
const SYNC_INTERVAL_MS = 120000;
|
|
22
32
|
async function replayCartOps() {
|
|
23
33
|
const ops = getCartOps();
|
|
24
34
|
if (ops.length === 0)
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
2
2
|
import { apiClient } from "../../../http";
|
|
3
3
|
import { NOTIFICATIONS_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
4
|
+
/**
|
|
5
|
+
* Poll cadence for the notification badge. See the comment at the
|
|
6
|
+
* `refetchInterval` below for why this is minutes rather than seconds.
|
|
7
|
+
*/
|
|
8
|
+
const NOTIFICATIONS_POLL_MS = 5 * 60000;
|
|
4
9
|
export function useNotifications(limit = 10, options) {
|
|
5
10
|
const notificationsEndpoint = options?.notificationsEndpoint ?? NOTIFICATIONS_ENDPOINTS.LIST;
|
|
6
11
|
const readAllEndpoint = options?.readAllEndpoint ?? NOTIFICATIONS_ENDPOINTS.READ_ALL;
|
|
@@ -8,12 +13,27 @@ export function useNotifications(limit = 10, options) {
|
|
|
8
13
|
const { data, isLoading, refetch } = useQuery({
|
|
9
14
|
queryKey: ["notifications", "list", String(limit)],
|
|
10
15
|
queryFn: () => apiClient.get(`${notificationsEndpoint}?limit=${limit}`),
|
|
11
|
-
staleTime:
|
|
16
|
+
staleTime: NOTIFICATIONS_POLL_MS,
|
|
12
17
|
// Notifications originate server-side (order/bid/moderation events) with
|
|
13
|
-
// no client-driven local-first path to short-circuit, unlike cart/
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
18
|
+
// no client-driven local-first path to short-circuit, unlike cart/wishlist,
|
|
19
|
+
// so the badge does need a poll rather than a local-first read.
|
|
20
|
+
//
|
|
21
|
+
// 🛑 This hook is mounted in TitleBar, i.e. on EVERY page of the site, so
|
|
22
|
+
// its interval is multiplied by every open tab of every signed-in user for
|
|
23
|
+
// as long as those tabs exist — it is wall-clock, not activity-gated, so a
|
|
24
|
+
// tab left open overnight keeps firing. At the original 30s that was 120
|
|
25
|
+
// invocations/hour/tab for an unread COUNT, and it was a material share of
|
|
26
|
+
// the Vercel function-invocation spend that paused the account (2026-09-14).
|
|
27
|
+
//
|
|
28
|
+
// 5 minutes + refetch-on-focus is the trade: a user who is actually looking
|
|
29
|
+
// at the tab gets a fresh badge the moment they return to it, and an
|
|
30
|
+
// abandoned tab costs 12 invocations/hour instead of 120.
|
|
31
|
+
refetchInterval: NOTIFICATIONS_POLL_MS,
|
|
32
|
+
// The global QueryClient default is `refetchOnWindowFocus: false`
|
|
33
|
+
// (src/app/[locale]/QueryProvider.tsx). Opt back in HERE specifically,
|
|
34
|
+
// because it is what makes the longer interval acceptable rather than
|
|
35
|
+
// simply slower.
|
|
36
|
+
refetchOnWindowFocus: true,
|
|
17
37
|
});
|
|
18
38
|
const { mutate: markRead } = useMutation({
|
|
19
39
|
mutationFn: (id) => apiClient.patch(readEndpoint(id), {}),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { useBottomActions } from "../../layout/hooks/useBottomActions";
|
|
5
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
5
6
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
7
|
import { ACTION_ID, ACTION_META } from "../../products/constants/action-defs";
|
|
7
8
|
import { useCountdown } from "../../../react";
|
|
@@ -40,6 +41,7 @@ export function AuctionBottomActions(props) {
|
|
|
40
41
|
const { currentBid, currency, bidCount, isEnded, auctionEndDate, buyNowPrice } = props;
|
|
41
42
|
const [open, setOpen] = useState(false);
|
|
42
43
|
const remaining = useCountdown(auctionEndDate ?? undefined);
|
|
44
|
+
const { canSeePrices } = useCanSeePrices();
|
|
43
45
|
const placeBidMeta = ACTION_META[ACTION_ID.PLACE_BID];
|
|
44
46
|
const buyNowMeta = ACTION_META[ACTION_ID.BUY_NOW_AUCTION];
|
|
45
47
|
// Same single predicate the form and the card use. `!!props.onBuyNow` matters
|
|
@@ -54,14 +56,20 @@ export function AuctionBottomActions(props) {
|
|
|
54
56
|
? [
|
|
55
57
|
{
|
|
56
58
|
...buyNowMeta,
|
|
57
|
-
|
|
59
|
+
// Both of these are plain strings on the bottom bar rather
|
|
60
|
+
// than slots, so the gate happens as they are built.
|
|
61
|
+
label: canSeePrices
|
|
62
|
+
? `Buy Now — ${formatCurrency(buyNowPrice, currency)}`
|
|
63
|
+
: "Buy Now",
|
|
58
64
|
onClick: () => setOpen(true),
|
|
59
65
|
},
|
|
60
66
|
]
|
|
61
67
|
: []),
|
|
62
68
|
],
|
|
63
69
|
secondaryLabel: formatCountdownLabel(remaining),
|
|
64
|
-
infoLabel:
|
|
70
|
+
infoLabel: canSeePrices
|
|
71
|
+
? `${formatCurrency(currentBid, currency)} · ${bidCount} bid${bidCount !== 1 ? "s" : ""}`
|
|
72
|
+
: `Sign in to see the bid · ${bidCount} bid${bidCount !== 1 ? "s" : ""}`,
|
|
65
73
|
desktop: "after-scroll",
|
|
66
74
|
});
|
|
67
75
|
return (_jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...props }) }));
|
|
@@ -17,7 +17,7 @@ import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
|
17
17
|
import { safeDisplayName } from "../../../security";
|
|
18
18
|
import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal";
|
|
19
19
|
import { isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
|
|
20
|
-
import { Button, Container, CountdownDisplay, Div, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, Dl, Dt, Dd, } from "../../../ui";
|
|
20
|
+
import { Button, Container, CountdownDisplay, Div, GatedPrice, PricesOnly, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, Dl, Dt, Dd, } from "../../../ui";
|
|
21
21
|
import { AuctionDetailView } from "../../products/components/AuctionDetailView";
|
|
22
22
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
23
23
|
import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
|
|
@@ -48,7 +48,7 @@ function toDescriptionHtml(raw) {
|
|
|
48
48
|
}
|
|
49
49
|
function renderAuctionInfoPanel(props) {
|
|
50
50
|
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
51
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
|
|
51
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: _jsx(GatedPrice, { label: "Sign in to see the Buy Now price", children: formatCurrency(buyNowPrice, currency) }) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Store reviews for the auction's seller.
|
|
@@ -218,7 +218,16 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
218
218
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
219
219
|
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => renderAuctionInfoPanel({ productId: String(product.id), title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale: isFinalSale(p), category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Stack, { id: "auction-bid-form", gap: "3", children: _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "xl", badgeSize: "xs" }), !isEnded && (_jsx(LiveMinIncrement, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride }))] }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] }) })) : (
|
|
220
220
|
/* Read-only bid panel — shown when no bid action is wired (preview/demo) */
|
|
221
|
-
_jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children:
|
|
221
|
+
_jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children: _jsx(Text, { size: "xs", color: "muted", children: _jsxs(GatedPrice, { label: "Sign in to see the starting bid", children: ["Starting bid: ", formatCurrency(startingBid, currency), !isEnded && _jsxs(_Fragment, { children: [" \u00B7 min increment ", formatCurrency(initialMinBidIncrement, currency)] })] }) }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number",
|
|
222
|
+
// The minimum is stated in the gated line above rather than
|
|
223
|
+
// in the placeholder: a placeholder is a plain string prop,
|
|
224
|
+
// so it cannot be gated in place and would print the bid
|
|
225
|
+
// floor to signed-out visitors.
|
|
226
|
+
placeholder: "Enter your bid", min: initialMinBid, "aria-label": "Your bid amount", disabled: isEnded }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && buyNowPrice !== null && (
|
|
227
|
+
// Hidden rather than prompted: the Buy Now chip at the top
|
|
228
|
+
// of the panel already carries the gate, so repeating the
|
|
229
|
+
// prompt here would be the same affordance twice.
|
|
230
|
+
_jsx(PricesOnly, { children: _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Buy Now available at ", formatCurrency(buyNowPrice, currency)] }) }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: _jsx(GatedPrice, { label: "Sign in to see the current bid", children: formatCurrency(currentBid, currency) }) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), endDate && (_jsxs(Text, { align: "center", className: "mb-3", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
|
|
222
231
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
223
232
|
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
224
233
|
id: s.id,
|