@mohasinac/appkit 4.9.5 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +3 -2
  2. package/dist/_internal/server/features/auctions/service.d.ts +4 -3
  3. package/dist/_internal/server/features/auctions/service.js +6 -6
  4. package/dist/_internal/server/features/bundles/data.d.ts +9 -0
  5. package/dist/_internal/server/features/bundles/data.js +14 -0
  6. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  7. package/dist/_internal/server/features/bundles/index.js +1 -1
  8. package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
  9. package/dist/_internal/server/features/catalogue/og.js +27 -0
  10. package/dist/_internal/server/features/grouped/data.d.ts +12 -0
  11. package/dist/_internal/server/features/grouped/data.js +26 -1
  12. package/dist/_internal/server/features/grouped/index.d.ts +1 -1
  13. package/dist/_internal/server/features/grouped/index.js +1 -1
  14. package/dist/_internal/server/features/products/data.d.ts +23 -0
  15. package/dist/_internal/server/features/products/data.js +159 -0
  16. package/dist/_internal/server/features/products/index.d.ts +1 -1
  17. package/dist/_internal/server/features/products/index.js +1 -1
  18. package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
  19. package/dist/_internal/shared/features/auctions/config.js +31 -1
  20. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
  21. package/dist/_internal/shared/listing-types/art/config.js +1 -1
  22. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
  23. package/dist/_internal/shared/listing-types/classified/config.js +1 -1
  24. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
  25. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  26. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
  27. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  28. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
  29. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  30. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
  31. package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
  32. package/dist/client.d.ts +0 -6
  33. package/dist/client.js +1 -4
  34. package/dist/constants/api-endpoints.d.ts +3 -0
  35. package/dist/constants/api-endpoints.js +1 -0
  36. package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
  37. package/dist/features/admin/schemas/firestore.d.ts +9 -0
  38. package/dist/features/admin/schemas/firestore.js +6 -0
  39. package/dist/features/auctions/actions/bid-actions.js +4 -1
  40. package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
  41. package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
  42. package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
  43. package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
  44. package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
  45. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
  46. package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
  47. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
  48. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
  49. package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
  50. package/dist/features/catalogue/index.d.ts +2 -0
  51. package/dist/features/catalogue/index.js +1 -0
  52. package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
  53. package/dist/features/categories/components/BundleDetailView.js +3 -2
  54. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
  55. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
  56. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
  57. package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
  58. package/dist/features/contact/components/ContactPageView.js +8 -2
  59. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
  60. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
  61. package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
  62. package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
  63. package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
  64. package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
  65. package/dist/features/layout/BottomActions.js +4 -3
  66. package/dist/features/layout/BottomActionsContext.d.ts +10 -0
  67. package/dist/features/layout/BottomActionsContext.js +5 -1
  68. package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
  69. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  70. package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
  71. package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
  72. package/dist/features/media/MediaVideo.js +17 -1
  73. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
  74. package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
  75. package/dist/features/products/components/ProductDetailPageView.js +11 -116
  76. package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
  77. package/dist/features/products/components/RelatedItemsSection.js +17 -0
  78. package/dist/features/seller/components/SellerProductShell.js +1 -1
  79. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +83 -3
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.js +1 -0
  82. package/dist/next/routing/route-map.d.ts +2 -0
  83. package/dist/next/routing/route-map.js +7 -0
  84. package/dist/seed/bids-seed-data.js +48 -3
  85. package/dist/seed/grouped-listings-seed-data.js +167 -0
  86. package/dist/seed/payouts-seed-data.js +4 -4
  87. package/dist/seed/products-art-seed-data.js +188 -5
  88. package/dist/seed/products-auctions-seed-data.js +215 -6
  89. package/dist/seed/products-classifieds-seed-data.js +255 -8
  90. package/dist/seed/products-digital-codes-seed-data.js +258 -19
  91. package/dist/seed/products-live-items-seed-data.js +143 -10
  92. package/dist/seed/products-preorders-seed-data.js +213 -8
  93. package/dist/seed/products-prize-draws-seed-data.js +198 -0
  94. package/dist/seed/products-standard-seed-data.js +141 -0
  95. package/dist/seed/products-stickers-seed-data.js +178 -5
  96. package/dist/seed/site-settings-seed-data.js +10 -29
  97. package/dist/seed/store-extensions-seed-data.js +1 -1
  98. package/dist/server-entry.d.ts +4 -1
  99. package/dist/server-entry.js +4 -1
  100. package/dist/server.d.ts +4 -2
  101. package/dist/server.js +4 -2
  102. package/dist/styles.css +29 -1
  103. package/dist/tailwind-utilities.css +1 -1
  104. package/dist/ui/components/ImageLightbox.js +37 -3
  105. package/dist/ui/components/ImageLightbox.style.css +28 -0
  106. package/dist/utils/media-url.d.ts +9 -0
  107. package/dist/utils/media-url.js +39 -0
  108. package/package.json +1 -1
