Connect storefront to backend catalog, search, and home slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
2f857f1a90
commit
0074ab6629
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import HoverFillButton from "./HoverFillButton";
|
||||
|
||||
export type HeroSlideData = {
|
||||
id: string;
|
||||
image: string;
|
||||
imagePosition?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
cta: { label: string; href: string };
|
||||
};
|
||||
|
||||
const FALLBACK_SLIDES: HeroSlideData[] = [
|
||||
{
|
||||
id: "autumn",
|
||||
image: "/images/home/banners/hero.jpg",
|
||||
imagePosition: "object-[center_20%]",
|
||||
title: "برای بانویی که خودش انتخاب میکند.",
|
||||
subtitle: "made for her",
|
||||
cta: { label: "کالکشن پاییزه", href: "/products?collection=autumn" },
|
||||
},
|
||||
{
|
||||
id: "new",
|
||||
image: "/images/home/banners/hero.jpg",
|
||||
imagePosition: "object-[center_40%]",
|
||||
title: "جدیدترینهای فصل همین حالا ببینید.",
|
||||
subtitle: "new arrivals",
|
||||
cta: { label: "جدیدترینها", href: "/products?sort=newest" },
|
||||
},
|
||||
{
|
||||
id: "sale",
|
||||
image: "/images/home/banners/hero.jpg",
|
||||
imagePosition: "object-[center_55%]",
|
||||
title: "فروش ویژه تا ۷۰٪ تخفیف",
|
||||
subtitle: "up to 70% off",
|
||||
cta: { label: "مشاهده تخفیفها", href: "/products?sale=1" },
|
||||
},
|
||||
];
|
||||
|
||||
const AUTO_MS = 10000;
|
||||
|
||||
type HeroSliderProps = {
|
||||
slides?: HeroSlideData[];
|
||||
};
|
||||
|
||||
export default function HeroSlider({ slides }: HeroSliderProps) {
|
||||
const list = useMemo(
|
||||
() => (slides?.length ? slides : FALLBACK_SLIDES),
|
||||
[slides],
|
||||
);
|
||||
const [index, setIndex] = useState(0);
|
||||
const [textKey, setTextKey] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const slide = list[index] ?? list[0];
|
||||
|
||||
useEffect(() => {
|
||||
setIndex(0);
|
||||
}, [list]);
|
||||
|
||||
const goTo = useCallback((next: number) => {
|
||||
setIndex(next);
|
||||
setTextKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
const goPrev = () => goTo((index - 1 + list.length) % list.length);
|
||||
const goNext = () => goTo((index + 1) % list.length);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused || list.length <= 1) return;
|
||||
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
if (reduce) return;
|
||||
const t = window.setInterval(() => {
|
||||
setIndex((i) => (i + 1) % list.length);
|
||||
setTextKey((k) => k + 1);
|
||||
}, AUTO_MS);
|
||||
return () => window.clearInterval(t);
|
||||
}, [index, paused, list.length]);
|
||||
|
||||
if (!slide) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative h-[min(100dvh,1081px)] min-h-[560px] w-full overflow-hidden bg-[#ebe6df]"
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
>
|
||||
{list.map((s, i) => {
|
||||
const active = i === index;
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`absolute inset-0 transition-opacity duration-1000 ease-out ${
|
||||
active ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
aria-hidden={!active}
|
||||
>
|
||||
<Image
|
||||
key={active ? `on-${s.id}-${index}` : `off-${s.id}`}
|
||||
src={s.image}
|
||||
alt=""
|
||||
fill
|
||||
priority={i === 0}
|
||||
sizes="100vw"
|
||||
className={`object-cover ${s.imagePosition ?? "object-center"} ${
|
||||
active ? "hero-ken-burns" : ""
|
||||
} ${active && paused ? "hero-ken-burns-paused" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-l from-white/25 via-transparent to-transparent" />
|
||||
|
||||
<div className="absolute inset-y-0 right-16 z-10 flex w-[min(360px,calc(100%-6.5rem))] items-center sm:right-24 lg:right-[200px]">
|
||||
<div
|
||||
key={textKey}
|
||||
dir="rtl"
|
||||
className="hero-copy-in w-full pt-16 text-right"
|
||||
>
|
||||
<h1 className="hero-copy-item whitespace-pre-line font-doran text-right text-[clamp(1.25rem,2.2vw,2.25rem)] font-medium leading-[1.4] text-ink">
|
||||
{slide.title}
|
||||
</h1>
|
||||
{slide.subtitle ? (
|
||||
<p
|
||||
dir="ltr"
|
||||
className="hero-copy-item hero-copy-item-2 mt-3 block w-full whitespace-pre-line text-right font-montserrat text-[clamp(0.875rem,1.4vw,1.35rem)] font-light uppercase tracking-[0.28em] text-[#6d6d6d]"
|
||||
>
|
||||
{slide.subtitle}
|
||||
</p>
|
||||
) : null}
|
||||
{slide.cta.label ? (
|
||||
<div className="hero-copy-item hero-copy-item-3">
|
||||
<HoverFillButton href={slide.cta.href || "#"} className="mt-7 w-full">
|
||||
{slide.cta.label}
|
||||
</HoverFillButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{list.length > 1 ? (
|
||||
<>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-4 z-20 flex items-center sm:left-8 lg:left-[120px]">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="اسلاید قبلی"
|
||||
onClick={goPrev}
|
||||
className="pointer-events-auto flex size-10 items-center justify-center rounded-[4px] bg-white/20 backdrop-blur-sm transition hover:bg-white/40 sm:size-12"
|
||||
>
|
||||
<Image
|
||||
src="/images/home/icons/caret-left-dark.svg"
|
||||
alt=""
|
||||
width={22}
|
||||
height={22}
|
||||
unoptimized
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-4 z-20 flex items-center sm:right-8 lg:right-[120px]">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="اسلاید بعدی"
|
||||
onClick={goNext}
|
||||
className="pointer-events-auto flex size-10 items-center justify-center rounded-[4px] bg-white/20 backdrop-blur-sm transition hover:bg-white/40 sm:size-12"
|
||||
>
|
||||
<Image
|
||||
src="/images/home/icons/caret-right.svg"
|
||||
alt=""
|
||||
width={22}
|
||||
height={22}
|
||||
unoptimized
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
dir="ltr"
|
||||
className="absolute inset-x-0 bottom-8 z-20 flex items-center justify-center gap-2.5 sm:bottom-12"
|
||||
role="tablist"
|
||||
aria-label="اسلایدهای هیرو"
|
||||
>
|
||||
{list.map((s, i) => {
|
||||
const active = i === index;
|
||||
return (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
aria-label={`اسلاید ${i + 1}`}
|
||||
onClick={() => goTo(i)}
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
active
|
||||
? "w-8 bg-ink"
|
||||
: "w-2 bg-ink/30 hover:bg-ink/50"
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user