// components/blocks/Banner.jsx import { useEffect, useState } from "react"; import { Megaphone } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } from "@/components/ui/carousel"; import { Progress } from "@/components/ui/progress"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { resolveAssetSrc } from "@/utils/media.util"; // An ad only ever has an internally-authored landing page when it has no // redirect_link (see applyAdvertisementFields on the backend, which clears // whichever one the admin didn't choose) — so its presence alone is enough // to tell whether there's Page Builder content worth surfacing. function hasLandingPage(ad) { const page = ad?.landing_page; if (!page) return false; return Boolean(page.title || page.description || page.body || page.links?.length); } // ── Banner ─────────────────────────────────────────────────────────────────── /** * Banner advertisement carousel — same carousel shell as Hero, so a placement * with several live ads (e.g. tier_plans.banner) rotates through all of them * instead of only ever showing the single highest-priority one. * Each slide's layout is driven by its own content_mode: * "content" — decorative panel with badge/headline/description/CTAs * "image" — full-bleed image, clickable through to the ad's redirect/CTA * * Props: * ads — array of advertisement objects { content_mode, badge_labels, headline, description, ctas, image, image_url, advertisement_id } * onCtaClick — (ad, cta) => void. cta is undefined for the whole-banner click on image-only ads. */ export function Banner({ ads, onCtaClick }) { const [api, setApi] = useState(); const [current, setCurrent] = useState(0); const [count, setCount] = useState(0); const [viewAd, setViewAd] = useState(null); // ad whose Page Builder content is open in the Dialog useEffect(() => { if (!api) return; setCount(api.scrollSnapList().length); setCurrent(api.selectedScrollSnap() + 1); api.on("select", () => { setCurrent(api.selectedScrollSnap() + 1); }); }, [api]); if (!ads?.length) return null; const progressValue = count ? (current / count) * 100 : 0; return (
{ad.description}
} {ctas.length > 0 && (