@@ -79,6 +79,14 @@ export interface BottomActionsState {
79
79
  * Useful for contextual data like "Current bid: ₹1,200" on auction pages.
80
80
  */
81
81
  infoLabel?: string;
82
+ /**
83
+ * Optional single-line label rendered as its own row ABOVE `infoLabel`
84
+ * (i.e. topmost of the three stacked rows: secondaryLabel, infoLabel,
85
+ * action row). Purely additive — no existing caller sets this, so every
86
+ * bar that doesn't pass it renders identically to before. Introduced for
87
+ * the auction detail page's live countdown ("Ends in 2d 5h").
88
+ */
89
+ secondaryLabel?: string;
82
90
  }
83
91
  interface BottomActionsContextValue {
84
92
  state: BottomActionsState;
@@ -97,6 +105,8 @@ interface BottomActionsContextValue {
97
105
  bulkClearRef: React.MutableRefObject<(() => void) | undefined>;
98
106
  /** Set or clear the contextual info label. */
99
107
  setInfoLabel: (label: string | undefined) => void;
108
+ /** Set or clear the secondary label (rendered above infoLabel). */
109
+ setSecondaryLabel: (label: string | undefined) => void;
100
110
  /** Clear all state (called on feature unmount). */
101
111
  clearAll: () => void;
102
112
  }
@@ -84,6 +84,9 @@ export function BottomActionsProvider({ children, }) {
84
84
  const setInfoLabel = useCallback((infoLabel) => {
85
85
  setState((prev) => ({ ...prev, infoLabel }));
86
86
  }, []);
87
+ const setSecondaryLabel = useCallback((secondaryLabel) => {
88
+ setState((prev) => ({ ...prev, secondaryLabel }));
89
+ }, []);
87
90
  const clearAll = useCallback(() => {
88
91
  actionCallbacksRef.current = new Map();
89
92
  bulkCallbacksRef.current = new Map();
@@ -98,8 +101,9 @@ export function BottomActionsProvider({ children, }) {
98
101
  bulkCallbacksRef,
99
102
  bulkClearRef,
100
103
  setInfoLabel,
104
+ setSecondaryLabel,
101
105
  clearAll,
102
- }), [state, setActions, setBulkConfig, setInfoLabel, clearAll]);
106
+ }), [state, setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll]);
103
107
  return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
104
108
  }
105
109
  // --- Hook ---------------------------------------------------------------------
