Initial commit

This commit is contained in:
Haitham Khalifa
2026-02-16 12:02:45 +01:00
commit 11252e6520
37 changed files with 8118 additions and 0 deletions
+174
View File
@@ -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>
);
}