261 lines
7.9 KiB
TypeScript
261 lines
7.9 KiB
TypeScript
import {useCallback, useEffect, useMemo, useState} from 'react';
|
|
import {Link} from 'react-router';
|
|
import {motion} from 'motion/react';
|
|
import {AbiirProductListItem} from '~/components/AbiirProductListItem';
|
|
import type {
|
|
AbiirProductListItemData,
|
|
FeaturedProductsCms,
|
|
} from '~/lib/featuredProducts';
|
|
import {
|
|
MOCK_FEATURED_PRODUCT_ROWS,
|
|
MOCK_FEATURED_PRODUCTS_CMS,
|
|
} from '~/lib/featuredProducts';
|
|
import {PAGE_TITLE_TYPOGRAPHY_CLASS} from '~/lib/pageTitle';
|
|
import {useSwipeNavigate} from '~/lib/useSwipeNavigate';
|
|
|
|
/** Desktop featured grid matches `ABIIR_PRODUCT_LIST_GRID_CLASS` at `lg:` only (`abiirProductListGrid.ts`). */
|
|
const FEATURED_DESKTOP_GRID_CLASS =
|
|
'hidden w-full max-w-full lg:grid lg:grid-cols-4 lg:gap-x-[34px] lg:gap-y-10';
|
|
|
|
/**
|
|
* GROQ: `home` singleton → featured products (Most Loved) module.
|
|
* Use with `context.sanity.loadQuery(FEATURED_PRODUCTS_GROQ)` or `useQuery` in the browser.
|
|
*/
|
|
export const FEATURED_PRODUCTS_GROQ = `*[_type == "home" && _id == "home"][0].featuredProducts{
|
|
eyebrow,
|
|
title,
|
|
filterPills[]{label, href},
|
|
viewAllLabel,
|
|
viewAllHref,
|
|
items[]{
|
|
productHandle,
|
|
hideBadge,
|
|
cardTitle,
|
|
subtitle,
|
|
benefitChips,
|
|
rating,
|
|
reviewCount
|
|
}
|
|
}` as const;
|
|
|
|
export type {FeaturedProductsCms, AbiirProductListItemData};
|
|
export {MOCK_FEATURED_PRODUCTS_CMS, MOCK_FEATURED_PRODUCT_ROWS};
|
|
|
|
const sectionBg = '#f8f4ef';
|
|
const burgundy = '#5d0b24';
|
|
const charcoal = '#1a1a18';
|
|
/** Mobile / tablet featured strip: auto-advance interval (desktop keeps grid). */
|
|
const FEATURED_MOBILE_SLIDE_MS = 4000;
|
|
|
|
function FeaturedProductsMobileSlider({rows}: {rows: AbiirProductListItemData[]}) {
|
|
const [active, setActive] = useState(0);
|
|
const rowsKey = useMemo(
|
|
() => rows.map((r) => r.handle).join('\0'),
|
|
[rows],
|
|
);
|
|
|
|
useEffect(() => {
|
|
setActive(0);
|
|
}, [rowsKey]);
|
|
|
|
useEffect(() => {
|
|
if (rows.length <= 1) return;
|
|
if (typeof window === 'undefined') return;
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
const id = window.setInterval(() => {
|
|
setActive((i) => (i + 1) % rows.length);
|
|
}, FEATURED_MOBILE_SLIDE_MS);
|
|
return () => window.clearInterval(id);
|
|
}, [rows.length]);
|
|
|
|
const goPrev = useCallback(() => {
|
|
if (rows.length <= 1) return;
|
|
setActive((i) => (i - 1 + rows.length) % rows.length);
|
|
}, [rows.length]);
|
|
|
|
const goNext = useCallback(() => {
|
|
if (rows.length <= 1) return;
|
|
setActive((i) => (i + 1) % rows.length);
|
|
}, [rows.length]);
|
|
|
|
const swipe = useSwipeNavigate(goPrev, goNext);
|
|
|
|
return (
|
|
<div
|
|
className="w-full overflow-hidden lg:hidden"
|
|
aria-roledescription="carousel"
|
|
aria-label="Featured products"
|
|
{...(rows.length > 1 ? swipe : {})}
|
|
>
|
|
<div
|
|
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
style={{transform: `translateX(-${active * 100}%)`}}
|
|
>
|
|
{rows.map((item, index) => (
|
|
<div
|
|
key={`${item.handle}-${index}`}
|
|
className={`w-full shrink-0 basis-full px-0.5 ${
|
|
index !== active ? 'pointer-events-none' : ''
|
|
}`}
|
|
aria-hidden={index !== active}
|
|
>
|
|
<AbiirProductListItem
|
|
index={index}
|
|
item={item}
|
|
variant="homepageFeatured"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div
|
|
className="mt-6 flex justify-center gap-1.5"
|
|
aria-hidden
|
|
>
|
|
{rows.map((_, i) => (
|
|
<span
|
|
key={i}
|
|
role="presentation"
|
|
className="h-1.5 rounded-full transition-all duration-300"
|
|
style={{
|
|
width: i === active ? 22 : 6,
|
|
background: i === active ? burgundy : 'rgba(26,26,24,0.22)',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
function isExternalHref(href: string) {
|
|
return /^https?:\/\//i.test(href);
|
|
}
|
|
|
|
function FilterLink({
|
|
href,
|
|
children,
|
|
}: {
|
|
href: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const className =
|
|
'inline-flex min-h-[44.07px] cursor-pointer items-center justify-center border border-[rgba(26,26,24,0.2)] px-[26.5px] py-[13.75px] font-["Jost",sans-serif] text-[12px] font-medium uppercase leading-[16.575px] tracking-[1.326px] text-[#1a1a18] no-underline transition-opacity duration-200 box-border hover:opacity-70';
|
|
if (isExternalHref(href)) {
|
|
return (
|
|
<a href={href} className={className}>
|
|
{children}
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<Link to={href} className={className}>
|
|
{children}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
function ViewAllLink({
|
|
href,
|
|
children,
|
|
}: {
|
|
href: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const className =
|
|
'inline-flex min-h-[51.844px] cursor-pointer items-center justify-center bg-[#b39763] px-[34px] py-[17px] font-["Raleway",sans-serif] text-[14px] font-bold uppercase leading-[19.89px] tracking-[2.3868px] text-white no-underline transition-opacity duration-200 box-border hover:opacity-90';
|
|
if (isExternalHref(href)) {
|
|
return (
|
|
<a href={href} className={className}>
|
|
{children}
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<Link to={href} className={className}>
|
|
{children}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export type FeaturedProductsProps = {
|
|
cms: FeaturedProductsCms;
|
|
/** Merged Shopify + Sanity rows (loader) or mock rows for static preview. */
|
|
rows: AbiirProductListItemData[];
|
|
};
|
|
|
|
/**
|
|
* Homepage “Most Loved” strip — `React/src/app/components/FeaturedProducts.tsx` (Tailwind).
|
|
* Placed below `BrandValues` on the index route.
|
|
*/
|
|
export function FeaturedProducts({cms, rows}: FeaturedProductsProps) {
|
|
const pills =
|
|
cms.filterPills.length > 0
|
|
? cms.filterPills
|
|
: MOCK_FEATURED_PRODUCTS_CMS.filterPills;
|
|
|
|
return (
|
|
<section className="relative w-full" style={{background: sectionBg}}>
|
|
<div className="mx-auto max-w-[1710px] px-6 pt-16 pb-20 sm:px-10 sm:pt-20 lg:px-16 lg:pb-28 lg:pt-[119px] xl:px-24 2xl:px-[206px]">
|
|
<div className="mb-5 flex flex-col gap-4 lg:mb-[68px] lg:flex-row lg:items-end lg:justify-between lg:gap-6">
|
|
<div className="max-w-[286px]">
|
|
<motion.p
|
|
initial={false}
|
|
whileInView={{opacity: 1, y: 0}}
|
|
viewport={{once: true}}
|
|
transition={{duration: 0.5}}
|
|
className="m-0 mb-2 font-['Jost',sans-serif] text-[12px] font-medium uppercase leading-[16.575px] tracking-[2.7625px] not-italic"
|
|
style={{color: burgundy}}
|
|
>
|
|
{cms.eyebrow}
|
|
</motion.p>
|
|
<motion.h2
|
|
initial={false}
|
|
whileInView={{opacity: 1, y: 0}}
|
|
viewport={{once: true}}
|
|
transition={{duration: 0.6, delay: 0.05}}
|
|
className={`m-0 ${PAGE_TITLE_TYPOGRAPHY_CLASS}`}
|
|
style={{color: charcoal}}
|
|
>
|
|
{cms.title}
|
|
</motion.h2>
|
|
</div>
|
|
|
|
<nav
|
|
className="flex flex-wrap items-center justify-start gap-[13px] lg:justify-end"
|
|
aria-label="Product filters"
|
|
>
|
|
{pills.map((pill, i) => (
|
|
<FilterLink key={`${pill.href}-${i}`} href={pill.href}>
|
|
{pill.label}
|
|
</FilterLink>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
<FeaturedProductsMobileSlider rows={rows} />
|
|
|
|
<div className={FEATURED_DESKTOP_GRID_CLASS}>
|
|
{rows.map((item, index) => (
|
|
<AbiirProductListItem
|
|
key={item.handle + String(index)}
|
|
index={index}
|
|
item={item}
|
|
variant="homepageFeatured"
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<motion.div
|
|
initial={false}
|
|
whileInView={{opacity: 1, y: 0}}
|
|
viewport={{once: true}}
|
|
transition={{duration: 0.55, delay: 0.15}}
|
|
className="mt-14 flex justify-center lg:mt-20"
|
|
>
|
|
<ViewAllLink href={cms.viewAllHref}>{cms.viewAllLabel}</ViewAllLink>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|