@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.
- package/dist/_internal/server/features/auctions/actions.js +3 -2
- package/dist/_internal/server/features/auctions/service.d.ts +4 -3
- package/dist/_internal/server/features/auctions/service.js +6 -6
- package/dist/_internal/server/features/bundles/data.d.ts +9 -0
- package/dist/_internal/server/features/bundles/data.js +14 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
- package/dist/_internal/server/features/catalogue/og.js +27 -0
- package/dist/_internal/server/features/grouped/data.d.ts +12 -0
- package/dist/_internal/server/features/grouped/data.js +26 -1
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/products/data.d.ts +23 -0
- package/dist/_internal/server/features/products/data.js +159 -0
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
- package/dist/_internal/shared/features/auctions/config.js +31 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/art/config.js +1 -1
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/classified/config.js +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +1 -1
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
- package/dist/client.d.ts +0 -6
- package/dist/client.js +1 -4
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
- package/dist/features/admin/schemas/firestore.d.ts +9 -0
- package/dist/features/admin/schemas/firestore.js +6 -0
- package/dist/features/auctions/actions/bid-actions.js +4 -1
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
- package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
- package/dist/features/catalogue/index.d.ts +2 -0
- package/dist/features/catalogue/index.js +1 -0
- package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
- package/dist/features/categories/components/BundleDetailView.js +3 -2
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
- package/dist/features/contact/components/ContactPageView.js +8 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
- package/dist/features/layout/BottomActions.js +4 -3
- package/dist/features/layout/BottomActionsContext.d.ts +10 -0
- package/dist/features/layout/BottomActionsContext.js +5 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
- package/dist/features/media/MediaVideo.js +17 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
- package/dist/features/products/components/ProductDetailPageView.js +11 -116
- package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
- package/dist/features/products/components/RelatedItemsSection.js +17 -0
- package/dist/features/seller/components/SellerProductShell.js +1 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +83 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +7 -0
- package/dist/seed/bids-seed-data.js +48 -3
- package/dist/seed/grouped-listings-seed-data.js +167 -0
- package/dist/seed/payouts-seed-data.js +4 -4
- package/dist/seed/products-art-seed-data.js +188 -5
- package/dist/seed/products-auctions-seed-data.js +215 -6
- package/dist/seed/products-classifieds-seed-data.js +255 -8
- package/dist/seed/products-digital-codes-seed-data.js +258 -19
- package/dist/seed/products-live-items-seed-data.js +143 -10
- package/dist/seed/products-preorders-seed-data.js +213 -8
- package/dist/seed/products-prize-draws-seed-data.js +198 -0
- package/dist/seed/products-standard-seed-data.js +141 -0
- package/dist/seed/products-stickers-seed-data.js +178 -5
- package/dist/seed/site-settings-seed-data.js +10 -29
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +4 -1
- package/dist/server-entry.js +4 -1
- package/dist/server.d.ts +4 -2
- package/dist/server.js +4 -2
- package/dist/styles.css +29 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/ImageLightbox.js +37 -3
- package/dist/ui/components/ImageLightbox.style.css +28 -0
- package/dist/utils/media-url.d.ts +9 -0
- package/dist/utils/media-url.js +39 -0
- 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-
|
|
210
|
-
const [reviewDocs,
|
|
211
|
-
reviewRepository
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
|
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" }))] }));
|