@mohasinac/appkit 4.32.5 → 4.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/products/actions.js +29 -3
- package/dist/_internal/server/features/products/list-public.d.ts +1 -1
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/stores/denormalize.d.ts +44 -0
- package/dist/_internal/server/features/stores/denormalize.js +58 -0
- package/dist/_internal/server/features/stores/index.d.ts +1 -0
- package/dist/_internal/server/features/stores/index.js +1 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/storeNameBackfill.d.ts +27 -0
- package/dist/_internal/server/jobs/core/storeNameBackfill.js +90 -0
- package/dist/_internal/shared/features/products/to-product-item.js +7 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -1
- package/dist/features/admin/actions/admin-actions.js +11 -1
- package/dist/features/admin/components/AdminSectionsView.js +17 -20
- package/dist/features/admin/components/sections/adminSectionsBuildParse.js +31 -14
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +37 -7
- package/dist/features/admin/components/sections/adminSectionsTypes.js +11 -7
- package/dist/features/auctions/components/AuctionDetailPageView.js +56 -36
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +9 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -2
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +7 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +3 -3
- package/dist/features/cart/repository/cart.repository.d.ts +27 -6
- package/dist/features/cart/repository/cart.repository.js +52 -21
- package/dist/features/cart/utils/guest-cart.js +5 -0
- package/dist/features/cart/utils/pending-ops.d.ts +15 -0
- package/dist/features/cart/utils/pending-ops.js +24 -0
- package/dist/features/events/components/EventRafflesSection.js +2 -1
- package/dist/features/homepage/components/BlogArticlesSection.d.ts +7 -1
- package/dist/features/homepage/components/BlogArticlesSection.js +5 -3
- package/dist/features/homepage/components/CTABannerSection.d.ts +18 -1
- package/dist/features/homepage/components/CTABannerSection.js +22 -4
- package/dist/features/homepage/components/CustomerReviewsSection.d.ts +7 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
- package/dist/features/homepage/components/FAQSection.js +2 -11
- package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +5 -3
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +7 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +6 -5
- package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedProductsSection.js +5 -3
- package/dist/features/homepage/components/HeroCarousel.d.ts +23 -2
- package/dist/features/homepage/components/HeroCarousel.js +22 -9
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +6 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +11 -3
- package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +9 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +7 -4
- package/dist/features/homepage/components/StatsCounterSection.d.ts +6 -1
- package/dist/features/homepage/components/StatsCounterSection.js +3 -3
- package/dist/features/homepage/constants/faq-category-labels.d.ts +15 -0
- package/dist/features/homepage/constants/faq-category-labels.js +23 -0
- package/dist/features/homepage/constants/section-copy.d.ts +107 -0
- package/dist/features/homepage/constants/section-copy.js +96 -0
- package/dist/features/homepage/lib/section-renderer.d.ts +16 -1
- package/dist/features/homepage/lib/section-renderer.js +58 -37
- package/dist/features/homepage/repository/homepage-sections.repository.d.ts +23 -0
- package/dist/features/homepage/repository/homepage-sections.repository.js +40 -0
- package/dist/features/homepage/schemas/firestore.d.ts +120 -44
- package/dist/features/media/MediaImage.d.ts +10 -1
- package/dist/features/media/MediaImage.js +6 -3
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/products/components/CardStoreLine.d.ts +32 -0
- package/dist/features/products/components/CardStoreLine.js +14 -0
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
- package/dist/features/products/components/ProductGrid.js +3 -6
- package/dist/features/products/schemas/firestore.d.ts +17 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/actions/seller-actions.d.ts +17 -0
- package/dist/features/seller/actions/seller-actions.js +54 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +3 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +119 -0
- package/dist/seed/carousel-slides-seed-data.js +154 -22
- package/dist/seed/carousels-seed-data.js +4 -1
- package/dist/seed/homepage-sections-seed-data.js +337 -254
- package/dist/seed/products-preorders-seed-data.d.ts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link";
|
|
3
|
-
import { Div, Heading, Row, Section, Span, Text } from "../../../ui";
|
|
3
|
+
import { Div, Heading, Row, Scrim, Section, Span, Text } from "../../../ui";
|
|
4
|
+
import { MediaImage } from "../../media/MediaImage";
|
|
4
5
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import { SECTION_TITLE, SECTION_COPY } from "../constants/section-copy";
|
|
7
|
+
/**
|
|
8
|
+
* `height` maps onto the existing spacing scale rather than emitting a
|
|
9
|
+
* `min-h-[300px]`-style utility.
|
|
10
|
+
*
|
|
11
|
+
* Two reasons. A class generated only inside appkit and absent from the
|
|
12
|
+
* consumer's `src/**` is purged from the production CSS (Root Cause #3), and
|
|
13
|
+
* `BANNER_HEIGHTS`' raw px values would be exactly that. And padding keeps the
|
|
14
|
+
* banner sized by its content, so a long headline grows the box instead of
|
|
15
|
+
* overflowing a fixed height.
|
|
16
|
+
*/
|
|
17
|
+
const HEIGHT_PADDING_Y = {
|
|
18
|
+
sm: "y-2xl",
|
|
19
|
+
md: "y-4xl",
|
|
20
|
+
lg: "y-5xl",
|
|
21
|
+
xl: "y-6xl",
|
|
22
|
+
};
|
|
23
|
+
export function CTABannerSection({ title = SECTION_TITLE.banner, subtitle, description, primaryLabel = SECTION_COPY.bannerPrimaryCta, primaryHref = String(ROUTES.PUBLIC.PRODUCTS), secondaryLabel = SECTION_COPY.bannerSecondaryCta, secondaryHref = String(ROUTES.PUBLIC.AUCTIONS), height = "md", backgroundImage, className = "", }) {
|
|
24
|
+
return (_jsxs(Section, { tone: "accent-banner", className: `relative ${className}`, overflow: "hidden", paddingY: HEIGHT_PADDING_Y[height], paddingX: "x-md", children: [backgroundImage && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: backgroundImage, alt: "", size: "banner", objectFit: "cover" }) }), _jsx(Scrim, { direction: "left-to-right", intensity: "strong", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative mx-auto max-w-3xl text-left", children: [_jsx(Span, { className: "mb-4 inline-block", size: "2xl", "aria-hidden": "true", children: "\u2728" }), _jsx(Heading, { color: "inverse", level: 2, mdSize: "3xl", lgSize: "4xl", size: "2xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { color: "inverse", className: "mt-3 /80", size: "base", children: subtitle })), description && (_jsx(Text, { color: "inverse", className: "mt-2 /70 max-w-2xl", size: "sm", children: description })), _jsxs(Row, { className: "mt-8", align: "center", justify: "start", gap: "md", wrap: true, children: [_jsx(Link, { href: primaryHref, className: "inline-flex items-center rounded-full bg-white px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-primary-700 shadow-sm transition-all hover:bg-surface-hover hover:shadow-md focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: primaryLabel }), secondaryHref && (_jsx(Link, { href: secondaryHref, className: "inline-flex items-center rounded-full border-2 border-white/80 px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-white transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: secondaryLabel }))] })] })] }));
|
|
7
25
|
}
|
|
@@ -12,5 +12,11 @@ export interface CustomerReviewsSectionProps<T = unknown> {
|
|
|
12
12
|
autoScroll?: boolean;
|
|
13
13
|
scrollInterval?: number;
|
|
14
14
|
loop?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Responsive cards-per-view. Defaults to the shared `reviews` preset; the
|
|
17
|
+
* homepage section passes an admin-configured override built from its
|
|
18
|
+
* `itemsPerView` / `mobileItemsPerView` config.
|
|
19
|
+
*/
|
|
20
|
+
perView?: Record<string, number>;
|
|
15
21
|
}
|
|
16
|
-
export declare function CustomerReviewsSection<T = unknown>({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel, isLoading, keyExtractor, className, autoScroll, scrollInterval, loop, }: CustomerReviewsSectionProps<T>): React.JSX.Element | null;
|
|
22
|
+
export declare function CustomerReviewsSection<T = unknown>({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel, isLoading, keyExtractor, className, autoScroll, scrollInterval, loop, perView, }: CustomerReviewsSectionProps<T>): React.JSX.Element | null;
|
|
@@ -10,12 +10,12 @@ const __O = {
|
|
|
10
10
|
hidden: "overflow-hidden",
|
|
11
11
|
};
|
|
12
12
|
// --- Section -----------------------------------------------------------------
|
|
13
|
-
export function CustomerReviewsSection({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel = "See all reviews", isLoading = false, keyExtractor, className = "", autoScroll = true, scrollInterval = 4500, loop = true, }) {
|
|
13
|
+
export function CustomerReviewsSection({ title, subtitle, items, renderItem, viewMoreHref, viewMoreLabel = "See all reviews", isLoading = false, keyExtractor, className = "", autoScroll = true, scrollInterval = 4500, loop = true, perView, }) {
|
|
14
14
|
const themed = { textPrimary: THEMED_TEXT_PRIMARY, textSecondary: THEMED_TEXT_SECONDARY };
|
|
15
15
|
if (isLoading) {
|
|
16
16
|
return (_jsx(Section, { className: `${__P.p8} ${className}`, surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", children: [_jsx(Div, { className: `h-8 ${skeleton.base} mb-8 max-w-xs mx-auto` }), _jsx(Div, { layout: "flex", gap: "6", className: `${__O.hidden}`, children: [...Array(3)].map((_, i) => (_jsx(Div, { className: `${skeleton.card} h-[clamp(180px,26vh,260px)] min-w-[clamp(150px,18vw,260px)] max-w-[clamp(240px,36vw,380px)] flex-none` }, i))) })] }) }));
|
|
17
17
|
}
|
|
18
18
|
if (items.length === 0)
|
|
19
19
|
return null;
|
|
20
|
-
return (_jsx(Section, { className: `${className}`, surface: "muted", padding: "y-3xl", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto md:px-[2rem]", padding: "x-md", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, className: `${themed.textPrimary} mb-3`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `${themed.textSecondary}`, size: "base", children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: CAROUSEL_PER_VIEW.reviews, gap: 24, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: loop }), viewMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: viewMoreHref, className: "text-primary hover:text-primary/80", size: "sm", weight: "medium", children: viewMoreLabel }) }))] }) }));
|
|
20
|
+
return (_jsx(Section, { className: `${className}`, surface: "muted", padding: "y-3xl", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto md:px-[2rem]", padding: "x-md", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, className: `${themed.textPrimary} mb-3`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `${themed.textSecondary}`, size: "base", children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: perView ?? CAROUSEL_PER_VIEW.reviews, gap: 24, autoScroll: autoScroll, autoScrollInterval: scrollInterval, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: loop }), viewMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: viewMoreHref, className: "text-primary hover:text-primary/80", size: "sm", weight: "medium", children: viewMoreLabel }) }))] }) }));
|
|
21
21
|
}
|
|
@@ -4,19 +4,10 @@ import { useState, useMemo } from "react";
|
|
|
4
4
|
import { THEMED_BG_PRIMARY, THEMED_BG_TERTIARY, THEMED_BORDER, THEMED_TEXT_PRIMARY, THEMED_TEXT_SECONDARY, FLEX_BETWEEN } from "../../../_internal/shared/styles/themed";
|
|
5
5
|
import { Button, Div, Heading, RichText, Section, Span, Stack, Tabs, TabsList, TabsTrigger, Text, TextLink } from "../../../ui";
|
|
6
6
|
import { ChevronDown } from "lucide-react";
|
|
7
|
+
import { FAQ_CATEGORY_LABELS } from "../constants/faq-category-labels";
|
|
7
8
|
const __O = {
|
|
8
9
|
hidden: "overflow-hidden",
|
|
9
10
|
};
|
|
10
|
-
// --- Constants ----------------------------------------------------------------
|
|
11
|
-
const CATEGORY_LABELS = {
|
|
12
|
-
general: "General",
|
|
13
|
-
orders_payment: "Orders & Payment",
|
|
14
|
-
shipping_delivery: "Shipping",
|
|
15
|
-
returns_refunds: "Returns & Refunds",
|
|
16
|
-
product_information: "Products",
|
|
17
|
-
account_security: "Account",
|
|
18
|
-
technical_support: "Support",
|
|
19
|
-
};
|
|
20
11
|
// --- Helpers -----------------------------------------------------------------
|
|
21
12
|
function isHtml(s) {
|
|
22
13
|
return /<[a-z][\s\S]*>/i.test(s);
|
|
@@ -37,7 +28,7 @@ export function FAQSection({ title, subtitle, items, tabs: tabsProp, showCategor
|
|
|
37
28
|
const cat = item.category ?? "general";
|
|
38
29
|
if (!seen.has(cat)) {
|
|
39
30
|
seen.add(cat);
|
|
40
|
-
result.push({ value: cat, label:
|
|
31
|
+
result.push({ value: cat, label: FAQ_CATEGORY_LABELS[cat] ?? cat });
|
|
41
32
|
}
|
|
42
33
|
}
|
|
43
34
|
return result;
|
|
@@ -9,8 +9,10 @@ export interface FeaturedAuctionsSectionProps {
|
|
|
9
9
|
filterByBrand?: string;
|
|
10
10
|
initialItems?: ProductItem[];
|
|
11
11
|
rows?: number;
|
|
12
|
+
/** Cap the rendered cards, mirroring FeaturedProductsSection. */
|
|
13
|
+
maxItems?: number;
|
|
12
14
|
autoScroll?: boolean;
|
|
13
15
|
scrollInterval?: number;
|
|
14
16
|
loop?: boolean;
|
|
15
17
|
}
|
|
16
|
-
export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, loop, }: FeaturedAuctionsSectionProps): React.JSX.Element;
|
|
18
|
+
export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, autoScroll, scrollInterval, loop, }: FeaturedAuctionsSectionProps): React.JSX.Element;
|
|
@@ -4,7 +4,9 @@ import { SectionCarousel } from "./SectionCarousel";
|
|
|
4
4
|
import { useFeaturedAuctions } from "../hooks/useFeaturedAuctions";
|
|
5
5
|
import { MarketplaceAuctionCard } from "../../auctions/components/MarketplaceAuctionCard";
|
|
6
6
|
import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
|
|
8
|
+
export function FeaturedAuctionsSection({ title = SECTION_TITLE.auctions, description, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.auctions, className = "", filterByBrand, initialItems, rows = 1, maxItems, autoScroll = false, scrollInterval = 5000, loop = true, }) {
|
|
9
|
+
const { data: fetched = [], isLoading } = useFeaturedAuctions({ filterByBrand, initialData: initialItems });
|
|
10
|
+
const items = maxItems && maxItems > 0 ? fetched.slice(0, maxItems) : fetched;
|
|
11
|
+
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: SECTION_TITLE.auctions, headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
|
|
10
12
|
}
|
|
@@ -12,9 +12,15 @@ export interface FeaturedBundlesSectionProps {
|
|
|
12
12
|
title?: string;
|
|
13
13
|
description?: string;
|
|
14
14
|
initialItems?: CategoryDocument[];
|
|
15
|
+
/** Cap the rendered bundles. Items are supplied pre-fetched, so this slices. */
|
|
16
|
+
maxItems?: number;
|
|
17
|
+
/** Show the "N% off" badge on each cover. Defaults to `true`. */
|
|
18
|
+
showSavingsBadge?: boolean;
|
|
19
|
+
/** "View all" link label. */
|
|
20
|
+
viewMoreLabel?: string;
|
|
15
21
|
className?: string;
|
|
16
22
|
onBuyNow?: (input: {
|
|
17
23
|
bundleSlug: string;
|
|
18
24
|
}) => Promise<unknown>;
|
|
19
25
|
}
|
|
20
|
-
export declare function FeaturedBundlesSection({ title, description, initialItems, className, onBuyNow, }: FeaturedBundlesSectionProps): React.JSX.Element | null;
|
|
26
|
+
export declare function FeaturedBundlesSection({ title, description, initialItems, maxItems, showSavingsBadge, viewMoreLabel, className, onBuyNow, }: FeaturedBundlesSectionProps): React.JSX.Element | null;
|
|
@@ -24,20 +24,21 @@ const __O = {
|
|
|
24
24
|
hidden: "overflow-hidden",
|
|
25
25
|
};
|
|
26
26
|
const PLACEHOLDER_EMOJI = "📦";
|
|
27
|
-
export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", onBuyNow, }) {
|
|
27
|
+
export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], maxItems, showSavingsBadge = true, viewMoreLabel = BUNDLE_COPY.featured.viewAll, className = "", onBuyNow, }) {
|
|
28
28
|
const { hand } = useHandMode();
|
|
29
|
-
const
|
|
29
|
+
const bundles = initialItems.filter((c) => c.categoryType === "bundle");
|
|
30
|
+
const items = maxItems && maxItems > 0 ? bundles.slice(0, maxItems) : bundles;
|
|
30
31
|
if (items.length === 0)
|
|
31
32
|
return null;
|
|
32
|
-
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children:
|
|
33
|
+
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: viewMoreLabel })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, showSavingsBadge: showSavingsBadge, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
|
|
33
34
|
}
|
|
34
|
-
function FeaturedBundleCard({ bundle, onBuyNow }) {
|
|
35
|
+
function FeaturedBundleCard({ bundle, showSavingsBadge = true, onBuyNow, }) {
|
|
35
36
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
36
37
|
const memberCount = bundle.bundleProductIds?.length ?? 0;
|
|
37
38
|
const cover = bundle.display?.coverImage;
|
|
38
39
|
const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
|
|
39
40
|
const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
|
|
40
|
-
return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePrice
|
|
41
|
+
return (_jsxs(Stack, { gap: "none", surface: "default", rounded: "xl", border: "default", children: [_jsxs(Link, { href: href, className: "group block p-[var(--appkit-space-3)] hover:no-underline", children: [_jsxs(Div, { className: `relative mb-2 aspect-square ${__O.hidden}`, rounded: "lg", surface: "subtle", children: [cover ? (_jsx(MediaImage, { src: cover, alt: bundle.name, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", className: "h-full w-full", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), showSavingsBadge && discount && (_jsx(Badge, { variant: "success", className: "absolute left-2 top-2", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }))] }), _jsx(Text, { className: "line-clamp-2", size: "sm", weight: "semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", wrap: true, children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePrice
|
|
41
42
|
? formatCurrency(bundle.bundlePrice, "INR")
|
|
42
43
|
: BUNDLE_COPY.featured.priceFallback }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, "INR") })), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }), onBuyNow && (_jsx(Div, { border: "subtle", className: `border-t ${__P.p3}`, padding: "t-xs", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
|
|
43
44
|
}
|
|
@@ -9,8 +9,10 @@ export interface FeaturedPreOrdersSectionProps {
|
|
|
9
9
|
filterByBrand?: string;
|
|
10
10
|
initialItems?: ProductItem[];
|
|
11
11
|
rows?: number;
|
|
12
|
+
/** Cap the rendered cards, mirroring FeaturedProductsSection. */
|
|
13
|
+
maxItems?: number;
|
|
12
14
|
autoScroll?: boolean;
|
|
13
15
|
scrollInterval?: number;
|
|
14
16
|
loop?: boolean;
|
|
15
17
|
}
|
|
16
|
-
export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, loop, }: FeaturedPreOrdersSectionProps): React.JSX.Element;
|
|
18
|
+
export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, autoScroll, scrollInterval, loop, }: FeaturedPreOrdersSectionProps): React.JSX.Element;
|
|
@@ -4,7 +4,9 @@ import { SectionCarousel } from "./SectionCarousel";
|
|
|
4
4
|
import { useFeaturedPreOrders } from "../hooks/useFeaturedPreOrders";
|
|
5
5
|
import { MarketplacePreorderCard } from "../../pre-orders/components/MarketplacePreorderCard";
|
|
6
6
|
import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
|
|
8
|
+
export function FeaturedPreOrdersSection({ title = SECTION_TITLE.preOrders, description, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.preOrders, className = "", filterByBrand, initialItems, rows = 1, maxItems, autoScroll = false, scrollInterval = 5000, loop = true, }) {
|
|
9
|
+
const { data: fetched = [], isLoading } = useFeaturedPreOrders({ filterByBrand, initialData: initialItems });
|
|
10
|
+
const items = maxItems && maxItems > 0 ? fetched.slice(0, maxItems) : fetched;
|
|
9
11
|
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
|
|
10
12
|
}
|
|
@@ -8,9 +8,11 @@ export interface FeaturedProductsSectionProps {
|
|
|
8
8
|
filterByBrand?: string;
|
|
9
9
|
initialItems?: ProductItem[];
|
|
10
10
|
rows?: number;
|
|
11
|
+
/** Cap the rendered cards. Was declared but never destructured — so every
|
|
12
|
+
* `maxItems` an admin configured was silently ignored. */
|
|
11
13
|
maxItems?: number;
|
|
12
14
|
autoScroll?: boolean;
|
|
13
15
|
scrollInterval?: number;
|
|
14
16
|
loop?: boolean;
|
|
15
17
|
}
|
|
16
|
-
export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, loop, }: FeaturedProductsSectionProps): import("react").JSX.Element;
|
|
18
|
+
export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, autoScroll, scrollInterval, loop, }: FeaturedProductsSectionProps): import("react").JSX.Element;
|
|
@@ -5,13 +5,15 @@ import { useFeaturedProducts } from "../hooks/useFeaturedProducts";
|
|
|
5
5
|
import { InteractiveProductCard } from "../../products/components/InteractiveProductCard";
|
|
6
6
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
7
7
|
import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
|
|
8
|
-
|
|
8
|
+
import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
|
|
9
|
+
export function FeaturedProductsSection({ title = SECTION_TITLE.products, description, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.products, className = "", filterByBrand, initialItems, rows = 1, maxItems, autoScroll = false, scrollInterval = 5000, loop = true, }) {
|
|
9
10
|
const { data, isLoading } = useFeaturedProducts({
|
|
10
11
|
filterByBrand,
|
|
11
12
|
initialData: initialItems?.length
|
|
12
13
|
? { items: initialItems, total: initialItems.length, page: 1, pageSize: initialItems.length, totalPages: 1, hasMore: false }
|
|
13
14
|
: undefined,
|
|
14
15
|
});
|
|
15
|
-
const
|
|
16
|
-
|
|
16
|
+
const fetched = data?.items ?? [];
|
|
17
|
+
const items = maxItems && maxItems > 0 ? fetched.slice(0, maxItems) : fetched;
|
|
18
|
+
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: SECTION_TITLE.products, headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: CAROUSEL_PER_VIEW.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, loop: loop, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(InteractiveProductCard, { product: product, href: pluginFor(product.listingType ?? "standard").detailRoute(product.slug ?? product.id ?? "") })), className: className }));
|
|
17
19
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { CarouselSlide } from "../types/index";
|
|
2
|
+
import type { CarouselSlide, CarouselSlideHeight } from "../types/index";
|
|
3
3
|
export interface HeroCarouselProps {
|
|
4
4
|
initialSlides?: CarouselSlide[];
|
|
5
5
|
/**
|
|
@@ -8,5 +8,26 @@ export interface HeroCarouselProps {
|
|
|
8
8
|
* Falls back to `window.location.href` assignment when omitted.
|
|
9
9
|
*/
|
|
10
10
|
push?: (href: string) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Everything below comes from `CarouselSectionConfig`, which the section
|
|
13
|
+
* renderer used to drop entirely — it rendered `<HeroCarousel initialSlides />`
|
|
14
|
+
* and nothing else, so every one of these admin controls was inert.
|
|
15
|
+
*/
|
|
16
|
+
/** Fallback slide height when a slide carries no `settings.height`. */
|
|
17
|
+
height?: CarouselSlideHeight;
|
|
18
|
+
/** Fallback autoplay delay when a slide carries no `settings.autoplayDelayMs`. */
|
|
19
|
+
defaultAutoplayDelayMs?: number;
|
|
20
|
+
/** Pause autoplay while the pointer is over the carousel. Defaults to `true`. */
|
|
21
|
+
pauseOnHover?: boolean;
|
|
22
|
+
/** Render the slide-position dots. Defaults to `true`. */
|
|
23
|
+
showDots?: boolean;
|
|
24
|
+
/** Render the prev/next arrows. Defaults to `true`. */
|
|
25
|
+
showArrows?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Accessible name for the carousel region. Deliberately NOT rendered as a
|
|
28
|
+
* visible heading — the config field is a label for the admin list, and
|
|
29
|
+
* printing it above the slides would add hero chrome nobody asked for.
|
|
30
|
+
*/
|
|
31
|
+
title?: string;
|
|
11
32
|
}
|
|
12
|
-
export declare function HeroCarousel({ initialSlides, push }?: HeroCarouselProps): React.JSX.Element;
|
|
33
|
+
export declare function HeroCarousel({ initialSlides, push, height, defaultAutoplayDelayMs, pauseOnHover, showDots, showArrows, title, }?: HeroCarouselProps): React.JSX.Element;
|
|
@@ -86,7 +86,7 @@ function SlideBackground({ bg, legacy, mobileLegacy, isMobile, priority, }) {
|
|
|
86
86
|
if (effectiveBg.type === "video") {
|
|
87
87
|
return (_jsxs(_Fragment, { children: [_jsx(MediaVideo, { src: src, thumbnailUrl: effectiveBg.thumbnail ?? legacy?.thumbnail, alt: "", autoPlayMuted: true, loop: true, controls: false }), effectiveBg.dimOverlay?.enabled && (_jsx(Div, { className: POSITION_FILL, style: { backgroundColor: `rgba(0,0,0,${(effectiveBg.dimOverlay.opacity ?? 0) / 100})` } }))] }));
|
|
88
88
|
}
|
|
89
|
-
return (_jsxs(_Fragment, { children: [_jsx(MediaImage, { src: src, alt: "", size: "hero", priority: priority }), effectiveBg.dimOverlay?.enabled && (_jsx(Div, { className: POSITION_FILL, style: { backgroundColor: `rgba(0,0,0,${(effectiveBg.dimOverlay.opacity ?? 0) / 100})` } }))] }));
|
|
89
|
+
return (_jsxs(_Fragment, { children: [_jsx(MediaImage, { src: src, alt: "", size: "hero", priority: priority, fetchPriority: priority ? "high" : "low" }), effectiveBg.dimOverlay?.enabled && (_jsx(Div, { className: POSITION_FILL, style: { backgroundColor: `rgba(0,0,0,${(effectiveBg.dimOverlay.opacity ?? 0) / 100})` } }))] }));
|
|
90
90
|
}
|
|
91
91
|
/** Renders the background of a card (image, video, color, gradient). */
|
|
92
92
|
function CardBackground({ bg }) {
|
|
@@ -117,14 +117,17 @@ function CarouselCardRenderer({ card, isMobile, push, }) {
|
|
|
117
117
|
gridRow: String(gridPos.gridRow),
|
|
118
118
|
gridColumn: String(gridPos.gridColumn),
|
|
119
119
|
minHeight: isMobile ? 100 : 140,
|
|
120
|
-
}, children: [_jsx(CardBackground, { bg: card.background }), !card.isButtonOnly && (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "bottom-up", intensity: "strong", className: POSITION_FILL }), _jsxs(Stack, { justify: "end", className: `${POSITION_FILL} md:p-[1.5rem] ${textAlignClass}`, padding: "xs", children: [card.content?.eyebrow && (_jsx(Text, { color: "inverse", shadow: "sm", className: "text-[10px] !/70 mb-0.5 tracking-wider", mdSize: "xs", transform: "uppercase", children: card.content.eyebrow })), card.content?.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "hidden md:block !/90 mb-0.5 md:mb-2", mdSize: "sm", size: "xs", children: card.content.subtitle })), card.content?.title && (
|
|
120
|
+
}, children: [_jsx(CardBackground, { bg: card.background }), !card.isButtonOnly && (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "bottom-up", intensity: "strong", className: POSITION_FILL }), _jsxs(Stack, { justify: "end", className: `${POSITION_FILL} md:p-[1.5rem] ${textAlignClass}`, padding: "xs", children: [card.content?.eyebrow && (_jsx(Text, { color: "inverse", shadow: "sm", className: "text-[10px] !/70 mb-0.5 tracking-wider", mdSize: "xs", transform: "uppercase", children: card.content.eyebrow })), card.content?.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "hidden md:block !/90 mb-0.5 md:mb-2", mdSize: "sm", size: "xs", children: card.content.subtitle })), card.content?.title && (
|
|
121
|
+
// level={3}: a card sits INSIDE a slide, so it is subordinate to
|
|
122
|
+
// that slide's own h2 overlay title rather than a sibling of it.
|
|
123
|
+
_jsx(Heading, { level: 3, className: `text-[11px] mb-0.5 md:mb-3`, truncate: 2, weight: "bold", shadow: "md", mdSize: "2xl", lgSize: "3xl", style: { color: card.content.textColor ?? "white" }, children: card.content.title })), card.content?.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: `text-[10px] md:text-[0.875rem] !/80 mb-1 md:mb-4`, truncate: true, children: card.content.description })), (card.buttons?.length ?? 0) > 0 && (_jsx(Row, { wrap: true, gap: "xs", className: "md:gap-[var(--appkit-space-2)]", children: (card.buttons ?? []).map((btn, i) => {
|
|
121
124
|
const variant = btn.variant === "link" || btn.variant === "ghost"
|
|
122
125
|
? "ghost"
|
|
123
126
|
: btn.variant;
|
|
124
127
|
return (_jsx(Button, { variant: variant, size: "sm", onClick: makeButtonClickHandler(btn.href, btn.openInNewTab, push), children: btn.text }, btn.id ?? i));
|
|
125
128
|
}) }))] })] })), card.isButtonOnly && card.buttons?.[0] && (_jsx(Button, { variant: "ghost", rounded: "none", className: `${POSITION_FILL} ${FLEX_CENTER} font-[600] text-white hover:bg-[rgba(0,0,0,0.2)] transition-colors p-[0]`, onClick: makeButtonClickHandler(card.buttons[0].href, card.buttons[0].openInNewTab, push), children: _jsx(Span, { size: "lg", mdSize: "2xl", children: card.buttons[0].text }) }))] }));
|
|
126
129
|
}
|
|
127
|
-
export function HeroCarousel({ initialSlides, push } = {}) {
|
|
130
|
+
export function HeroCarousel({ initialSlides, push, height, defaultAutoplayDelayMs, pauseOnHover = true, showDots = true, showArrows = true, title, } = {}) {
|
|
128
131
|
const { hand } = useHandMode();
|
|
129
132
|
const [currentSlide, setCurrentSlide] = useState(0);
|
|
130
133
|
const [isPaused, setIsPaused] = useState(false);
|
|
@@ -139,8 +142,9 @@ export function HeroCarousel({ initialSlides, push } = {}) {
|
|
|
139
142
|
?.sort((a, b) => a.order - b.order)
|
|
140
143
|
?.slice(0, 5) || [];
|
|
141
144
|
const currentSlideData = slides[currentSlide];
|
|
142
|
-
|
|
143
|
-
const
|
|
145
|
+
// Per-slide setting wins, then the section-level config, then the built-in.
|
|
146
|
+
const autoplayDelay = currentSlideData?.settings?.autoplayDelayMs ?? defaultAutoplayDelayMs ?? 4000;
|
|
147
|
+
const heightClass = getSlideHeightClass(currentSlideData?.settings?.height ?? height);
|
|
144
148
|
const goToSlide = useCallback((index) => {
|
|
145
149
|
const el = slidesRef.current;
|
|
146
150
|
if (!el)
|
|
@@ -184,16 +188,25 @@ export function HeroCarousel({ initialSlides, push } = {}) {
|
|
|
184
188
|
if (!slides || slides.length === 0) {
|
|
185
189
|
return (_jsxs(Div, { gap: "4", layout: "flex-col", align: "start", justify: "center", className: "relative w-full min-h-[260px] md:min-h-[60vh] bg-[image:var(--appkit-gradient-section-cool)] px-[var(--appkit-space-6)]", children: [_jsx(Heading, { level: 2, className: "text-primary", size: "2xl", weight: "bold", align: "start", children: "Coming Soon" }), _jsx(Text, { variant: "secondary", className: "max-w-md", align: "start", children: "Amazing deals are on their way. Stay tuned!" })] }));
|
|
186
190
|
}
|
|
187
|
-
return (_jsxs(Section, { ref: sectionRef, className: `relative w-full ${heightClass} overflow-hidden`, "aria-roledescription": "carousel", "aria-label": "Hero carousel", onKeyDown: handleKeyDown,
|
|
191
|
+
return (_jsxs(Section, { ref: sectionRef, className: `relative w-full ${heightClass} overflow-hidden`, "aria-roledescription": "carousel", "aria-label": title?.trim() || "Hero carousel", onKeyDown: handleKeyDown,
|
|
192
|
+
// Hover-pause is configurable; focus-pause is NOT. WCAG 2.2.2 requires a
|
|
193
|
+
// mechanism to pause moving content, and keyboard users have no hover.
|
|
194
|
+
onMouseEnter: pauseOnHover ? () => setIsPaused(true) : undefined, onMouseLeave: pauseOnHover ? () => setIsPaused(false) : undefined, onFocus: () => setIsPaused(true), onBlur: () => setIsPaused(false), tabIndex: 0, children: [_jsx(Div, { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: `Slide ${currentSlide + 1} of ${slides.length}` }), _jsx(Div, { className: POSITION_FILL, children: _jsx(Div, { layout: "flex", ref: slidesRef, onScroll: handleSlidesScroll, className: "h-full overflow-x-auto scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", style: { gap: 0 }, children: slides.map((slide, slideIndex) => {
|
|
188
195
|
const slideHeightClass = getSlideHeightClass(slide.settings?.height);
|
|
189
196
|
const hasCards = slide.cards && slide.cards.length > 0;
|
|
190
|
-
return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 !/80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (
|
|
197
|
+
return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 !/80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (
|
|
198
|
+
// level={2}, NOT 1. The welcome section above this
|
|
199
|
+
// carousel owns the page's single <h1>; a carousel that
|
|
200
|
+
// emits one <h1> per slide gives the document as many
|
|
201
|
+
// h1s as it has slides. `size` is independent of `level`,
|
|
202
|
+
// so this changes the outline and not one pixel.
|
|
203
|
+
_jsx(Heading, { color: "inverse", level: 2, className: "stagger-2 font-display ! mb-2 md:mb-4 break-words", size: "4xl", shadow: "2xl", mdSize: "6xl", lgSize: "8xl", children: slide.overlay.title })), slide.overlay.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-3 !/90 mb-4 md:mb-8 max-w-2xl", mdSize: "lg", lgSize: "xl", size: "sm", children: slide.overlay.description })), slide.overlay.button && (_jsx(Div, { className: "stagger-4", children: _jsx(Button, { variant: slide.overlay.button.variant, size: "sm", onClick: makeButtonClickHandler(slide.overlay.button.link, slide.overlay.button.openInNewTab, push), children: slide.overlay.button.text }) }))] })), hasCards && (_jsx(Div, { className: `${POSITION_FILL} grid gap-[var(--appkit-space-2)] md:gap-[var(--appkit-space-4)] md:p-[var(--appkit-space-8)]`, padding: "md", style: {
|
|
191
204
|
gridTemplateRows: "repeat(2, 1fr)",
|
|
192
205
|
gridTemplateColumns: isMobile ? "1fr" : "repeat(3, 1fr)",
|
|
193
206
|
alignContent: "center",
|
|
194
207
|
alignItems: "stretch",
|
|
195
208
|
}, children: slide.cards.slice(0, 6).map((card) => (_jsx(CarouselCardRenderer, { card: card, isMobile: isMobile, push: push }, card.id))) }))] }, slide.id));
|
|
196
|
-
}) }) }), slides.length > 1 && (_jsx(Row, { gap: "sm", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10", children: slides.map((_, index) => (_jsx(Button, { variant: "ghost", className: `relative overflow-hidden rounded-full transition-all duration-500 p-[var(--appkit-space-0)] !min-h-0 ${index === currentSlide
|
|
209
|
+
}) }) }), showDots && slides.length > 1 && (_jsx(Row, { gap: "sm", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10", children: slides.map((_, index) => (_jsx(Button, { variant: "ghost", className: `relative overflow-hidden rounded-full transition-all duration-500 p-[var(--appkit-space-0)] !min-h-0 ${index === currentSlide
|
|
197
210
|
? HERO_CAROUSEL_DOT_ACTIVE
|
|
198
|
-
: `${HERO_CAROUSEL_DOT_INACTIVE} hover:bg-white/75`}`, onClick: () => goToSlide(index), "aria-label": `Go to slide ${index + 1}`, children: index === currentSlide && (_jsx(Span, { className: "absolute inset-y-0 left-0 bg-[rgba(0,0,0,0.2)] animate-[progress-fill_4s_linear_forwards]", rounded: "full", "aria-hidden": "true" })) }, index))) })), slides.length > 1 && (_jsxs(Row, { gap: "sm", className: `absolute bottom-4 ${hand === "left" ? "left-4" : "right-4"} z-20`, children: [_jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goPrev, "aria-label": "Previous slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goNext, "aria-label": "Next slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] }))] }));
|
|
211
|
+
: `${HERO_CAROUSEL_DOT_INACTIVE} hover:bg-white/75`}`, onClick: () => goToSlide(index), "aria-label": `Go to slide ${index + 1}`, children: index === currentSlide && (_jsx(Span, { className: "absolute inset-y-0 left-0 bg-[rgba(0,0,0,0.2)] animate-[progress-fill_4s_linear_forwards]", rounded: "full", "aria-hidden": "true" })) }, index))) })), showArrows && slides.length > 1 && (_jsxs(Row, { gap: "sm", className: `absolute bottom-4 ${hand === "left" ? "left-4" : "right-4"} z-20`, children: [_jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goPrev, "aria-label": "Previous slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goNext, "aria-label": "Next slide", children: _jsx("svg", { className: "w-7 h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] }))] }));
|
|
199
212
|
}
|
|
@@ -4,9 +4,14 @@ export interface HomepageCustomerReviewsSectionProps {
|
|
|
4
4
|
subtitle?: string;
|
|
5
5
|
viewMoreHref?: string;
|
|
6
6
|
viewMoreLabel?: string;
|
|
7
|
+
/** Cap the rendered reviews. */
|
|
8
|
+
maxReviews?: number;
|
|
9
|
+
/** Cards visible per view on desktop / mobile. */
|
|
10
|
+
itemsPerView?: number;
|
|
11
|
+
mobileItemsPerView?: number;
|
|
7
12
|
className?: string;
|
|
8
13
|
autoScroll?: boolean;
|
|
9
14
|
scrollInterval?: number;
|
|
10
15
|
loop?: boolean;
|
|
11
16
|
}
|
|
12
|
-
export declare function HomepageCustomerReviewsSection({ title, subtitle, viewMoreHref, viewMoreLabel, className, autoScroll, scrollInterval, loop, }: HomepageCustomerReviewsSectionProps): React.JSX.Element;
|
|
17
|
+
export declare function HomepageCustomerReviewsSection({ title, subtitle, viewMoreHref, viewMoreLabel, maxReviews, itemsPerView, mobileItemsPerView, className, autoScroll, scrollInterval, loop, }: HomepageCustomerReviewsSectionProps): React.JSX.Element;
|
|
@@ -3,7 +3,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { CustomerReviewsSection } from "./CustomerReviewsSection";
|
|
4
4
|
import { useHomepageReviews } from "../hooks/useHomepageReviews";
|
|
5
5
|
import { ReviewCard } from "../../reviews/components";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { SECTION_TITLE, VIEW_MORE_LABEL } from "../constants/section-copy";
|
|
7
|
+
export function HomepageCustomerReviewsSection({ title = SECTION_TITLE.reviews, subtitle, viewMoreHref, viewMoreLabel = VIEW_MORE_LABEL.reviews, maxReviews, itemsPerView, mobileItemsPerView, className = "", autoScroll = true, scrollInterval = 4500, loop = true, }) {
|
|
8
|
+
const { data: fetched = [], isLoading } = useHomepageReviews();
|
|
9
|
+
const reviews = maxReviews && maxReviews > 0 ? fetched.slice(0, maxReviews) : fetched;
|
|
10
|
+
return (_jsx(CustomerReviewsSection, { title: title, subtitle: subtitle, items: reviews, renderItem: (review) => (_jsx(ReviewCard, { review: review, className: "h-full shadow-sm border-[var(--appkit-color-border)]" })), viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, isLoading: isLoading, keyExtractor: (review) => review.id, className: className, autoScroll: autoScroll, scrollInterval: scrollInterval, loop: loop,
|
|
11
|
+
// Only override the shared preset when the admin actually set one of the
|
|
12
|
+
// two counts — otherwise a partial override would silently flatten the
|
|
13
|
+
// preset's sm/lg/2xl steps down to two breakpoints.
|
|
14
|
+
perView: itemsPerView || mobileItemsPerView
|
|
15
|
+
? { base: mobileItemsPerView ?? 1, md: itemsPerView ?? 3 }
|
|
16
|
+
: undefined }));
|
|
9
17
|
}
|
|
@@ -5,5 +5,13 @@ export interface MarketplaceHomepageViewProps {
|
|
|
5
5
|
newsletterFormSlot?: React.ReactNode;
|
|
6
6
|
/** Callback when user dismisses announcement banner — wire to a server action to persist. */
|
|
7
7
|
onBannerDismiss?: (hash: string) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Consumer's site identity. Pass this from whatever single source already
|
|
10
|
+
* owns it (this app uses the same `appkit.config.js` block that backs
|
|
11
|
+
* `SITE_IDENTITY` in the root layout). Without it the sections fall back to
|
|
12
|
+
* brand-neutral copy — which is correct for a library, and is why the
|
|
13
|
+
* renderer no longer hardcodes "LIR" or "LetItRip".
|
|
14
|
+
*/
|
|
15
|
+
brand?: import("../lib/section-renderer").HomepageBrand;
|
|
8
16
|
}
|
|
9
|
-
export declare function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, }?: MarketplaceHomepageViewProps): Promise<React.JSX.Element>;
|
|
17
|
+
export declare function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, brand, }?: MarketplaceHomepageViewProps): Promise<React.JSX.Element>;
|
|
@@ -6,6 +6,7 @@ import { carouselRepository, faqsRepository, siteSettingsRepository } from "../.
|
|
|
6
6
|
import { safeRead } from "../../../errors/safe-read";
|
|
7
7
|
import { fetchLiveStats } from "../lib/live-stats";
|
|
8
8
|
import { renderSection, AnnouncementBar } from "../lib/section-renderer";
|
|
9
|
+
import { SECTION_COPY } from "../constants/section-copy";
|
|
9
10
|
import { homepageSectionsRepository } from "../repository/homepage-sections.repository";
|
|
10
11
|
import { isListingTypeEnabled } from "../../../_internal/shared/listing-types/feature-flags";
|
|
11
12
|
/**
|
|
@@ -38,7 +39,7 @@ function toCarouselSlides(docs) {
|
|
|
38
39
|
updatedAt: updatedAt instanceof Date ? updatedAt.toISOString() : undefined,
|
|
39
40
|
}));
|
|
40
41
|
}
|
|
41
|
-
export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, } = {}) {
|
|
42
|
+
export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, brand, } = {}) {
|
|
42
43
|
// Every read on this page is a RAIL: the homepage must still render when one
|
|
43
44
|
// fails, but a failure has to be distinguishable from an empty catalogue —
|
|
44
45
|
// that is exactly Root Cause #59, and safeRead is what records it.
|
|
@@ -48,8 +49,10 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
|
|
|
48
49
|
const siteSettings = await safeRead(() => siteSettingsRepository.getSingleton(), {
|
|
49
50
|
route: "/", key: "homepage.siteSettings", fallback: null,
|
|
50
51
|
});
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// The fallback used to be a hardcoded seasonal Pokémon TCG discount, which
|
|
53
|
+
// any install leaving the bar unconfigured would have published as live
|
|
54
|
+
// promotional copy for a promotion that does not exist.
|
|
55
|
+
const announcementMessage = siteSettings?.announcementBar?.message?.trim() || SECTION_COPY.announcementFallback;
|
|
53
56
|
const showAnnouncement = siteSettings?.announcementBar?.enabled ?? true;
|
|
54
57
|
const [allSections, rawFaqItems] = await Promise.all([
|
|
55
58
|
safeRead(() => homepageSectionsRepository.getEnabledSections(), {
|
|
@@ -172,5 +175,5 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
|
|
|
172
175
|
: undefined,
|
|
173
176
|
};
|
|
174
177
|
const carouselSlides = toCarouselSlides(slides);
|
|
175
|
-
return (_jsx(Main, { children: _jsxs(Div, { className: "relative", children: [showAnnouncement ? (_jsx(AnnouncementBar, { overlay: true, message: announcementMessage, link: siteSettings?.announcementBar?.link, onDismiss: onBannerDismiss })) : null, orderedSections.map((section) => renderSection(section, adSlots, newsletterFormSlot ?? null, faqItems, carouselSlides, liveStats, sectionData))] }) }));
|
|
178
|
+
return (_jsx(Main, { children: _jsxs(Div, { className: "relative", children: [showAnnouncement ? (_jsx(AnnouncementBar, { overlay: true, message: announcementMessage, link: siteSettings?.announcementBar?.link, onDismiss: onBannerDismiss })) : null, orderedSections.map((section) => renderSection(section, adSlots, newsletterFormSlot ?? null, faqItems, carouselSlides, liveStats, sectionData, brand))] }) }));
|
|
176
179
|
}
|
|
@@ -9,6 +9,11 @@ export interface StatItem {
|
|
|
9
9
|
}
|
|
10
10
|
export interface StatsCounterSectionProps {
|
|
11
11
|
stats: StatItem[];
|
|
12
|
+
/**
|
|
13
|
+
* Optional heading above the tiles. The section config has always carried a
|
|
14
|
+
* `title` and this component had nowhere to put it, so it never rendered.
|
|
15
|
+
*/
|
|
16
|
+
title?: string;
|
|
12
17
|
className?: string;
|
|
13
18
|
}
|
|
14
|
-
export declare function StatsCounterSection({ stats, className, }: StatsCounterSectionProps): import("react").JSX.Element;
|
|
19
|
+
export declare function StatsCounterSection({ stats, title, className, }: StatsCounterSectionProps): import("react").JSX.Element;
|
|
@@ -12,10 +12,10 @@ function StatCard({ stat, visible, delayClass, isLast, }) {
|
|
|
12
12
|
delayClass,
|
|
13
13
|
visible ? "translate-y-0 opacity-100" : "translate-y-6 opacity-0",
|
|
14
14
|
!isLast ? "border-r border-[var(--appkit-color-border)]" : "",
|
|
15
|
-
].join(" "), children: [stat.renderIcon && (_jsx(Row, { className: "mb-4 h-14 w-14 bg-primary-50 dark:bg-primary-950/40 border border-[var(--appkit-color-primary-100)] dark:border-[var(--appkit-color-primary-900)]/30", align: "center", justify: "center", rounded: "2xl", children: stat.renderIcon({ className: "w-7 h-7" }) })), _jsx(Heading, { level:
|
|
15
|
+
].join(" "), children: [stat.renderIcon && (_jsx(Row, { className: "mb-4 h-14 w-14 bg-primary-50 dark:bg-primary-950/40 border border-[var(--appkit-color-primary-100)] dark:border-[var(--appkit-color-primary-900)]/30", align: "center", justify: "center", rounded: "2xl", children: stat.renderIcon({ className: "w-7 h-7" }) })), _jsx(Heading, { level: 3, variant: "none", gradient: "brand", className: "mb-1 font-display font-black", mdSize: "5xl", size: "4xl", children: stat.value }), _jsx(Text, { variant: "none", className: "uppercase tracking-widest", color: "muted", size: "xs", weight: "medium", children: stat.label })] }));
|
|
16
16
|
}
|
|
17
17
|
// --- Sectmon -----------------------------------------------------------------
|
|
18
|
-
export function StatsCounterSection({ stats, className = "", }) {
|
|
18
|
+
export function StatsCounterSection({ stats, title, className = "", }) {
|
|
19
19
|
const sectionRef = useRef(null);
|
|
20
20
|
const [visible, setVisible] = useState(false);
|
|
21
21
|
useEffect(() => {
|
|
@@ -31,5 +31,5 @@ export function StatsCounterSection({ stats, className = "", }) {
|
|
|
31
31
|
observer.observe(el);
|
|
32
32
|
return () => observer.disconnect();
|
|
33
33
|
}, []);
|
|
34
|
-
return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children:
|
|
34
|
+
return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsxs(Div, { className: "mx-auto max-w-5xl", children: [title && (_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", className: "mb-6 text-left", children: title })), _jsx(Grid, { cols: "statTiles", gap: "none", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) })] }) }));
|
|
35
35
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAQ category → display label.
|
|
3
|
+
*
|
|
4
|
+
* Single owner. This map previously existed twice, byte-identical: once inline
|
|
5
|
+
* in `lib/section-renderer.tsx` and once as `CATEGORY_LABELS` in
|
|
6
|
+
* `components/FAQSection.tsx`. Adding a category meant two commits or a silent
|
|
7
|
+
* disagreement between the homepage strip's tabs and the section's own — the
|
|
8
|
+
* Duplication Framework's "bug-fix multiplier" trigger.
|
|
9
|
+
*
|
|
10
|
+
* Keyed loosely (`Record<string, string>`) rather than on `FAQCategoryKey`
|
|
11
|
+
* on purpose: `FAQCategoryKey` (7 values) and `FAQCategory` (8, including
|
|
12
|
+
* `scam_awareness`) have already drifted apart, and an unknown key here falls
|
|
13
|
+
* back to rendering the raw key rather than crashing a tab bar.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FAQ_CATEGORY_LABELS: Record<string, string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAQ category → display label.
|
|
3
|
+
*
|
|
4
|
+
* Single owner. This map previously existed twice, byte-identical: once inline
|
|
5
|
+
* in `lib/section-renderer.tsx` and once as `CATEGORY_LABELS` in
|
|
6
|
+
* `components/FAQSection.tsx`. Adding a category meant two commits or a silent
|
|
7
|
+
* disagreement between the homepage strip's tabs and the section's own — the
|
|
8
|
+
* Duplication Framework's "bug-fix multiplier" trigger.
|
|
9
|
+
*
|
|
10
|
+
* Keyed loosely (`Record<string, string>`) rather than on `FAQCategoryKey`
|
|
11
|
+
* on purpose: `FAQCategoryKey` (7 values) and `FAQCategory` (8, including
|
|
12
|
+
* `scam_awareness`) have already drifted apart, and an unknown key here falls
|
|
13
|
+
* back to rendering the raw key rather than crashing a tab bar.
|
|
14
|
+
*/
|
|
15
|
+
export const FAQ_CATEGORY_LABELS = {
|
|
16
|
+
general: "General",
|
|
17
|
+
orders_payment: "Orders & Payment",
|
|
18
|
+
shipping_delivery: "Shipping",
|
|
19
|
+
returns_refunds: "Returns & Refunds",
|
|
20
|
+
product_information: "Products",
|
|
21
|
+
account_security: "Account",
|
|
22
|
+
technical_support: "Support",
|
|
23
|
+
};
|