@@ -3,5 +3,7 @@ export interface UseBottomActionsOptions {
3
3
  actions?: BottomAction[];
4
4
  bulk?: BottomBulkConfig;
5
5
  infoLabel?: string;
6
+ /** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
7
+ secondaryLabel?: string;
6
8
  }
7
9
  export declare function useBottomActions(options?: UseBottomActionsOptions): void;
@@ -2,7 +2,7 @@
2
2
  import { useEffect, useRef } from "react";
3
3
  import { useBottomActionsContext, } from "../BottomActionsContext";
4
4
  export function useBottomActions(options = {}) {
5
- const { setActions, setBulkConfig, setInfoLabel, clearAll } = useBottomActionsContext();
5
+ const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll } = useBottomActionsContext();
6
6
  const actionsRef = useRef(options.actions ?? []);
7
7
  actionsRef.current = options.actions ?? [];
8
8
  const bulkRef = useRef(options.bulk);
@@ -22,6 +22,9 @@ export function useBottomActions(options = {}) {
22
22
  useEffect(() => {
23
23
  setInfoLabel(options.infoLabel);
24
24
  }, [setInfoLabel, options.infoLabel]);
25
+ useEffect(() => {
26
+ setSecondaryLabel(options.secondaryLabel);
27
+ }, [setSecondaryLabel, options.secondaryLabel]);
25
28
  useEffect(() => {
26
29
  return () => clearAll();
27
30
  }, [clearAll]);
@@ -0,0 +1,9 @@
1
+ import type { ProductDocument } from "../../products/schemas/firestore";
2
+ export interface LiveItemDetailPageViewProps {
3
+ slug: string;
4
+ /** Pre-fetched product document — dedupes with generateMetadata() via React.cache(). */
5
+ initialProduct?: ProductDocument | null;
6
+ /** Render-prop for the add-to-cart CTA — wired by the page shim (carries the vendor-verification gate). */
7
+ renderActions?: (product: ProductDocument) => React.ReactNode;
8
+ }
9
+ export declare function LiveItemDetailPageView({ slug, initialProduct, renderActions }: LiveItemDetailPageViewProps): Promise<import("react").JSX.Element>;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import Link from "next/link";
3
+ import { getLiveItemForDetail } from "../../../_internal/server/features/live/data";
4
+ const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
5
+ const CLS_LIVE_ITEM_BADGE = "inline-block rounded-full bg-lime-100 dark:bg-lime-900/30 px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-lime-700 dark:text-lime-300";
6
+ const CLS_WARN_BOX = "rounded-lg border border-warning bg-warning-surface p-[var(--appkit-space-4)] text-[length:var(--appkit-text-sm)]";
7
+ const CLS_WARN_TITLE = "font-medium text-warning";
8
+ const CLS_WARN_BODY = "mt-1 text-warning";
9
+ import { ROUTES } from "../../../next";
10
+ import { getDefaultCurrency } from "../../../core/baseline-resolver";
11
+ import { formatCurrency } from "../../../utils/number.formatter";
12
+ import { normalizeRichTextHtml } from "../../../utils/string.formatter";
13
+ import { safeDisplayName } from "../../../security";
14
+ import { Div, Container, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Dl, Dt, Dd, } from "../../../ui";
15
+ import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
16
+ import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
17
+ import { ProductTabsShell } from "../../products/components/ProductTabsShell";
18
+ import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
19
+ import { ShareButton } from "../../products/components/ShareButton";
20
+ import { HistoryTracker } from "../../history/components/HistoryTracker";
21
+ import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
22
+ import { computeRelatedItems, getReviewItemsForProduct } from "../../../_internal/server/features/products/data";
23
+ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
24
+ import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
25
+ import { ReviewsList } from "../../reviews/components/ReviewsList";
26
+ function toDescriptionHtml(raw) {
27
+ if (!raw)
28
+ return "";
29
+ const s = typeof raw === "string" ? raw : JSON.stringify(raw);
30
+ return normalizeRichTextHtml(s);
31
+ }
32
+ export async function LiveItemDetailPageView({ slug, initialProduct, renderActions }) {
33
+ const product = initialProduct !== undefined
34
+ ? (initialProduct ?? undefined)
35
+ : await getLiveItemForDetail(slug).catch(() => undefined);
36
+ if (!product) {
37
+ return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Live Listing Not Found" }), _jsx(Text, { color: "muted", children: "The live listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Live Listings" })] }) }) }) }));
38
+ }
39
+ const p = product;
40
+ const currency = p.currency || getDefaultCurrency();
41
+ const title = String(p.title ?? p.name ?? "Live Listing");
42
+ const price = typeof p.price === "number" ? p.price : null;
43
+ const images = Array.isArray(p.images) ? p.images : typeof p.mainImage === "string" ? [p.mainImage] : [];
44
+ const productVideo = p.video;
45
+ const meta = product.liveItem;
46
+ const jurisdictions = meta?.jurisdictionAllowed ?? [];
47
+ const transport = meta?.transport;
48
+ const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? String(p.categorySlugs[0]) : (typeof p.category === "string" ? p.category : null);
49
+ const categoryName = Array.isArray(p.categoryNames) && p.categoryNames.length > 0 ? String(p.categoryNames[0]) : (typeof p.categoryName === "string" ? p.categoryName : null);
50
+ const brand = typeof p.brand === "string" ? p.brand : undefined;
51
+ const storeName = typeof p.storeName === "string" ? p.storeName : null;
52
+ const safeSeller = storeName ? safeDisplayName(storeName, "") : null;
53
+ const storeSlug = (typeof p.storeSlug === "string" ? p.storeSlug : null) || (typeof p.storeId === "string" ? p.storeId : null);
54
+ const storeHref = storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null;
55
+ const specs = Array.isArray(p.specifications) ? p.specifications : [];
56
+ const customSections = Array.isArray(p.customSections) ? p.customSections : [];
57
+ const descriptionHtml = toDescriptionHtml(p.description);
58
+ const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, reviews] = await Promise.all([
59
+ computeRelatedItems(product),
60
+ getGroupsWithItemsForProduct(product.id),
61
+ getReviewItemsForProduct(product.id),
62
+ ]);
63
+ return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
64
+ title,
65
+ thumb: images[0],
66
+ price: price ?? undefined,
67
+ storeId: typeof p.storeId === "string" ? p.storeId : undefined,
68
+ storeName: storeName ?? undefined,
69
+ } }), _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.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), 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(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_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_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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: "Sold 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" }))] }) }))] })), 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, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
70
+ id: s.id,
71
+ label: s.title,
72
+ content: _jsx(CustomSectionTabContent, { section: s }),
73
+ })) })), renderBuyBar: () => renderActions?.(product), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
74
+ }
@@ -2,13 +2,14 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useEffect } from "react";
4
4
  import { Div, Row, Span } from "../../ui";
