Initial commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import Image from "next/image";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type ImageSlide = {
|
||||
type: "image";
|
||||
id: string;
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
type StatItem = {
|
||||
headline: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type StatsSlide = {
|
||||
type: "stats";
|
||||
id: string;
|
||||
title: string;
|
||||
items: StatItem[];
|
||||
};
|
||||
|
||||
export type HeroSlide = ImageSlide | StatsSlide;
|
||||
|
||||
type HeroSliderProps = {
|
||||
slides: HeroSlide[];
|
||||
autoAdvanceMs?: number;
|
||||
};
|
||||
|
||||
export default function HeroSlider({
|
||||
slides,
|
||||
autoAdvanceMs = 7000,
|
||||
}: HeroSliderProps) {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const touchStartX = useRef<number | null>(null);
|
||||
const touchDelta = useRef(0);
|
||||
|
||||
const maxIndex = slides.length - 1;
|
||||
|
||||
const goTo = (index: number) => {
|
||||
if (index < 0) return setActiveIndex(maxIndex);
|
||||
if (index > maxIndex) return setActiveIndex(0);
|
||||
return setActiveIndex(index);
|
||||
};
|
||||
|
||||
const next = () => goTo(activeIndex + 1);
|
||||
const prev = () => goTo(activeIndex - 1);
|
||||
|
||||
const currentSlide = useMemo(() => slides[activeIndex], [slides, activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaused || slides.length <= 1) return undefined;
|
||||
const timer = window.setInterval(() => {
|
||||
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
|
||||
}, autoAdvanceMs);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [autoAdvanceMs, isPaused, slides.length]);
|
||||
|
||||
const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
|
||||
touchStartX.current = event.touches[0]?.clientX ?? null;
|
||||
touchDelta.current = 0;
|
||||
};
|
||||
|
||||
const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (touchStartX.current === null) return;
|
||||
touchDelta.current =
|
||||
(event.touches[0]?.clientX ?? touchStartX.current) - touchStartX.current;
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
const delta = touchDelta.current;
|
||||
touchStartX.current = null;
|
||||
touchDelta.current = 0;
|
||||
if (Math.abs(delta) < 40) return;
|
||||
if (delta > 0) prev();
|
||||
if (delta < 0) next();
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="w-full"
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<div className="relative h-[320px] w-full overflow-hidden sm:h-[420px] lg:h-[560px]">
|
||||
{currentSlide.type === "image" ? (
|
||||
<Image
|
||||
src={currentSlide.src}
|
||||
alt={currentSlide.alt}
|
||||
fill
|
||||
sizes="100vw"
|
||||
priority
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-brand-purple-dark px-6">
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
<p className="text-center text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
|
||||
{currentSlide.title}
|
||||
</p>
|
||||
<div className="mt-8 grid gap-6 text-center md:grid-cols-3">
|
||||
{currentSlide.items.map((item) => (
|
||||
<div
|
||||
key={item.headline}
|
||||
className="rounded-2xl border border-white/15 bg-white/10 p-6 backdrop-blur"
|
||||
>
|
||||
<p className="text-3xl font-bold text-brand-pink sm:text-4xl">
|
||||
{item.headline}
|
||||
</p>
|
||||
<p className="mt-3 text-sm text-white/85">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-4 flex items-center justify-center gap-2">
|
||||
{slides.map((slide, index) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
className={`pointer-events-auto h-2.5 w-2.5 rounded-full transition ${
|
||||
index === activeIndex
|
||||
? "bg-brand-pink"
|
||||
: "bg-white/50 hover:bg-white"
|
||||
}`}
|
||||
onClick={() => goTo(index)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{slides.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="absolute left-4 top-1/2 hidden -translate-y-1/2 rounded-full border border-white/30 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:block"
|
||||
onClick={prev}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-4 top-1/2 hidden -translate-y-1/2 rounded-full border border-white/30 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:block"
|
||||
onClick={next}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 rounded-full border border-white/40 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:hidden"
|
||||
onClick={prev}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full border border-white/40 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:hidden"
|
||||
onClick={next}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user