Files
tvone/app/components/tvone-promo-strip.tsx
T

142 lines
4.0 KiB
TypeScript
Raw Normal View History

2026-03-28 23:01:17 +01:00
"use client";
2026-03-27 22:56:54 +01:00
import Image from "next/image";
2026-03-28 23:10:16 +01:00
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties } from "react";
2026-03-27 22:56:54 +01:00
2026-04-06 16:16:15 +01:00
export interface SliderPhoto {
src: string;
alt: string;
id?: string | number;
}
2026-03-27 22:56:54 +01:00
2026-03-28 23:01:17 +01:00
const ROTATE_MS = 5500;
2026-04-15 09:58:11 +01:00
// --- 1. SLIDE COMPONENT ---
2026-04-06 16:16:15 +01:00
interface SlideProps {
photo: SliderPhoto;
}
2026-03-27 23:10:26 +01:00
2026-04-15 09:58:11 +01:00
function PromoStripSingleSlide({ photo }: SlideProps) {
2026-03-27 23:10:26 +01:00
return (
2026-04-06 16:16:15 +01:00
<div className="relative h-full w-full">
<Image
src={photo.src}
alt={photo.alt}
fill
priority
className="object-cover object-center"
sizes="100vw"
/>
2026-03-28 23:01:17 +01:00
</div>
2026-03-27 23:10:26 +01:00
);
}
2026-04-15 09:58:11 +01:00
// --- 2. DATA HOOK ---
2026-03-28 23:01:17 +01:00
function useSliderPhotos(): { photos: SliderPhoto[]; loading: boolean } {
const [photos, setPhotos] = useState<SliderPhoto[]>([]);
2026-04-06 16:16:15 +01:00
const [loading, setLoading] = useState<boolean>(true);
2026-03-28 23:01:17 +01:00
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)) {
2026-04-06 16:16:15 +01:00
const validated = data.filter(
(x): x is SliderPhoto =>
typeof x === "object" && x !== null && "src" in x && "alt" in x
2026-03-28 23:01:17 +01:00
);
2026-04-06 16:16:15 +01:00
setPhotos(validated);
2026-03-28 23:01:17 +01:00
}
})
.catch(() => {
if (!cancelled) setPhotos([]);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
2026-04-06 16:16:15 +01:00
return () => { cancelled = true; };
2026-03-28 23:01:17 +01:00
}, []);
return { photos, loading };
2026-03-27 23:10:26 +01:00
}
2026-04-15 09:58:11 +01:00
// --- 3. MAIN COMPONENT ---
2026-03-27 22:56:54 +01:00
export function TvonePromoStrip() {
2026-03-28 23:01:17 +01:00
const { photos, loading } = useSliderPhotos();
2026-04-06 16:16:15 +01:00
const [index, setIndex] = useState<number>(0);
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
2026-03-27 22:56:54 +01:00
2026-04-15 09:58:11 +01:00
// Constants for the 4.8:1 ratio
const FIXED_RATIO = 4.8 / 1;
2026-03-28 23:01:17 +01:00
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;
2026-04-06 16:16:15 +01:00
setIndex((i: number) => (i + 1) % photos.length);
2026-03-28 23:01:17 +01:00
}, [photos.length]);
useEffect(() => {
if (photos.length <= 1 || reduceMotion) return;
const id = window.setInterval(advance, ROTATE_MS);
return () => window.clearInterval(id);
}, [photos.length, reduceMotion, advance]);
2026-04-15 09:58:11 +01:00
// Loading state with fixed ratio
2026-04-06 16:16:15 +01:00
if (loading && photos.length === 0) {
2026-04-15 09:58:11 +01:00
return (
<div
className="w-full animate-pulse bg-neutral-100 dark:bg-neutral-800"
style={{ aspectRatio: "4.8 / 1" }}
/>
);
2026-04-06 16:16:15 +01:00
}
2026-03-28 23:01:17 +01:00
2026-04-06 16:16:15 +01:00
if (photos.length === 0) return null;
2026-03-28 23:01:17 +01:00
return (
2026-04-06 16:16:15 +01:00
<section
2026-04-15 09:58:11 +01:00
className="relative w-full overflow-hidden bg-neutral-100 dark:bg-neutral-900 transition-all duration-700 ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{ aspectRatio: "4.8 / 1" }} // Locked to 4.8:1
2026-03-28 23:01:17 +01:00
role="region"
aria-roledescription="carrossel"
>
2026-04-15 09:58:11 +01:00
{/* THE TRACK */}
2026-04-06 16:16:15 +01:00
<div
className="flex h-full w-full transition-transform duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{ transform: `translateX(-${index * 100}%)` }}
>
{photos.map((photo: SliderPhoto, i: number) => (
<div key={photo.src || i} className="h-full w-full flex-shrink-0">
2026-04-15 09:58:11 +01:00
<PromoStripSingleSlide photo={photo} />
2026-04-06 16:16:15 +01:00
</div>
))}
2026-03-27 22:56:54 +01:00
</div>
2026-04-06 16:16:15 +01:00
2026-04-15 09:58:11 +01:00
{/* INDICATORS */}
2026-04-06 16:16:15 +01:00
{photos.length > 1 && (
2026-04-15 09:58:11 +01:00
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 z-20">
2026-04-06 16:16:15 +01:00
{photos.map((_, i: number) => (
<button
key={i}
onClick={() => setIndex(i)}
2026-04-15 09:58:11 +01:00
className={`h-1 transition-all duration-500 ${
index === i ? "w-6 bg-white" : "w-1.5 bg-white/30 hover:bg-white/60"
2026-04-06 16:16:15 +01:00
}`}
aria-label={`Ir para slide ${i + 1}`}
/>
))}
</div>
)}
</section>
2026-03-27 22:56:54 +01:00
);
2026-04-06 16:16:15 +01:00
}