diff --git a/app/api/slider-photos/route.ts b/app/api/slider-photos/route.ts new file mode 100644 index 0000000..4008151 --- /dev/null +++ b/app/api/slider-photos/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { readSliderPhotos } from "@/lib/slider-photos"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const photos = readSliderPhotos(); + return NextResponse.json(photos); + } catch { + return NextResponse.json([]); + } +} diff --git a/app/components/tvone-content.tsx b/app/components/tvone-content.tsx index 2169405..5247935 100644 --- a/app/components/tvone-content.tsx +++ b/app/components/tvone-content.tsx @@ -294,9 +294,6 @@ export function TvoneFooter() {

Sobre a TV ONE

tvone

-

- Informação e entretenimento para Angola e para quem acompanha a atualidade. -

diff --git a/app/components/tvone-promo-strip.tsx b/app/components/tvone-promo-strip.tsx index c5d59b8..9decd94 100644 --- a/app/components/tvone-promo-strip.tsx +++ b/app/components/tvone-promo-strip.tsx @@ -1,141 +1,129 @@ +"use client"; + import Image from "next/image"; -import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; -const PROMO_IMG_LEFT = - "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=800&q=80&auto=format&fit=crop"; -const PROMO_IMG_RIGHT = - "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=800&q=80&auto=format&fit=crop"; +import type { SliderPhoto } from "@/lib/slider-photos"; + +const ROTATE_MS = 5500; + +function PromoStripSingleSlide({ + photos, + activeIndex, +}: { + photos: SliderPhoto[]; + activeIndex: number; +}) { + const n = photos.length; -function IconFacebook({ className }: { className?: string }) { return ( - - - +

+ {n > 0 ? ( + photos.map((photo, i) => ( + {photo.alt} + )) + ) : ( +
+ )} +
); } -function IconLinkedIn({ className }: { className?: string }) { - return ( - - - - ); +function useSliderPhotos(): { photos: SliderPhoto[]; loading: boolean } { + const [photos, setPhotos] = useState([]); + 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 }; } -function IconInstagram({ className }: { className?: string }) { - return ( - - - - ); -} - -/** Insurance / partner strip — scrolls with the page. */ +/** 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 ( -
- {/* Ambient glow */} -
-
-
+
+ + {current ? current.alt : loading ? "A carregar" : "Sem imagens"} + -
- Mulher em atividade ao ar livre -
-
-
- -
-
-
- - - - - - Nossa Seguros - -

- - Seguro Saúde Mulher - - — cuidado que acompanha o seu ritmo. -

- - Saber mais - - → - - -
- -
- Redes sociais -
- - - - - - - - - -
-
-
- -
- Bem-estar e cuidados de saúde -
-
+
+
); diff --git a/lib/slider-photos.ts b/lib/slider-photos.ts new file mode 100644 index 0000000..c2f2c48 --- /dev/null +++ b/lib/slider-photos.ts @@ -0,0 +1,63 @@ +import fs from "fs"; +import path from "path"; + +export type SliderPhoto = { src: string; alt: string }; + +function slugToAlt(filename: string): string { + const base = filename.replace(/\.[^.]+$/, ""); + const words = base.replace(/[-_]+/g, " ").trim(); + return words || "Slide"; +} + +export function parseManifest(data: unknown): SliderPhoto[] { + if (!Array.isArray(data)) return []; + const out: SliderPhoto[] = []; + for (const item of data) { + if (typeof item !== "object" || item === null || !("src" in item)) continue; + const src = (item as { src: unknown }).src; + if (typeof src !== "string" || !src.startsWith("/")) continue; + const altRaw = (item as { alt?: unknown }).alt; + const alt = typeof altRaw === "string" && altRaw.trim() ? altRaw.trim() : slugToAlt(src.split("/").pop() ?? ""); + out.push({ src, alt }); + } + return out; +} + +const IMAGE_EXT = /\.(jpe?g|png|webp|gif|avif)$/i; + +function scanSliderDirectory(dir: string): SliderPhoto[] { + let names: string[] = []; + try { + names = fs.readdirSync(dir); + } catch { + return []; + } + return names + .filter((f) => IMAGE_EXT.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })) + .map((f) => ({ + src: `/slider/${f}`, + alt: slugToAlt(f), + })); +} + +/** + * Reads `public/slider/manifest.json` when present (full control: order + alt). + * Otherwise scans `public/slider` for image files (drop-in updates, no code edits). + */ +export function readSliderPhotos(): SliderPhoto[] { + const dir = path.join(process.cwd(), "public", "slider"); + if (!fs.existsSync(dir)) return []; + + const manifestPath = path.join(dir, "manifest.json"); + if (fs.existsSync(manifestPath)) { + try { + const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as unknown; + return parseManifest(raw); + } catch { + return []; + } + } + + return scanSliderDirectory(dir); +} diff --git a/public/slider/.gitkeep b/public/slider/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/slider/slide1.jpeg b/public/slider/slide1.jpeg new file mode 100644 index 0000000..fa90989 Binary files /dev/null and b/public/slider/slide1.jpeg differ diff --git a/public/slider/slide2.jpeg b/public/slider/slide2.jpeg new file mode 100644 index 0000000..4dad055 Binary files /dev/null and b/public/slider/slide2.jpeg differ diff --git a/public/slider/slide3.jpeg b/public/slider/slide3.jpeg new file mode 100644 index 0000000..52aeca0 Binary files /dev/null and b/public/slider/slide3.jpeg differ diff --git a/public/slider/slide4.jpeg b/public/slider/slide4.jpeg new file mode 100644 index 0000000..38e3ec8 Binary files /dev/null and b/public/slider/slide4.jpeg differ diff --git a/public/slider/slide5.jpeg b/public/slider/slide5.jpeg new file mode 100644 index 0000000..eb8bbfc Binary files /dev/null and b/public/slider/slide5.jpeg differ