Files
2026-09-08 09:49:27 +02:00

164 lines
4.4 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import Image from "next/image";
import useEmblaCarousel from "embla-carousel-react";
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./Slider.module.css";
type Slide = {
heading: string;
text: string;
image: string;
};
type SliderProps = {
slides: Slide[];
imageAlt: string;
};
export function Slider({ slides, imageAlt }: SliderProps) {
const [emblaRef, emblaApi] = useEmblaCarousel({
align: "start",
loop: false,
duration: 28,
});
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const scrollPrevious = useCallback(() => {
emblaApi?.scrollPrev();
}, [emblaApi]);
const scrollNext = useCallback(() => {
emblaApi?.scrollNext();
}, [emblaApi]);
const scrollTo = useCallback(
(index: number) => {
emblaApi?.scrollTo(index);
},
[emblaApi],
);
const updateSelectedSlide = useCallback(() => {
if (!emblaApi) {
return;
}
setSelectedIndex(emblaApi.selectedScrollSnap());
setCanScrollPrev(emblaApi.canScrollPrev());
setCanScrollNext(emblaApi.canScrollNext());
}, [emblaApi]);
const initializeNavigation = useCallback(() => {
if (!emblaApi) {
return;
}
setScrollSnaps(emblaApi.scrollSnapList());
updateSelectedSlide();
}, [emblaApi, updateSelectedSlide]);
useEffect(() => {
if (!emblaApi) {
return;
}
initializeNavigation();
emblaApi.on("reInit", initializeNavigation);
emblaApi.on("select", updateSelectedSlide);
return () => {
emblaApi.off("reInit", initializeNavigation);
emblaApi.off("select", updateSelectedSlide);
};
}, [emblaApi, initializeNavigation, updateSelectedSlide]);
if (slides.length === 0) {
return null;
}
return (
<section
className={styles.slider}
aria-roledescription="carousel"
aria-label="Weitere Informationen"
>
<div className={styles.viewport} ref={emblaRef}>
<div className={styles.container}>
{slides.map((slide, index) => (
<article
key={`${slide.heading}-${index}`}
className={styles.slide}
aria-hidden={selectedIndex !== index}
>
<div className={styles.split}>
<div className={styles.textPanel}>
<h3 className={styles.heading}>{slide.heading}</h3>
<p className={styles.text}>{slide.text}</p>
</div>
<div className={styles.imagePanel}>
<Image
src={slide.image}
alt={imageAlt}
fill
sizes="(max-width: 700px) 100vw, 50vw"
className={styles.image}
/>
</div>
</div>
</article>
))}
</div>
</div>
{slides.length > 1 && (
<div className={styles.controls}>
<button
type="button"
className={styles.arrow}
onClick={scrollPrevious}
disabled={!canScrollPrev}
suppressHydrationWarning
aria-label="Vorheriger Slide"
>
<CaretLeftIcon size={20} weight="bold" aria-hidden="true" />
</button>
<div className={styles.dots} aria-label="Slide auswählen">
{scrollSnaps.map((_, index) => (
<button
key={index}
type="button"
className={`${styles.dot} ${
selectedIndex === index ? styles.dotActive : ""
}`}
onClick={() => scrollTo(index)}
aria-label={`Slide ${index + 1} von ${slides.length}`}
aria-current={selectedIndex === index ? "true" : undefined}
/>
))}
</div>
<button
type="button"
className={styles.arrow}
onClick={scrollNext}
disabled={!canScrollNext}
suppressHydrationWarning
aria-label="Nächster Slide"
>
<CaretRightIcon size={20} weight="bold" aria-hidden="true" />
</button>
</div>
)}
</section>
);
}