5
- import { resolveMediaUrl } from "../../utils/media-url";
5
+ import { resolveMediaUrl, getYouTubeVideoId } from "../../utils/media-url";
6
6
  import { useSiteSettings } from "../../core/hooks/useSiteSettings";
7
7
  export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true, autoPlayMuted = false, loop = false, trimStart, trimEnd, objectFit = "cover", watermark, }) {
8
8
  const videoRef = useRef(null);
9
9
  const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
10
10
  const resolvedSrc = resolveMediaUrl(src);
11
11
  const resolvedPoster = resolveMediaUrl(thumbnailUrl);
12
+ const youtubeId = getYouTubeVideoId(src);
12
13
  // Apply trimStart on load
13
14
  useEffect(() => {
14
15
  const el = videoRef.current;
@@ -46,6 +47,21 @@ export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true,
46
47
  // audit-color-pair-function-ok: deliberate faded placeholder-icon shade, not a readability regression
47
48
  className: "absolute inset-0 text-zinc-400 text-[length:var(--appkit-text-4xl)]", surface: "subtle", align: "center", justify: "center", role: "img", "aria-label": alt, children: _jsx(Span, { "aria-hidden": "true", children: "\uD83C\uDFAC" }) }));
48
49
  }
50
+ // A YouTube watch/share URL (MediaUploadField's "YouTube" tab writes one)
51
+ // is never a raw playable file — <video src> fails with "no supported
52
+ // format" for it. Render the iframe embed instead of the native player.
53
+ if (youtubeId) {
54
+ const embedParams = new URLSearchParams({ playsinline: "1", rel: "0" });
55
+ if (autoPlayMuted) {
56
+ embedParams.set("autoplay", "1");
57
+ embedParams.set("mute", "1");
58
+ }
59
+ if (loop) {
60
+ embedParams.set("loop", "1");
61
+ embedParams.set("playlist", youtubeId);
62
+ }
63
+ return (_jsx("iframe", { src: `https://www.youtube-nocookie.com/embed/${youtubeId}?${embedParams.toString()}`, title: alt, allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true, className: "absolute inset-0 w-full h-full border-0" }));
64
+ }
49
65
  return (_jsxs(_Fragment, { children: [_jsx("video", { ref: videoRef, src: resolvedSrc, poster: resolvedPoster, controls: controls, autoPlay: autoPlayMuted, muted: autoPlayMuted, loop: loop, playsInline: true, "aria-label": alt, className: `absolute inset-0 w-full h-full ${fitClass}` }), effectiveWatermark ? _jsx(MediaVideoWatermarkLayer, { config: effectiveWatermark }) : null] }));
50
66
  }
