diff --git a/app/components/tvone-promo-strip.tsx b/app/components/tvone-promo-strip.tsx index 6e8cb54..eff0be6 100644 --- a/app/components/tvone-promo-strip.tsx +++ b/app/components/tvone-promo-strip.tsx @@ -4,84 +4,53 @@ import Image from "next/image"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties } from "react"; -import type { SliderPhoto } from "@/lib/slider-photos"; +// Definição do tipo para garantir 100% de compatibilidade TS +export interface SliderPhoto { + src: string; + alt: string; + id?: string | number; +} const ROTATE_MS = 5500; -function PromoStripSingleSlide({ - photos, - activeIndex, -}: { - photos: SliderPhoto[]; - activeIndex: number; -}) { - const [ratioBySrc, setRatioBySrc] = useState>({}); - const lastKnownRatioRef = useRef(16 / 9); - - const active = photos[activeIndex]; - const activeRatio = active ? ratioBySrc[active.src] : undefined; - - const displayRatio = - activeRatio != null && activeRatio > 0 ? activeRatio : photos.length > 0 ? lastKnownRatioRef.current : undefined; - - useEffect(() => { - if (activeRatio != null && activeRatio > 0) { - lastKnownRatioRef.current = activeRatio; - } - }, [activeRatio]); - - const containerStyle = useMemo((): CSSProperties => { - if (photos.length === 0) { - return { minHeight: "10rem" }; - } - if (displayRatio != null) { - return { aspectRatio: displayRatio }; - } - return { minHeight: "10rem" }; - }, [photos.length, displayRatio]); - - const handleImageLoad = useCallback((src: string, naturalWidth: number, naturalHeight: number) => { - if (naturalHeight <= 0) return; - setRatioBySrc((prev) => { - const next = naturalWidth / naturalHeight; - if (prev[src] === next) return prev; - return { ...prev, [src]: next }; - }); - }, []); - - const n = photos.length; +// --- 1. COMPONENTE DE SLIDE INDIVIDUAL --- +interface SlideProps { + photo: SliderPhoto; + onRatioLoad: (src: string, ratio: number) => void; +} +function PromoStripSingleSlide({ photo, onRatioLoad }: SlideProps) { return ( -
- {n > 0 ? ( - photos.map((photo, i) => ( - {photo.alt} handleImageLoad(photo.src, e.currentTarget.naturalWidth, e.currentTarget.naturalHeight)} - /> - )) - ) : ( -
- )} +
+ {photo.alt} { + const target = e.currentTarget; + if (target.naturalHeight > 0) { + onRatioLoad(photo.src, target.naturalWidth / target.naturalHeight); + } + }} + /> + + {/* Label Interna - Estilo Premium/iOS */} +
+ + Destaque + +
); } +// --- 2. HOOK DE DADOS (Simulação de Fetch) --- function useSliderPhotos(): { photos: SliderPhoto[]; loading: boolean } { const [photos, setPhotos] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; @@ -90,19 +59,11 @@ function useSliderPhotos(): { photos: SliderPhoto[]; loading: boolean } { .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", - ), + const validated = data.filter( + (x): x is SliderPhoto => + typeof x === "object" && x !== null && "src" in x && "alt" in x ); - } else { - setPhotos([]); + setPhotos(validated); } }) .catch(() => { @@ -111,20 +72,20 @@ function useSliderPhotos(): { photos: SliderPhoto[]; loading: boolean } { .finally(() => { if (!cancelled) setLoading(false); }); - return () => { - cancelled = true; - }; + return () => { cancelled = true; }; }, []); return { photos, loading }; } -/** Slider: one image at a time; container height follows each image aspect ratio (no fixed empty band). */ +// --- 3. COMPONENTE PRINCIPAL (O CARROSSEL) --- export function TvonePromoStrip() { const { photos, loading } = useSliderPhotos(); - const [index, setIndex] = useState(0); - const [reduceMotion, setReduceMotion] = useState(false); + const [index, setIndex] = useState(0); + const [ratios, setRatios] = useState>({}); + const [reduceMotion, setReduceMotion] = useState(false); + // Detetar preferência de movimento do sistema (iOS/MacOS/Windows) useEffect(() => { const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); const sync = () => setReduceMotion(mq.matches); @@ -133,9 +94,10 @@ export function TvonePromoStrip() { return () => mq.removeEventListener("change", sync); }, []); + // Lógica de avanço automático const advance = useCallback(() => { if (photos.length <= 1) return; - setIndex((i) => (i + 1) % photos.length); + setIndex((i: number) => (i + 1) % photos.length); }, [photos.length]); useEffect(() => { @@ -144,27 +106,60 @@ export function TvonePromoStrip() { return () => window.clearInterval(id); }, [photos.length, reduceMotion, advance]); - useEffect(() => { - setIndex((prev) => (photos.length === 0 ? 0 : Math.min(prev, photos.length - 1))); - }, [photos.length]); + // Handler para atualizar rácios de imagem + const handleRatio = useCallback((src: string, ratio: number) => { + setRatios((prev) => (prev[src] === ratio ? prev : { ...prev, [src]: ratio })); + }, []); - const current = photos.length > 0 ? photos[index] : null; - const regionLabel = loading ? "A carregar galeria" : current?.alt ?? "Galeria"; + // Calcula a proporção do contentor baseado no slide ativo + const currentRatio = useMemo(() => { + const activePhoto = photos[index]; + return activePhoto ? (ratios[activePhoto.src] || 16 / 9) : 16 / 9; + }, [photos, index, ratios]); + + if (loading && photos.length === 0) { + return
; + } + + if (photos.length === 0) return null; return ( -
- - {current ? current.alt : loading ? "A carregar" : "Sem imagens"} - - -
- + {/* O "ROLO" (A fita flexível) */} +
+ {photos.map((photo: SliderPhoto, i: number) => ( +
+ +
+ ))}
-
+ + {/* INDICADORES (iOS Dot Style) */} + {photos.length > 1 && ( +
+ {photos.map((_, i: number) => ( +
+ )} + ); -} +} \ No newline at end of file