@mohasinac/appkit 4.32.6 → 4.33.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/products/actions.js +29 -3
- package/dist/_internal/server/features/products/list-public.d.ts +1 -1
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/stores/denormalize.d.ts +44 -0
- package/dist/_internal/server/features/stores/denormalize.js +58 -0
- package/dist/_internal/server/features/stores/index.d.ts +1 -0
- package/dist/_internal/server/features/stores/index.js +1 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/storeNameBackfill.d.ts +27 -0
- package/dist/_internal/server/jobs/core/storeNameBackfill.js +90 -0
- package/dist/_internal/shared/features/products/to-product-item.js +7 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -1
- package/dist/features/admin/actions/admin-actions.js +11 -1
- package/dist/features/admin/components/AdminSectionsView.js +17 -20
- package/dist/features/admin/components/sections/adminSectionsBuildParse.js +31 -14
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +37 -7
- package/dist/features/admin/components/sections/adminSectionsTypes.js +11 -7
- package/dist/features/auctions/components/AuctionDetailPageView.js +56 -36
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +9 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -2
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +7 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +3 -3
- package/dist/features/cart/repository/cart.repository.d.ts +27 -6
- package/dist/features/cart/repository/cart.repository.js +52 -21
- package/dist/features/cart/utils/guest-cart.js +5 -0
- package/dist/features/cart/utils/pending-ops.d.ts +15 -0
- package/dist/features/cart/utils/pending-ops.js +24 -0
- package/dist/features/homepage/components/BlogArticlesSection.d.ts +7 -1
- package/dist/features/homepage/components/BlogArticlesSection.js +5 -3
- package/dist/features/homepage/components/CTABannerSection.d.ts +18 -1
- package/dist/features/homepage/components/CTABannerSection.js +22 -4
- package/dist/features/homepage/components/CustomerReviewsSection.d.ts +7 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
- package/dist/features/homepage/components/FAQSection.js +2 -11
- package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +5 -3
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +7 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +6 -5
- package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedProductsSection.js +5 -3
- package/dist/features/homepage/components/HeroCarousel.d.ts +23 -2
- package/dist/features/homepage/components/HeroCarousel.js +22 -9
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +6 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +11 -3
- package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +9 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +7 -4
- package/dist/features/homepage/components/StatsCounterSection.d.ts +6 -1
- package/dist/features/homepage/components/StatsCounterSection.js +3 -3
- package/dist/features/homepage/constants/faq-category-labels.d.ts +15 -0
- package/dist/features/homepage/constants/faq-category-labels.js +23 -0
- package/dist/features/homepage/constants/section-copy.d.ts +107 -0
- package/dist/features/homepage/constants/section-copy.js +96 -0
- package/dist/features/homepage/lib/section-renderer.d.ts +16 -1
- package/dist/features/homepage/lib/section-renderer.js +58 -37
- package/dist/features/homepage/repository/homepage-sections.repository.d.ts +23 -0
- package/dist/features/homepage/repository/homepage-sections.repository.js +40 -0
- package/dist/features/homepage/schemas/firestore.d.ts +120 -44
- package/dist/features/media/MediaImage.d.ts +10 -1
- package/dist/features/media/MediaImage.js +6 -3
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/products/components/CardStoreLine.d.ts +32 -0
- package/dist/features/products/components/CardStoreLine.js +14 -0
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
- package/dist/features/products/components/ProductGrid.js +3 -6
- package/dist/features/products/schemas/firestore.d.ts +17 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/actions/seller-actions.d.ts +17 -0
- package/dist/features/seller/actions/seller-actions.js +54 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +3 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +119 -0
- package/dist/seed/carousel-slides-seed-data.js +154 -22
- package/dist/seed/carousels-seed-data.js +4 -1
- package/dist/seed/homepage-sections-seed-data.js +337 -254
- package/dist/seed/products-preorders-seed-data.d.ts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -152,19 +152,27 @@ export declare const carouselSlideQueryHelpers: {
|
|
|
152
152
|
export declare function createCarouselId(title: string): string;
|
|
153
153
|
export declare function isValidGridPosition(gridRow: number, gridCol: number): boolean;
|
|
154
154
|
export declare function canActivateSlide(currentActiveCount: number): boolean;
|
|
155
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Config for the hero carousel section — references the carouselSlides collection.
|
|
157
|
+
*
|
|
158
|
+
* Every field here reaches `HeroCarousel` as a prop. It did not used to: the
|
|
159
|
+
* section renderer emitted `<HeroCarousel initialSlides={slides} />` and dropped
|
|
160
|
+
* the whole config, so all six controls were inert in the admin UI.
|
|
161
|
+
*
|
|
162
|
+
* `carouselId` was REMOVED. Nothing ever read it — SSR calls
|
|
163
|
+
* `carouselRepository.getActiveSlides()` (all active, capped at 5) and the
|
|
164
|
+
* client refetch goes through `useHeroCarousel`, and neither accepts an id.
|
|
165
|
+
* Honouring it means changing the repository, `GET /api/homepage/carousel` and
|
|
166
|
+
* the hook; leaving it in the schema advertised a capability that did not exist.
|
|
167
|
+
*/
|
|
156
168
|
export interface CarouselSectionConfig {
|
|
169
|
+
/** Accessible name for the carousel region. Not rendered as visible copy. */
|
|
157
170
|
title?: string;
|
|
158
171
|
height?: CarouselSlideHeight;
|
|
159
172
|
defaultAutoplayDelayMs?: number;
|
|
160
173
|
pauseOnHover?: boolean;
|
|
161
174
|
showDots?: boolean;
|
|
162
175
|
showArrows?: boolean;
|
|
163
|
-
/**
|
|
164
|
-
* ID of a named CarouselDocument (EX2).
|
|
165
|
-
* When absent, the section loads all active slides (pre-EX2 default hero behavior).
|
|
166
|
-
*/
|
|
167
|
-
carouselId?: string;
|
|
168
176
|
}
|
|
169
177
|
export interface WelcomeSectionConfig {
|
|
170
178
|
h1: string;
|
|
@@ -173,8 +181,11 @@ export interface WelcomeSectionConfig {
|
|
|
173
181
|
showCTA: boolean;
|
|
174
182
|
ctaText?: string;
|
|
175
183
|
ctaLink?: string;
|
|
176
|
-
/**
|
|
177
|
-
|
|
184
|
+
/** Small eyebrow pill above the headline. Defaults to a generic label. */
|
|
185
|
+
pillLabel?: string;
|
|
186
|
+
/** Secondary CTA beside the primary one. */
|
|
187
|
+
secondaryCtaText?: string;
|
|
188
|
+
secondaryCtaLink?: string;
|
|
178
189
|
}
|
|
179
190
|
export type TrustIndicator = {
|
|
180
191
|
id: string;
|
|
@@ -193,11 +204,14 @@ export interface SectionCTA {
|
|
|
193
204
|
}
|
|
194
205
|
export interface CategoriesSectionConfig {
|
|
195
206
|
title: string;
|
|
196
|
-
|
|
207
|
+
/** Cards to render. Was pinned to the literal `4`; widened so the cap is real. */
|
|
208
|
+
maxCategories?: number;
|
|
197
209
|
autoScroll: boolean;
|
|
198
210
|
scrollInterval: number;
|
|
199
211
|
/** Circular auto-rotate at the carousel edges. Default: true. */
|
|
200
212
|
loop?: boolean;
|
|
213
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
214
|
+
viewMoreLabel?: string;
|
|
201
215
|
/** Optional CTA button rendered below the scroller. */
|
|
202
216
|
cta?: SectionCTA;
|
|
203
217
|
/** Optional client-side filter chips above the scroller. */
|
|
@@ -206,8 +220,6 @@ export interface CategoriesSectionConfig {
|
|
|
206
220
|
rootOnly?: boolean;
|
|
207
221
|
rootCategoryId?: string;
|
|
208
222
|
};
|
|
209
|
-
/** Optional section background — reuses the carousel's background schema for consistency. */
|
|
210
|
-
background?: CarouselBackground;
|
|
211
223
|
}
|
|
212
224
|
export interface BrandsSectionConfig {
|
|
213
225
|
title: string;
|
|
@@ -217,6 +229,8 @@ export interface BrandsSectionConfig {
|
|
|
217
229
|
scrollInterval: number;
|
|
218
230
|
/** Circular auto-rotate at the carousel edges. Default: true. */
|
|
219
231
|
loop?: boolean;
|
|
232
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
233
|
+
viewMoreLabel?: string;
|
|
220
234
|
/** Optional CTA button rendered below the scroller. */
|
|
221
235
|
cta?: SectionCTA;
|
|
222
236
|
/** Optional client-side filter chips above the scroller. */
|
|
@@ -229,6 +243,7 @@ export type SectionPagination = "load-more" | "arrows" | "auto-scroll";
|
|
|
229
243
|
export interface ProductsSectionConfig {
|
|
230
244
|
title: string;
|
|
231
245
|
subtitle?: string;
|
|
246
|
+
/** @deprecated Legacy name the admin builder writes; prefer `maxItems`. */
|
|
232
247
|
maxProducts?: number;
|
|
233
248
|
/** Number of display rows (1–4). Each row shows itemsPerRow cards. Default: 1. */
|
|
234
249
|
rows?: number;
|
|
@@ -240,6 +255,8 @@ export interface ProductsSectionConfig {
|
|
|
240
255
|
pagination?: SectionPagination;
|
|
241
256
|
autoScroll: boolean;
|
|
242
257
|
scrollInterval: number;
|
|
258
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
259
|
+
viewMoreLabel?: string;
|
|
243
260
|
filterByBrand?: string;
|
|
244
261
|
sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
|
|
245
262
|
filterByCategory?: string;
|
|
@@ -288,32 +305,47 @@ export interface StatsSectionConfig {
|
|
|
288
305
|
suffix?: string;
|
|
289
306
|
}>;
|
|
290
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* The count fields here were declared as LITERAL types (`maxAuctions: 18`,
|
|
310
|
+
* `rows: 2`, …), which made them unconfigurable by construction — an admin
|
|
311
|
+
* setting 12 was a type error, and the pre-order config already contradicted
|
|
312
|
+
* its own literals in the seed. They are plain optional numbers now that the
|
|
313
|
+
* renderer actually forwards them.
|
|
314
|
+
*/
|
|
291
315
|
export interface AuctionsSectionConfig {
|
|
292
316
|
title: string;
|
|
293
317
|
subtitle?: string;
|
|
294
|
-
maxAuctions
|
|
295
|
-
rows
|
|
296
|
-
itemsPerRow
|
|
297
|
-
mobileItemsPerRow
|
|
318
|
+
maxAuctions?: number;
|
|
319
|
+
rows?: number;
|
|
320
|
+
itemsPerRow?: number;
|
|
321
|
+
mobileItemsPerRow?: number;
|
|
298
322
|
autoScroll: boolean;
|
|
299
323
|
scrollInterval: number;
|
|
324
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
325
|
+
viewMoreLabel?: string;
|
|
300
326
|
filterByBrand?: string;
|
|
301
327
|
sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
|
|
302
328
|
filterByCategory?: string;
|
|
303
329
|
maxCount?: 5 | 10 | 20;
|
|
304
330
|
loop?: boolean;
|
|
305
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* `source` and `placeId` were REMOVED. They implied this section could render
|
|
334
|
+
* Google reviews, which it never could — `HomepageCustomerReviewsSection` only
|
|
335
|
+
* ever calls `useHomepageReviews()`. The `google-reviews` section type is the
|
|
336
|
+
* real surface for that, and it has its own config with a required `placeId`.
|
|
337
|
+
*/
|
|
306
338
|
export interface ReviewsSectionConfig {
|
|
307
339
|
title: string;
|
|
308
|
-
maxReviews
|
|
309
|
-
itemsPerView
|
|
310
|
-
mobileItemsPerView
|
|
340
|
+
maxReviews?: number;
|
|
341
|
+
itemsPerView?: number;
|
|
342
|
+
mobileItemsPerView?: number;
|
|
311
343
|
autoScroll: boolean;
|
|
312
344
|
scrollInterval: number;
|
|
313
345
|
/** Circular auto-rotate at the carousel edges. Default: true. */
|
|
314
346
|
loop?: boolean;
|
|
315
|
-
|
|
316
|
-
|
|
347
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
348
|
+
viewMoreLabel?: string;
|
|
317
349
|
}
|
|
318
350
|
export interface WhatsAppCommunitySectionConfig {
|
|
319
351
|
title: string;
|
|
@@ -324,9 +356,30 @@ export interface WhatsAppCommunitySectionConfig {
|
|
|
324
356
|
buttonText: string;
|
|
325
357
|
testimonial?: string;
|
|
326
358
|
}
|
|
359
|
+
export interface FeatureItem {
|
|
360
|
+
key: string;
|
|
361
|
+
title: string;
|
|
362
|
+
description: string;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* `features: string[]` could never reach `SecurityHighlightsSection`, which
|
|
366
|
+
* needs `{key, title, description}` cards — so the renderer silently fell back
|
|
367
|
+
* to `DEFAULT_SECURITY_ITEMS` and the configured list was dead.
|
|
368
|
+
*
|
|
369
|
+
* `items` is the real shape. `features` is kept readable for back-compat with
|
|
370
|
+
* documents written before this change; when only `features` is present each
|
|
371
|
+
* string becomes a title-only card.
|
|
372
|
+
*/
|
|
327
373
|
export interface FeaturesSectionConfig {
|
|
328
374
|
title: string;
|
|
329
|
-
|
|
375
|
+
items?: FeatureItem[];
|
|
376
|
+
/** @deprecated Use `items`. Title-only cards. */
|
|
377
|
+
features?: string[];
|
|
378
|
+
/** Small eyebrow pill above the heading. */
|
|
379
|
+
pillLabel?: string;
|
|
380
|
+
/** "Learn more" link below the cards. */
|
|
381
|
+
learnMoreLabel?: string;
|
|
382
|
+
learnMoreLink?: string;
|
|
330
383
|
}
|
|
331
384
|
export type FAQCategoryKey = "general" | "orders_payment" | "shipping_delivery" | "returns_refunds" | "product_information" | "account_security" | "technical_support";
|
|
332
385
|
export interface FAQSectionConfig {
|
|
@@ -345,19 +398,28 @@ export interface FAQSectionConfig {
|
|
|
345
398
|
linkToFullPage: boolean;
|
|
346
399
|
/** Which FAQ categories to pull items from for this section. */
|
|
347
400
|
categories: FAQCategoryKey[];
|
|
401
|
+
/** Label for the "view all FAQs" link. Defaults to a generic one. */
|
|
402
|
+
viewMoreLabel?: string;
|
|
348
403
|
}
|
|
349
404
|
export interface BlogArticlesSectionConfig {
|
|
350
405
|
title: string;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
406
|
+
/** Was the literal `4`, so the cap could not be changed. */
|
|
407
|
+
maxArticles?: number;
|
|
408
|
+
showReadTime?: boolean;
|
|
409
|
+
showAuthor?: boolean;
|
|
410
|
+
showThumbnails?: boolean;
|
|
411
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
412
|
+
viewMoreLabel?: string;
|
|
355
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* `placeholder` and `buttonText` were REMOVED. The form itself is supplied by
|
|
416
|
+
* the consumer through `MarketplaceHomepageViewProps.newsletterFormSlot` — the
|
|
417
|
+
* section only renders the surrounding band — so appkit has no element to apply
|
|
418
|
+
* them to. They were controls that could never affect anything.
|
|
419
|
+
*/
|
|
356
420
|
export interface NewsletterSectionConfig {
|
|
357
421
|
title: string;
|
|
358
422
|
description: string;
|
|
359
|
-
placeholder: string;
|
|
360
|
-
buttonText: string;
|
|
361
423
|
privacyText: string;
|
|
362
424
|
privacyLink: string;
|
|
363
425
|
}
|
|
@@ -367,12 +429,12 @@ export interface StoresSectionConfig {
|
|
|
367
429
|
maxStores: number;
|
|
368
430
|
autoScroll: boolean;
|
|
369
431
|
scrollInterval: number;
|
|
432
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
433
|
+
viewMoreLabel?: string;
|
|
370
434
|
sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
|
|
371
435
|
filterByCategory?: string;
|
|
372
436
|
maxCount?: 5 | 10 | 20;
|
|
373
437
|
loop?: boolean;
|
|
374
|
-
/** Optional section background — reuses the carousel's background schema for consistency. */
|
|
375
|
-
background?: CarouselBackground;
|
|
376
438
|
}
|
|
377
439
|
export interface EventsSectionConfig {
|
|
378
440
|
title: string;
|
|
@@ -380,12 +442,12 @@ export interface EventsSectionConfig {
|
|
|
380
442
|
maxEvents: number;
|
|
381
443
|
autoScroll: boolean;
|
|
382
444
|
scrollInterval: number;
|
|
445
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
446
|
+
viewMoreLabel?: string;
|
|
383
447
|
sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
|
|
384
448
|
filterByCategory?: string;
|
|
385
449
|
maxCount?: 5 | 10 | 20;
|
|
386
450
|
loop?: boolean;
|
|
387
|
-
/** Optional section background — reuses the carousel's background schema for consistency. */
|
|
388
|
-
background?: CarouselBackground;
|
|
389
451
|
}
|
|
390
452
|
export type SocialPlatform = "instagram" | "facebook" | "tiktok" | "deviantart" | "youtube";
|
|
391
453
|
export type SocialPostType = "all" | "images" | "videos" | "reels";
|
|
@@ -446,23 +508,35 @@ export type SocialPost = {
|
|
|
446
508
|
export interface PreOrdersSectionConfig {
|
|
447
509
|
title: string;
|
|
448
510
|
subtitle?: string;
|
|
449
|
-
maxItems
|
|
450
|
-
rows
|
|
451
|
-
itemsPerRow
|
|
452
|
-
mobileItemsPerRow
|
|
511
|
+
maxItems?: number;
|
|
512
|
+
rows?: number;
|
|
513
|
+
itemsPerRow?: number;
|
|
514
|
+
mobileItemsPerRow?: number;
|
|
453
515
|
autoScroll: boolean;
|
|
454
516
|
scrollInterval: number;
|
|
517
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
518
|
+
viewMoreLabel?: string;
|
|
455
519
|
filterByBrand?: string;
|
|
456
520
|
sortBy?: "latest" | "oldest" | "priceLow" | "priceHigh" | "featured" | "onSale" | "popular";
|
|
457
521
|
filterByCategory?: string;
|
|
458
522
|
maxCount?: 5 | 10 | 20;
|
|
459
523
|
loop?: boolean;
|
|
460
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Four fields were REMOVED here:
|
|
527
|
+
*
|
|
528
|
+
* - `backgroundColor` / `gradient`. The band's copy is fixed inverse (white)
|
|
529
|
+
* ink, so an arbitrary admin-chosen fill has no safe outcome — that is
|
|
530
|
+
* Root Cause #67 exactly. `backgroundImage` survives because it renders
|
|
531
|
+
* under a scrim, which guarantees the contrast. The themed `accent-banner`
|
|
532
|
+
* tone already gives a brand gradient that follows the active theme.
|
|
533
|
+
* - `clickable` / `clickLink`. A link wrapping the whole banner would nest the
|
|
534
|
+
* two CTA buttons inside an anchor, which is an a11y violation and
|
|
535
|
+
* ambiguous besides. The buttons are the click target.
|
|
536
|
+
*/
|
|
461
537
|
export interface BannerSectionConfig {
|
|
462
538
|
height: "sm" | "md" | "lg" | "xl";
|
|
463
539
|
backgroundImage?: string;
|
|
464
|
-
backgroundColor?: string;
|
|
465
|
-
gradient?: string;
|
|
466
540
|
content: {
|
|
467
541
|
title: string;
|
|
468
542
|
subtitle?: string;
|
|
@@ -473,8 +547,6 @@ export interface BannerSectionConfig {
|
|
|
473
547
|
link: string;
|
|
474
548
|
variant: "primary" | "secondary" | "outline";
|
|
475
549
|
}>;
|
|
476
|
-
clickable: boolean;
|
|
477
|
-
clickLink?: string;
|
|
478
550
|
}
|
|
479
551
|
export type CustomCardsCard = {
|
|
480
552
|
id: string;
|
|
@@ -524,18 +596,22 @@ export interface CollectionCardsSectionConfig {
|
|
|
524
596
|
maxItems?: number;
|
|
525
597
|
showCollectionTabs?: boolean;
|
|
526
598
|
cta?: SectionCTA;
|
|
527
|
-
/** Optional section background — reuses the carousel's background schema for consistency. */
|
|
528
|
-
background?: CarouselBackground;
|
|
529
599
|
}
|
|
530
600
|
export declare const COLLECTION_CARDS_MAX_ENTRIES: 3;
|
|
601
|
+
/**
|
|
602
|
+
* `storeId`, `categorySlug` and `sortBy` were REMOVED. Bundles arrive
|
|
603
|
+
* pre-fetched from `listFeaturedBundles(8)` in `MarketplaceHomepageView`, which
|
|
604
|
+
* takes no scope or sort argument — honouring them means changing that fetch,
|
|
605
|
+
* and a per-section fetch would breach the SSR round-trip budget (Rule #6).
|
|
606
|
+
* `maxItems` survives because slicing an already-fetched list is real.
|
|
607
|
+
*/
|
|
531
608
|
export interface FeaturedBundlesSectionConfig {
|
|
532
609
|
title?: string;
|
|
533
610
|
subtitle?: string;
|
|
534
611
|
maxItems?: number;
|
|
535
|
-
storeId?: string;
|
|
536
|
-
categorySlug?: string;
|
|
537
|
-
sortBy?: "newest" | "savings-desc" | "price-asc";
|
|
538
612
|
showSavingsBadge?: boolean;
|
|
613
|
+
/** Label for the "view all" link. Defaults to a generic one. */
|
|
614
|
+
viewMoreLabel?: string;
|
|
539
615
|
}
|
|
540
616
|
export interface PrizeDrawsSectionConfig {
|
|
541
617
|
title?: string;
|
|
@@ -15,6 +15,15 @@ export interface MediaImageProps {
|
|
|
15
15
|
priority?: boolean;
|
|
16
16
|
/** Override browser loading strategy when needed (`eager` for LCP images). */
|
|
17
17
|
loading?: "lazy" | "eager";
|
|
18
|
+
/**
|
|
19
|
+
* Explicit `fetchpriority` hint. Defaults to what `priority` implies.
|
|
20
|
+
*
|
|
21
|
+
* Set `"low"` on eagerly-loaded images that are NOT the LCP candidate — a
|
|
22
|
+
* carousel's off-screen slides are the motivating case. All five hero banners
|
|
23
|
+
* otherwise race each other at the browser's default priority and delay the
|
|
24
|
+
* one slide the visitor can actually see.
|
|
25
|
+
*/
|
|
26
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
18
27
|
/** CSS object-fit applied to the underlying img element. Defaults to `'cover'`. */
|
|
19
28
|
objectFit?: "cover" | "contain";
|
|
20
29
|
/**
|
|
@@ -66,5 +75,5 @@ export interface MediaImageProps {
|
|
|
66
75
|
media?: string;
|
|
67
76
|
}>;
|
|
68
77
|
}
|
|
69
|
-
export declare function MediaImage({ src, alt, size, priority, loading, objectFit, fallback, className, rounded, shadow, style, sources, onError, }: MediaImageProps): import("react").JSX.Element;
|
|
78
|
+
export declare function MediaImage({ src, alt, size, priority, loading, fetchPriority, objectFit, fallback, className, rounded, shadow, style, sources, onError, }: MediaImageProps): import("react").JSX.Element;
|
|
70
79
|
export default MediaImage;
|
|
@@ -48,7 +48,7 @@ function resolveSrcSet(input) {
|
|
|
48
48
|
.filter(Boolean)
|
|
49
49
|
.join(", ");
|
|
50
50
|
}
|
|
51
|
-
export function MediaImage({ src, alt, size = "card", priority = false, loading, objectFit = "cover", fallback, className, rounded, shadow, style, sources, onError, }) {
|
|
51
|
+
export function MediaImage({ src, alt, size = "card", priority = false, loading, fetchPriority, objectFit = "cover", fallback, className, rounded, shadow, style, sources, onError, }) {
|
|
52
52
|
const [hasError, setHasError] = useState(false);
|
|
53
53
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
54
54
|
const [retryCount, setRetryCount] = useState(0);
|
|
@@ -98,15 +98,18 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
|
|
|
98
98
|
const requestSrc = retryCount > 0
|
|
99
99
|
? `${resolvedSrc}${resolvedSrc.includes("?") ? "&" : "?"}retry=${retryCount}`
|
|
100
100
|
: resolvedSrc;
|
|
101
|
+
// An explicit hint always wins; otherwise mirror what `priority` already
|
|
102
|
+
// implies, so existing call sites keep their current behaviour exactly.
|
|
103
|
+
const resolvedFetchPriority = fetchPriority ?? (priority ? "high" : undefined);
|
|
101
104
|
// Art-directed `<picture>` path — bypasses Next.js `<Image>` because the
|
|
102
105
|
// browser must pick the matching `<source>` itself. Loading + decoding hints
|
|
103
106
|
// come from the consumer's preset.
|
|
104
107
|
if (sources && sources.length > 0) {
|
|
105
|
-
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: requestSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), decoding: "async",
|
|
108
|
+
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: requestSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), fetchPriority: resolvedFetchPriority, decoding: "async",
|
|
106
109
|
// catalogued primitive for media (variant catalogue allows the inner
|
|
107
110
|
// <img> here). Object-fit + className come from typed props.
|
|
108
111
|
className: `absolute inset-0 w-full h-full ${fitClass}`, onLoad: () => setIsLoaded(true), onError: handleError })] })] }));
|
|
109
112
|
}
|
|
110
|
-
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: requestSrc, alt: alt, fill: true, priority: priority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: handleError, unoptimized: isSvg })] }));
|
|
113
|
+
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: requestSrc, alt: alt, fill: true, priority: priority, fetchPriority: resolvedFetchPriority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: handleError, unoptimized: isSvg })] }));
|
|
111
114
|
}
|
|
112
115
|
export default MediaImage;
|
|
@@ -18,6 +18,7 @@ import { BaseListingCard, Button, RichText, Row, Span, Stack, Text, TextLink } f
|
|
|
18
18
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
19
19
|
import { PreorderBadge } from "./PreorderCard";
|
|
20
20
|
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
|
|
21
|
+
import { CardStoreLine } from "../../products/components/CardStoreLine";
|
|
21
22
|
const DEFAULT_LABELS = {
|
|
22
23
|
preOrderBadge: "Pre-order",
|
|
23
24
|
featuredBadge: "Featured",
|
|
@@ -65,7 +66,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
65
66
|
onSelect(product.id, !isSelected);
|
|
66
67
|
}, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
|
|
67
68
|
/* ── Compact list layout ── */
|
|
68
|
-
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
69
|
+
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
69
70
|
/* ── Full grid layout ── */
|
|
70
|
-
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
71
|
+
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
71
72
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CardStoreLine — the "by <seller>" line on a listing card.
|
|
3
|
+
*
|
|
4
|
+
* One implementation because there are six card variants that need it and only
|
|
5
|
+
* ONE of them rendered it: `ProductCard` had this block inline while
|
|
6
|
+
* `ProductListRow`, `MarketplacePreorderCard`, `MarketplacePrizeDrawCard`,
|
|
7
|
+
* `PreorderCard` and `MarketplaceAuctionCard` showed no seller at all, even
|
|
8
|
+
* when the data was right there on the item. Six copies of a three-line block
|
|
9
|
+
* is six chances for the fallback rule and the muted tone to drift.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately NOT a link. A listing card is itself wrapped in a `<Link>` to the
|
|
12
|
+
* product, and nesting an anchor inside an anchor is invalid HTML that browsers
|
|
13
|
+
* resolve by splitting the outer link — which is how a card loses its own click
|
|
14
|
+
* target (the shape of Root Cause #43). The seller is reachable from the product
|
|
15
|
+
* detail page, which does link it.
|
|
16
|
+
*/
|
|
17
|
+
import React from "react";
|
|
18
|
+
export interface CardStoreLineProps {
|
|
19
|
+
storeName?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Present when the listing has a seller at all, whether or not its display
|
|
22
|
+
* name was denormalized onto the document.
|
|
23
|
+
*
|
|
24
|
+
* This is the distinction the old inline block could not make: it called
|
|
25
|
+
* `safeDisplayName(storeName, "")`, so a missing name produced `""` and the
|
|
26
|
+
* line rendered nothing — indistinguishable from "this listing has no store".
|
|
27
|
+
* With the id in hand, a listing that HAS a seller says so.
|
|
28
|
+
*/
|
|
29
|
+
storeId?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function CardStoreLine({ storeName, storeId, className }: CardStoreLineProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Text } from "../../../ui/components/Typography";
|
|
4
|
+
import { safeDisplayName } from "../../../security/pii-redact";
|
|
5
|
+
export function CardStoreLine({ storeName, storeId, className }) {
|
|
6
|
+
if (!storeId && !storeName)
|
|
7
|
+
return null;
|
|
8
|
+
// `safeDisplayName` also guards the encrypted-value case: a PII-ciphertext
|
|
9
|
+
// string reaching a card must render as a label, never as `enc:v1:…`.
|
|
10
|
+
// `storeId` IS the store slug in this codebase, so it is a readable last
|
|
11
|
+
// resort rather than an opaque key.
|
|
12
|
+
const seller = safeDisplayName(storeName, storeId ?? "Seller");
|
|
13
|
+
return (_jsxs(Text, { className: className ?? "mt-0.5 text-[11px]", color: "faint", children: ["by ", seller] }));
|
|
14
|
+
}
|
|
@@ -10,6 +10,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
|
10
10
|
const CLS_PRIZE_PILL = "inline-flex items-center rounded-full bg-fuchsia-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-white";
|
|
11
11
|
import { BaseListingCard, Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
12
12
|
import { MediaImage } from "../../media/MediaImage";
|
|
13
|
+
import { CardStoreLine } from "./CardStoreLine";
|
|
13
14
|
const DEFAULT_LABELS = {
|
|
14
15
|
prizeDrawBadge: "Prize draw",
|
|
15
16
|
pendingBadge: "Reveal pending",
|
|
@@ -98,5 +99,5 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
98
99
|
onSelect(product.id, !isSelected);
|
|
99
100
|
}, className: selectable || isSelected
|
|
100
101
|
? "opacity-100"
|
|
101
|
-
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "primary", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
102
|
+
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "primary", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
102
103
|
}
|
|
@@ -8,8 +8,8 @@ import { BaseListingCard, Button, Div, Grid, PriceDisplay, Row, Span, Stack, Tex
|
|
|
8
8
|
import { MediaImage } from "../../media/MediaImage";
|
|
9
9
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
10
10
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
11
|
-
import { safeDisplayName } from "../../../security";
|
|
12
11
|
import { useLongPress } from "../../../react/hooks/useLongPress";
|
|
12
|
+
import { CardStoreLine } from "./CardStoreLine";
|
|
13
13
|
import { FeatureBadgeList } from "./FeatureBadge";
|
|
14
14
|
import { useProductFeatures } from "./ProductFeaturesContext";
|
|
15
15
|
import { PRODUCT_FEATURE_CARD_MAX_VISIBLE } from "../constants/product-features.constants";
|
|
@@ -97,10 +97,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
97
97
|
},
|
|
98
98
|
}
|
|
99
99
|
: {}), children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] }));
|
|
100
|
-
})(), (() => {
|
|
101
|
-
const seller = safeDisplayName(product.storeName, "");
|
|
102
|
-
return seller ? (_jsxs(Text, { className: "mt-0.5 text-[11px]", color: "faint", children: ["by ", seller] })) : null;
|
|
103
|
-
})(), product.rating !== undefined && (_jsxs(Row, { className: "mt-1", gap: "xs", children: [_jsx(Span, { className: CLS_STAR, children: "\u2605" }), _jsxs(Span, { className: "text-[11px]", color: "muted", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto", padding: "t-xs", children: [_jsx(PriceDisplay, { amount: product.price, originalAmount: product.originalPrice, currency: getDefaultCurrency(), variant: "compact" }), (() => {
|
|
100
|
+
})(), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.rating !== undefined && (_jsxs(Row, { className: "mt-1", gap: "xs", children: [_jsx(Span, { className: CLS_STAR, children: "\u2605" }), _jsxs(Span, { className: "text-[11px]", color: "muted", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto", padding: "t-xs", children: [_jsx(PriceDisplay, { amount: product.price, originalAmount: product.originalPrice, currency: getDefaultCurrency(), variant: "compact" }), (() => {
|
|
104
101
|
const stock = product.stockCount ?? product.stockQuantity ?? product.availableQuantity;
|
|
105
102
|
if (stock === undefined || !listingPlugin.showsStockQuantity)
|
|
106
103
|
return null;
|
|
@@ -136,7 +133,7 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
|
136
133
|
onClick ? "cursor-pointer" : "",
|
|
137
134
|
]
|
|
138
135
|
.filter(Boolean)
|
|
139
|
-
.join(" "), children: [_jsx(Div, { className: `flex-shrink-0 w-16 h-16 sm:w-20 sm:h-20 ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "lg", children: product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "thumbnail" })) : (_jsx(Div, { className: "w-full h-full bg-neutral-200" })) }), _jsxs(Stack, { className: "flex-1 min-w-0 gap-[2px]", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }), (product.categoryName || product.brand) && (_jsx(Span, { className: "text-[11px] truncate", color: "faint", children: [product.categoryName, product.brand].filter(Boolean).join(" · ") })), _jsxs(Row, { className: "mt-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "sm", weight: "semibold", className: "text-primary", children: formatCurrency(product.price, getDefaultCurrency()) }), discount && (_jsxs(Span, { weight: "bold", className: CLS_DISCOUNT_TEXT_BARE, children: ["-", discount, "%"] })), product.rating !== undefined && (_jsxs(Span, { layout: "flex", gap: "2xs", className: "text-[11px]", color: "faint", children: [_jsx(Span, { className: CLS_STAR_BARE, children: "\u2605" }), product.rating.toFixed(1)] })), (() => {
|
|
136
|
+
.join(" "), children: [_jsx(Div, { className: `flex-shrink-0 w-16 h-16 sm:w-20 sm:h-20 ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "lg", children: product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "thumbnail" })) : (_jsx(Div, { className: "w-full h-full bg-neutral-200" })) }), _jsxs(Stack, { className: "flex-1 min-w-0 gap-[2px]", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }), (product.categoryName || product.brand) && (_jsx(Span, { className: "text-[11px] truncate", color: "faint", children: [product.categoryName, product.brand].filter(Boolean).join(" · ") })), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId, className: "text-[11px]" }), _jsxs(Row, { className: "mt-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "sm", weight: "semibold", className: "text-primary", children: formatCurrency(product.price, getDefaultCurrency()) }), discount && (_jsxs(Span, { weight: "bold", className: CLS_DISCOUNT_TEXT_BARE, children: ["-", discount, "%"] })), product.rating !== undefined && (_jsxs(Span, { layout: "flex", gap: "2xs", className: "text-[11px]", color: "faint", children: [_jsx(Span, { className: CLS_STAR_BARE, children: "\u2605" }), product.rating.toFixed(1)] })), (() => {
|
|
140
137
|
const stock = product.stockCount ?? product.stockQuantity ?? product.availableQuantity;
|
|
141
138
|
if (stock === undefined)
|
|
142
139
|
return null;
|
|
@@ -155,7 +155,24 @@ export interface ProductDocument extends BaseDocument {
|
|
|
155
155
|
video?: ProductVideoField;
|
|
156
156
|
status: ProductStatus;
|
|
157
157
|
storeId: string;
|
|
158
|
+
/**
|
|
159
|
+
* Denormalized seller display name — what every listing card renders as
|
|
160
|
+
* "by …", and the only place a card can get it: the public browse query
|
|
161
|
+
* never touches `storeRepository`.
|
|
162
|
+
*
|
|
163
|
+
* Optional for back-compat with documents written before the create paths
|
|
164
|
+
* populated it, NOT because it is optional in practice. `resolveStoreFields`
|
|
165
|
+
* fills it on every create, and a store rename re-syncs it through the
|
|
166
|
+
* `storeNameBackfill` job — that job is the mirror-guard, and removing it
|
|
167
|
+
* turns this into a field that silently goes stale (Root Cause #42).
|
|
168
|
+
*/
|
|
158
169
|
storeName?: string;
|
|
170
|
+
/**
|
|
171
|
+
* Denormalized `storeSlug`, so a card or a detail page can build a
|
|
172
|
+
* `/stores/{slug}` link without a second read. Written alongside `storeName`
|
|
173
|
+
* and re-synced by the same job.
|
|
174
|
+
*/
|
|
175
|
+
storeSlug?: string;
|
|
159
176
|
/** W1-34 — denormalized seller rating (0–5) shown on product detail store card. */
|
|
160
177
|
storeRating?: number;
|
|
161
178
|
/** W1-34 — denormalized seller total review count shown alongside the rating. */
|
|
@@ -98,6 +98,8 @@ export interface ProductItem {
|
|
|
98
98
|
brands?: string[];
|
|
99
99
|
storeId?: string;
|
|
100
100
|
storeName?: string;
|
|
101
|
+
/** Denormalized store slug — lets a card name its seller with no second read. */
|
|
102
|
+
storeSlug?: string;
|
|
101
103
|
specifications?: {
|
|
102
104
|
name: string;
|
|
103
105
|
value: string;
|
|
@@ -113,6 +113,23 @@ export declare function updatePayoutSettings(userId: string, input: UpdatePayout
|
|
|
113
113
|
}>;
|
|
114
114
|
export declare function requestPayout(userId: string, userName: string, userEmail: string, input: RequestPayoutInput): Promise<unknown>;
|
|
115
115
|
export declare function bulkSellerOrder(userId: string, userRole: string, userDisplayName: string, userEmail: string, orderIds: string[]): Promise<BulkSellerOrderResult>;
|
|
116
|
+
/**
|
|
117
|
+
* The real "create a listing" path — every seller `new` page under `/store`
|
|
118
|
+
* routes here.
|
|
119
|
+
*
|
|
120
|
+
* The store fields are resolved from the OWNER, never taken from the draft.
|
|
121
|
+
* `SellerProductDraft` has no `storeId`, `storeName` or `storeSlug` field at
|
|
122
|
+
* all, so this used to write `sellerId`/`sellerName`/`sellerEmail` and nothing
|
|
123
|
+
* else about the store — leaving every seller-created listing with no `storeId`
|
|
124
|
+
* whatsoever. That is not just a blank "by …" line on the card: `storeId` is
|
|
125
|
+
* what the store page queries on, what `splitCartIntoOrderGroups` groups on,
|
|
126
|
+
* and what a payout hangs off. Seed data set all three by hand, so a seeded
|
|
127
|
+
* environment looked entirely correct.
|
|
128
|
+
*
|
|
129
|
+
* Resolving server-side rather than accepting them from the client is also the
|
|
130
|
+
* only safe shape — a caller-supplied `storeId` would let a seller file a
|
|
131
|
+
* listing under someone else's store.
|
|
132
|
+
*/
|
|
116
133
|
export declare function createSellerProduct(userId: string, userName: string, userEmail: string, input: Record<string, JsonValue>): Promise<void>;
|
|
117
134
|
export declare function getSellerStore(userId: string): Promise<StoreDocument | null>;
|
|
118
135
|
export declare function getSellerShipping(userId: string): Promise<import("../../auth").SellerShippingConfig | null>;
|