51
67
  /**
@@ -30,6 +30,10 @@ import { ShareButton } from "../../products/components/ShareButton";
30
30
  import { SublistingCarouselSection } from "../../products/components/SublistingCarouselSection";
31
31
  import { HistoryTracker } from "../../history/components/HistoryTracker";
32
32
  import { ShowGroupSection } from "../../products/components/ShowGroupSection";
33
+ import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
34
+ import { computeRelatedItems } from "../../../_internal/server/features/products/data";
35
+ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
36
+ import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
33
37
  function toDescriptionHtml(raw) {
34
38
  if (!raw)
35
39
  return "";
@@ -141,6 +145,10 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
141
145
  const groupId = typeof p.groupId === "string" ? p.groupId : null;
142
146
  const isGroupParent = p.isGroupParent === true;
143
147
  const groupTitle = typeof p.groupTitle === "string" ? p.groupTitle : undefined;
148
+ const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups] = await Promise.all([
149
+ computeRelatedItems(product),
150
+ getGroupsWithItemsForProduct(product.id),
151
+ ]);
144
152
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "preorder", snapshot: {
145
153
  title: typeof p.title === "string" ? p.title : undefined,
146
154
  thumb: typeof p.mainImage === "string"
@@ -159,5 +167,5 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
159
167
  id: s.id,
160
168
  label: s.title,
161
169
  content: _jsx(CustomSectionTabContent, { section: s }),
162
- })) })), renderBuyBar: () => (_jsx(PreOrderBuyBarPanel, { reserveTarget: reserveTarget, reservedCount: reservedCount, progressPct: progressPct, productId: String(product.id), price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) }), _jsx(PreOrderBottomActions, { price: price, currency: currency })] })] }));
170
+ })) })), renderBuyBar: () => (_jsx(PreOrderBuyBarPanel, { reserveTarget: reserveTarget, reservedCount: reservedCount, progressPct: progressPct, productId: String(product.id), price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), _jsx(PreOrderBottomActions, { price: price, currency: currency })] })] }));
163
171
  }
@@ -18,6 +18,10 @@ import { ShareButton } from "./ShareButton";
18
18
  import { PrizeDrawCollage } from "./PrizeDrawCollage";
19
19
  import { PrizeDrawEntryActions } from "./PrizeDrawEntryActions";
20
20
  import { HistoryTracker } from "../../history/components/HistoryTracker";
21
+ import { RelatedItemsSection } from "./RelatedItemsSection";
22
+ import { computeRelatedItems } from "../../../_internal/server/features/products/data";
23
+ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
24
+ import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
21
25
  function toDescriptionHtml(raw) {
22
26
  if (!raw)
23
27
  return "";
@@ -112,11 +116,17 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
112
116
  ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug))
113
117
  : null;
114
118
  const descriptionHtml = toDescriptionHtml(p.description);
119
+ const categoryName = Array.isArray(p.categoryNames) && p.categoryNames.length > 0 ? String(p.categoryNames[0]) : undefined;
120
+ const brand = typeof p.brand === "string" ? p.brand : undefined;
121
+ const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups] = await Promise.all([
122
+ computeRelatedItems(product),
123
+ getGroupsWithItemsForProduct(product.id),
124
+ ]);
115
125
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "prize-draw", snapshot: {
116
126
  title,
117
127
  thumb,
118
128
  price: pricePerEntry,
119
129
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
120
130
  storeName: storeName ?? undefined,
121
- } }), _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", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 transition-colors", children: "Prize Draws" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_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_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), 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: "Sold 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" }))] }) }))] })), 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 })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, thumb: thumb, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl, storeId: typeof p.storeId === "string" ? p.storeId : undefined, storeName: storeName ?? undefined }) })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
131
+ } }), _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", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 transition-colors", children: "Prize Draws" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_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_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), 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: "Sold 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" }))] }) }))] })), 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 })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, thumb: thumb, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl, storeId: typeof p.storeId === "string" ? p.storeId : undefined, storeName: storeName ?? undefined }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
122
132
  }
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
- import { SIEVE_OP, sieveAnd, sieveFilter, sortBy } from "@mohasinac/appkit";
4
3
  import { productRepository, reviewRepository } from "../../../repositories";
5
4
  const __P = {
6
5
  p3: "p-[var(--appkit-space-3)]",
@@ -32,59 +31,18 @@ import { ProductGalleryClient } from "./ProductGalleryClient";
32
31
  import { ProductTabsShell } from "./ProductTabsShell";
33
32
  import { ProductFeatureBadges } from "./ProductFeatureBadges";
34
33
  import { FeatureBadgeList } from "./FeatureBadge";
35
- import { RelatedProductsCarousel } from "./RelatedProductsCarousel";
36
34
  import { ShareButton } from "./ShareButton";
37
35
  import { CustomSectionTabContent } from "./CustomSectionTabContent";
38
36
  import { SublistingCarouselSection } from "./SublistingCarouselSection";
39
37
  import { ShowGroupSection } from "./ShowGroupSection";
40
38
  import { HistoryTracker } from "../../history/components/HistoryTracker";
39
+ import { RelatedItemsSection } from "./RelatedItemsSection";
40
+ import { computeRelatedItems, toReview } from "../../../_internal/server/features/products/data";
41
+ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
42
+ import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
41
43
  // ---------------------------------------------------------------------------
42
44
  // Helpers
43
45
  // ---------------------------------------------------------------------------
44
- function toProductItem(doc) {
45
- return {
46
- id: String(doc.id ?? ""),
47
- title: String(doc.title ?? doc.name ?? ""),
48
- price: typeof doc.price === "number" ? doc.price : 0,
49
- originalPrice: typeof doc.originalPrice === "number" ? doc.originalPrice : undefined,
50
- mainImage: Array.isArray(doc.images)
51
- ? doc.images[0]
52
- : typeof doc.mainImage === "string"
53
- ? doc.mainImage
54
- : undefined,
55
- status: doc.status ?? "published",
56
- slug: typeof doc.slug === "string" ? doc.slug : undefined,
57
- storeName: typeof doc.storeName === "string" ? doc.storeName : undefined,
58
- rating: typeof doc.rating === "number" ? doc.rating : undefined,
59
- reviewCount: typeof doc.reviewCount === "number" ? doc.reviewCount : undefined,
60
- };
61
- }
62
- function toReview(doc) {
63
- const images = Array.isArray(doc.images) ? doc.images : undefined;
64
- const createdAt = doc.createdAt instanceof Date
65
- ? doc.createdAt.toISOString()
66
- : typeof doc.createdAt === "string"
67
- ? doc.createdAt
68
- : undefined;
69
- const rawRating = typeof doc.rating === "number" ? doc.rating : 3;
70
- const rating = Math.min(5, Math.max(1, Math.round(rawRating)));
71
- return {
72
- id: String(doc.id ?? ""),
73
- productId: String(doc.productId ?? ""),
74
- userId: String(doc.userId ?? ""),
75
- userName: String(doc.userName ?? "Anonymous"),
76
- userAvatar: typeof doc.userAvatar === "string" ? doc.userAvatar : undefined,
77
- rating,
78
- title: typeof doc.title === "string" ? doc.title : undefined,
79
- comment: typeof doc.comment === "string" ? doc.comment : undefined,
80
- images,
81
- status: doc.status ?? "approved",
82
- helpfulCount: typeof doc.helpfulCount === "number" ? doc.helpfulCount : undefined,
83
- verified: doc.verified === true,
84
- featured: doc.featured === true,
85
- createdAt,
86
- };
87
- }
88
46
  function toDescriptionHtml(raw) {
89
47
  if (!raw)
90
48
  return "";
@@ -206,74 +164,14 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
206
164
  const isGroupParent = p.isGroupParent === true;
207
165
  const groupTitle = typeof p.groupTitle === "string" ? p.groupTitle : undefined;
208
166
  const storeId = typeof p.storeId === "string" ? p.storeId : null;
209
- // -- Fetch reviews + 4 related-listing signals in parallel ------------------
210
- const [reviewDocs, sameCategoryDocs, sameBrandDocs, tagOverlapDocs, sameStoreDocs] = await Promise.all([
211
- reviewRepository
212
- .findApprovedByProduct(product.id)
213
- .catch(() => []),
214
- primaryCategorySlug
215
- ? productRepository.findByCategory(primaryCategorySlug).catch(() => [])
216
- : Promise.resolve([]),
217
- brand
218
- ? productRepository
219
- .list({
220
- filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("brand", SIEVE_OP.EQ, brand)),
221
- sorts: sortBy("createdAt", "DESC"),
222
- page: 1,
223
- pageSize: 9,
224
- })
225
- .then((r) => r.items)
226
- .catch(() => [])
227
- : Promise.resolve([]),
228
- tags.length > 0
229
- ? productRepository.findByTagsOverlap(tags, 9).catch(() => [])
230
- : Promise.resolve([]),
231
- storeId
232
- ? productRepository
233
- .list({
234
- filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("storeId", SIEVE_OP.EQ, storeId)),
235
- sorts: sortBy("createdAt", "DESC"),
236
- page: 1,
237
- pageSize: 9,
238
- })
239
- .then((r) => r.items)
240
- .catch(() => [])
241
- : Promise.resolve([]),
167
+ // -- Fetch reviews + the shared 4-signal related-items computation + grouped listings in parallel --
168
+ const [reviewDocs, related, groups] = await Promise.all([
169
+ reviewRepository.findApprovedByProduct(product.id).catch(() => []),
170
+ computeRelatedItems(product),
171
+ getGroupsWithItemsForProduct(product.id),
242
172
  ]);
243
173
  const reviews = reviewDocs.map(toReview);
244
- const _now = new Date();
245
- const currentProductId = product.id;
246
- function toRelatedItems(docs) {
247
- return docs
248
- .filter((r) => {
249
- if (r.id === currentProductId)
250
- return false;
251
- const s = r.status;
252
- if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
253
- return false;
254
- if (r.isSold === true)
255
- return false;
256
- if (r.availableQuantity === 0)
257
- return false;
258
- if (r.listingType === "auction" && r.auctionEndDate) {
259
- const end = r.auctionEndDate;
260
- const endDate = typeof end.toDate === "function"
261
- ? end.toDate()
262
- : end instanceof Date ? end : new Date(String(end));
263
- if (endDate <= _now)
264
- return false;
265
- }
266
- if (r.listingType === "prize-draw" && r.prizeRevealStatus === "closed")
267
- return false;
268
- return true;
269
- })
270
- .slice(0, 8)
271
- .map(toProductItem);
272
- }
273
- const relatedItems = toRelatedItems(sameCategoryDocs);
274
- const relatedByBrand = toRelatedItems(sameBrandDocs);
275
- const relatedByTags = toRelatedItems(tagOverlapDocs);
276
- const relatedByStore = toRelatedItems(sameStoreDocs);
174
+ const { relatedItems, relatedByBrand, relatedByTags, relatedByStore } = related;
277
175
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: product.id, productType: "product", snapshot: {
278
176
  title: product.title,
279
177
  thumb: product.mainImage ?? product.images?.[0],
@@ -339,10 +237,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
339
237
  content: _jsx(CustomSectionTabContent, { section: s }),
340
238
  })) })), renderBundleSection: bundleMemberships.length > 0
341
239
  ? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
342
- : undefined, renderRelated: () => relatedItems.length > 0 ||
343
- relatedByBrand.length > 0 ||
344
- relatedByTags.length > 0 ||
345
- relatedByStore.length > 0 ? (_jsxs(Stack, { gap: "xl", children: [relatedItems.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedItems, title: categoryName || category ? `More in ${categoryName || category}` : "More in this category" })), relatedByBrand.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByBrand, title: brand ? `More by ${brand}` : "More by this brand" })), relatedByTags.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByTags, title: "You might also like" })), relatedByStore.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByStore, title: storeName ? `More from ${storeName}` : "More from this seller" }))] })) : null }), renderPrimaryActions
240
+ : undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
346
241
  ? renderPrimaryActions({
347
242
  productId: product.id,
348
243
  productSlug: product.slug ?? slug,
@@ -0,0 +1,16 @@
1
+ import type { ProductItem } from "../types";
2
+ export interface RelatedItemsSectionProps {
3
+ relatedItems: ProductItem[];
4
+ relatedByBrand: ProductItem[];
5
+ relatedByTags: ProductItem[];
6
+ relatedByStore: ProductItem[];
7
+ categoryLabel?: string;
8
+ brandLabel?: string;
9
+ storeLabel?: string;
10
+ }
11
+ /**
12
+ * The same-shape "More in category / More by brand / You might also like /
13
+ * More from store" carousel stack, shared by every listing-type detail page
14
+ * that surfaces computeRelatedItems() results.
15
+ */
16
+ export declare function RelatedItemsSection({ relatedItems, relatedByBrand, relatedByTags, relatedByStore, categoryLabel, brandLabel, storeLabel, }: RelatedItemsSectionProps): import("react").JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Stack } from "../../../ui";
3
+ import { RelatedProductsCarousel } from "./RelatedProductsCarousel";
4
+ /**
5
+ * The same-shape "More in category / More by brand / You might also like /
6
+ * More from store" carousel stack, shared by every listing-type detail page
7
+ * that surfaces computeRelatedItems() results.
8
+ */
9
+ export function RelatedItemsSection({ relatedItems, relatedByBrand, relatedByTags, relatedByStore, categoryLabel, brandLabel, storeLabel, }) {
10
+ if (relatedItems.length === 0 &&
11
+ relatedByBrand.length === 0 &&
12
+ relatedByTags.length === 0 &&
13
+ relatedByStore.length === 0) {
14
+ return null;
15
+ }
16
+ return (_jsxs(Stack, { gap: "xl", children: [relatedItems.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedItems, title: categoryLabel ? `More in ${categoryLabel}` : "More in this category" })), relatedByBrand.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByBrand, title: brandLabel ? `More by ${brandLabel}` : "More by this brand" })), relatedByTags.length > 0 && _jsx(RelatedProductsCarousel, { items: relatedByTags, title: "You might also like" }), relatedByStore.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByStore, title: storeLabel ? `More from ${storeLabel}` : "More from this seller" }))] }));
17
+ }
@@ -87,7 +87,7 @@ function StepMedia({ values, onChange, storeSlug = "store", }) {
87
87
  }
