// 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 (
{ads.map((ad) => { const imageSrc = resolveAssetSrc(ad.image) || ad.image_url || null; const ctas = Array.isArray(ad.ctas) ? ad.ctas : []; const badgeLabels = Array.isArray(ad.badge_labels) ? ad.badge_labels : []; return ( {ad.content_mode !== "content" ? ( // ── Full Image ── // A landing page (Page Builder content) always wins over the CTA // click-through — it's the only destination this ad actually has. ) : ( // ── Content + Image ──
{badgeLabels.length > 0 && (
{badgeLabels.map((label, i) => ( {label} ))}
)} {ad.headline &&
{ad.headline}
} {ad.description &&

{ad.description}

} {ctas.length > 0 && (
{ctas.map((cta, i) => ( ))}
)}
)}
); })}
{/* Navigation and Progress — only meaningful with more than one slide */} {ads.length > 1 && (
)}
{/* ── Page Builder content Dialog ─────────────────────────────────── */} !open && setViewAd(null)}> {viewAd && (() => { const page = viewAd.landing_page ?? {}; const imageSrc = resolveAssetSrc(viewAd.image) || viewAd.image_url || null; const links = Array.isArray(page.links) ? page.links : []; return ( <> {page.title || viewAd.headline || "Advertisement"} {page.description && {page.description}} {imageSrc && (
{page.title
)} {page.body && (
{page.body}
)} {links.length > 0 && (
{links.map((l, i) => ( ))}
)} ); })()}
); } // ── BannerSkeleton ─────────────────────────────────────────────────────────── export function BannerSkeleton() { return ; }