mirror of
https://github.com/PeterMaquiran/tvone.git
synced 2026-04-18 15:27:52 +00:00
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
import type { SliderPhoto } from "@/lib/slider-photos";
|
|
|
|
const ROTATE_MS = 5500;
|
|
|
|
function PromoStripSingleSlide({
|
|
photos,
|
|
activeIndex,
|
|
}: {
|
|
photos: SliderPhoto[];
|
|
activeIndex: number;
|
|
}) {
|
|
const n = photos.length;
|
|
|
|
return (
|
|
<div className="relative min-h-[140px] w-full sm:min-h-[168px] md:min-h-[190px] lg:min-h-[210px]">
|
|
{n > 0 ? (
|
|
photos.map((photo, i) => (
|
|
<Image
|
|
key={photo.src}
|
|
src={photo.src}
|
|
alt={photo.alt}
|
|
fill
|
|
className={`object-cover object-center transition-opacity duration-[900ms] ease-in-out ${
|
|
i === activeIndex ? "z-[1] opacity-100" : "pointer-events-none z-0 opacity-0"
|
|
}`}
|
|
sizes="100vw"
|
|
aria-hidden={i !== activeIndex}
|
|
/>
|
|
))
|
|
) : (
|
|
<div className="absolute inset-0 bg-neutral-200 dark:bg-neutral-800" aria-hidden />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function useSliderPhotos(): { photos: SliderPhoto[]; loading: boolean } {
|
|
const [photos, setPhotos] = useState<SliderPhoto[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
fetch("/api/slider-photos", { cache: "no-store" })
|
|
.then((r) => (r.ok ? r.json() : []))
|
|
.then((data: unknown) => {
|
|
if (cancelled) return;
|
|
if (Array.isArray(data)) {
|
|
setPhotos(
|
|
data.filter(
|
|
(x): x is SliderPhoto =>
|
|
typeof x === "object" &&
|
|
x !== null &&
|
|
"src" in x &&
|
|
typeof (x as SliderPhoto).src === "string" &&
|
|
"alt" in x &&
|
|
typeof (x as SliderPhoto).alt === "string",
|
|
),
|
|
);
|
|
} else {
|
|
setPhotos([]);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setPhotos([]);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
return { photos, loading };
|
|
}
|
|
|
|
/** Slider: one image at a time from `public/slider` (see `lib/slider-photos.ts`). */
|
|
export function TvonePromoStrip() {
|
|
const { photos, loading } = useSliderPhotos();
|
|
const [index, setIndex] = useState(0);
|
|
const [reduceMotion, setReduceMotion] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
const sync = () => setReduceMotion(mq.matches);
|
|
sync();
|
|
mq.addEventListener("change", sync);
|
|
return () => mq.removeEventListener("change", sync);
|
|
}, []);
|
|
|
|
const advance = useCallback(() => {
|
|
if (photos.length <= 1) return;
|
|
setIndex((i) => (i + 1) % photos.length);
|
|
}, [photos.length]);
|
|
|
|
useEffect(() => {
|
|
if (photos.length <= 1 || reduceMotion) return;
|
|
const id = window.setInterval(advance, ROTATE_MS);
|
|
return () => window.clearInterval(id);
|
|
}, [photos.length, reduceMotion, advance]);
|
|
|
|
useEffect(() => {
|
|
setIndex((prev) => (photos.length === 0 ? 0 : Math.min(prev, photos.length - 1)));
|
|
}, [photos.length]);
|
|
|
|
const current = photos.length > 0 ? photos[index] : null;
|
|
const regionLabel = loading ? "A carregar galeria" : current?.alt ?? "Galeria";
|
|
|
|
return (
|
|
<div
|
|
className="relative w-full overflow-hidden border-y border-neutral-200/80 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900"
|
|
role="region"
|
|
aria-roledescription="carrossel"
|
|
aria-label={regionLabel}
|
|
>
|
|
<span className="sr-only" aria-live="polite">
|
|
{current ? current.alt : loading ? "A carregar" : "Sem imagens"}
|
|
</span>
|
|
|
|
<div className="relative flex min-h-[140px] w-full flex-col sm:min-h-[168px] md:min-h-[190px] lg:min-h-[210px]">
|
|
<PromoStripSingleSlide photos={photos} activeIndex={index} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|