88
88
  // ── Step 3: Auction Settings ───────────────────────────────────────────────
89
89
  function StepAuctionSettings({ values, onChange, }) {
90
- return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "Prices are in Indian Rupees (\u20B9). Enter the rupee value \u2014 no paise." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: "Starting Bid (\u20B9)", type: "number", value: toRupeesString(values.startingBid), onChange: (v) => onChange({ startingBid: fromRupeesString(v) }), placeholder: "100" }), _jsx(FormField, { name: "auctionEndDate", label: "Auction End Date & Time", type: "datetime-local", value: values.auctionEndDate ?? "", onChange: (v) => onChange({ auctionEndDate: v }) })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "reservePrice", label: "Reserve Price (\u20B9, optional)", type: "number", value: toRupeesString(values.reservePrice), onChange: (v) => onChange({ reservePrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Minimum price below which you won't sell" }), _jsx(FormField, { name: "buyNowPrice", label: "Buy Now Price (\u20B9, optional)", type: "number", value: toRupeesString(values.buyNowPrice), onChange: (v) => onChange({ buyNowPrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Allows instant purchase before auction ends" })] }), _jsx(FormField, { name: "minBidIncrement", label: "Minimum Bid Increment (\u20B9)", type: "number", value: toRupeesString(values.minBidIncrement), onChange: (v) => onChange({ minBidIncrement: fromRupeesString(v) }), placeholder: "50" }), _jsx(FormField, { name: "auctionShippingPaidBy", label: "Shipping paid by", type: "select", value: values.auctionShippingPaidBy ?? "winner", onChange: (v) => onChange({ auctionShippingPaidBy: v }), options: [
90
+ return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "Prices are in Indian Rupees (\u20B9). Enter the rupee value \u2014 no paise." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: "Starting Bid (\u20B9)", type: "number", value: toRupeesString(values.startingBid), onChange: (v) => onChange({ startingBid: fromRupeesString(v) }), placeholder: "100" }), _jsx(FormField, { name: "auctionEndDate", label: "Auction End Date & Time", type: "datetime-local", value: values.auctionEndDate ?? "", onChange: (v) => onChange({ auctionEndDate: v }) })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "reservePrice", label: "Reserve Price (\u20B9, optional)", type: "number", value: toRupeesString(values.reservePrice), onChange: (v) => onChange({ reservePrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Minimum price below which you won't sell" }), _jsx(FormField, { name: "buyNowPrice", label: "Buy Now Price (\u20B9, optional)", type: "number", value: toRupeesString(values.buyNowPrice), onChange: (v) => onChange({ buyNowPrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Allows instant purchase before auction ends" })] }), _jsx(FormField, { name: "minBidIncrement", label: "Minimum Bid Increment (\u20B9)", type: "number", value: toRupeesString(values.minBidIncrement), onChange: (v) => onChange({ minBidIncrement: fromRupeesString(v) }), placeholder: "50", helpText: "Optional \u2014 can only raise the platform's tiered minimum for this listing, never lower it. Leave blank to use the tier as-is." }), _jsx(FormField, { name: "auctionShippingPaidBy", label: "Shipping paid by", type: "select", value: values.auctionShippingPaidBy ?? "winner", onChange: (v) => onChange({ auctionShippingPaidBy: v }), options: [
91
91
  { value: "winner", label: "Winner pays shipping" },
92
92
  { value: "seller", label: "Seller includes shipping" },
93
93
  ] }), _jsx(Toggle, { checked: !!values.autoExtendable, onChange: (checked) => onChange({ autoExtendable: checked }), label: "Auto-extend auction if bid placed in final minutes" }), values.autoExtendable && (_jsx(FormField, { name: "auctionExtensionMinutes", label: "Extension period (minutes)", type: "number", value: String(values.auctionExtensionMinutes ?? 5), onChange: (v) => onChange({ auctionExtensionMinutes: Number(v) || 5 }), placeholder: "5" }))] }));