'use client'; import React, { useEffect } from 'react'; import type { AdCreativeCard, CreativeAspect, CreativeAsset, } from '@/lib/ad-creatives-meta'; import { pickMetaPreviewAsset } from '@/lib/ad-creatives-meta'; import { formatEur, formatEurCents, formatNum } from '@/lib/metrics'; import { ImageIcon, Play, Maximize2, X, ExternalLink } from 'lucide-react'; export function proxiedMediaUrl(raw: string): string { if (!raw) return ''; if (raw.startsWith('/api/')) return raw; return `/api/ad-tracker/media?url=${encodeURIComponent(raw)}`; } function adHeadlines(ad: AdCreativeCard): string[] { const all = ad.headlines?.length ? ad.headlines : (ad.headline || '') .split('\n') .map((s) => s.trim()) .filter(Boolean); if (ad.platform === 'google') return all; return all.slice(0, 5); } function adBodies(ad: AdCreativeCard): string[] { const all = ad.bodies?.length ? ad.bodies : (ad.primaryText || '') .split('\n') .map((s) => s.trim()) .filter(Boolean); if (ad.platform === 'google') return all; return all.slice(0, 5); } function adLinkDescriptions(ad: AdCreativeCard): string[] { if (ad.descriptions?.length) return ad.descriptions; return []; } function adAssets(ad: AdCreativeCard): CreativeAsset[] { if (ad.assets?.length) return ad.assets; const out: CreativeAsset[] = []; const u = ad.imageUrl || ad.thumbnailUrl; if (u) out.push({ kind: 'image', url: u, label: 'preview', aspect: '9:16' }); if (ad.youtubeId) { out.push({ kind: 'youtube', url: `https://i.ytimg.com/vi/${ad.youtubeId}/hqdefault.jpg`, youtubeId: ad.youtubeId, label: 'YOUTUBE', aspect: '16:9', }); } return out; } function resolveAspect(asset: CreativeAsset): CreativeAspect { if (asset.aspect && asset.aspect !== 'unknown') return asset.aspect; const L = (asset.label || '').toLowerCase(); if (/9\s*:?\s*16|story|reels|vertical|tall/.test(L)) return '9:16'; if (/4\s*:?\s*5|portrait/.test(L)) return '4:5'; if (/16\s*:?\s*9|1\.91|landscape|horizontal|youtube/.test(L)) { return '16:9'; } if (/1\s*:?\s*1|square|logo/.test(L)) return '1:1'; return 'unknown'; } function aspectClass(aspect: CreativeAspect): string { switch (aspect) { case '9:16': return 'aspect-[9/16]'; case '4:5': return 'aspect-[4/5]'; case '16:9': return 'aspect-video'; case '1:1': return 'aspect-square'; default: return 'aspect-[4/5]'; } } /** Meta: frame 9:16 by default; carousel 1:1 then 4:5. Google PMax stays portrait-first. */ function previewMeta(ad: AdCreativeCard): { url: string; aspect: CreativeAspect } { const assets = adAssets(ad); const picked = pickMetaPreviewAsset( assets, ad.mediaType || 'image', ad.imageUrl || ad.thumbnailUrl || '' ); // Frame rules (user): vertical default; carousel square / 4:5 const aspect: CreativeAspect = ad.mediaType === 'carousel' ? picked.aspect === '4:5' ? '4:5' : '1:1' : '9:16'; const raw = picked.url || ad.imageUrl || ad.thumbnailUrl || assets.find((a) => a.url)?.url || ''; return { url: raw ? proxiedMediaUrl(raw) : '', aspect }; } function previewGoogle(ad: AdCreativeCard): { url: string; aspect: CreativeAspect } { const assets = adAssets(ad); const isSearch = ad.mediaType === 'rsa' || ad.channel === 'search_brand' || ad.channel === 'search_generic'; if (isSearch) { return { url: '', aspect: '9:16' }; } const portrait = assets.find( (a) => (a.kind === 'image' || a.kind === 'logo') && (resolveAspect(a) === '4:5' || resolveAspect(a) === '9:16') ); const square = assets.find( (a) => (a.kind === 'image' || a.kind === 'logo') && resolveAspect(a) === '1:1' ); const landscape = assets.find( (a) => a.kind === 'image' && resolveAspect(a) === '16:9' ); const raw = portrait?.url || square?.url || landscape?.url || ad.imageUrl || ad.thumbnailUrl || assets.find((a) => a.kind === 'image')?.url || (ad.youtubeId ? `https://i.ytimg.com/vi/${ad.youtubeId}/hqdefault.jpg` : '') || assets.find((a) => a.youtubeId)?.url || ''; const aspect: CreativeAspect = portrait ? resolveAspect(portrait) : square ? '1:1' : landscape ? '16:9' : '4:5'; return { url: raw ? proxiedMediaUrl(raw) : '', aspect }; } function previewFor(ad: AdCreativeCard): { url: string; aspect: CreativeAspect } { return ad.platform === 'meta' ? previewMeta(ad) : previewGoogle(ad); } function formatThumbs(ad: AdCreativeCard): { url: string; aspect: CreativeAspect; label: string }[] { if (ad.mediaType !== 'pmax' && ad.channel !== 'pmax') return []; const assets = adAssets(ad).filter( (a) => (a.kind === 'image' || a.kind === 'logo' || a.kind === 'youtube') && a.url ); const seen = new Set(); const out: { url: string; aspect: CreativeAspect; label: string }[] = []; for (const a of assets) { if (seen.has(a.url)) continue; seen.add(a.url); const asp = resolveAspect(a); out.push({ url: proxiedMediaUrl(a.url), aspect: asp === 'unknown' ? '1:1' : asp, label: a.label || asp, }); } return out; } function CopyList({ label, items, strong, muted, }: { label: string; items: string[]; strong?: boolean; muted?: boolean; }) { return (

{label}

); } function MiniKpi({ label, value }: { label: string; value: string }) { return (

{label}

{value}

); } export function CreativesGallery({ platform, ads, loading, warning, onZoom, }: { platform: 'meta' | 'google'; ads: AdCreativeCard[]; loading: boolean; warning?: string | null; onZoom: (ad: AdCreativeCard) => void; }) { const filtered = ads.filter((a) => a.platform === platform); const platformWarning = warning ?.split(' · ') .find((w) => { // Never surface soft Graph/insights failures when we still render cards if (/insights ad-level|fetch failed|métricas repartidas|sin imagen/i.test(w)) { return false; } return platform === 'meta' ? /meta|token meta|sin ads meta|sin actividad meta|sin cuenta meta/i.test(w) : /google|pmax|rsa|sin ads google|sin assets|oauth|warehouse|reconecta/i.test( w ); }) || (platform === 'google' && warning && /google|oauth|token/i.test(warning) ? warning : null); const googleGroups: { key: string; title: string; hint: string; items: AdCreativeCard[]; }[] = platform === 'google' ? [ { key: 'search_brand', title: 'Search · Brand', hint: 'RSA de marca (titulares + URL)', items: filtered.filter((a) => a.channel === 'search_brand'), }, { key: 'search_generic', title: 'Search · Generic', hint: 'RSA genéricos / captación', items: filtered.filter( (a) => a.channel === 'search_generic' || (!a.channel && a.mediaType === 'rsa') ), }, { key: 'pmax', title: 'Performance Max', hint: 'Asset groups · imágenes, logos y vídeo', items: filtered.filter( (a) => a.channel === 'pmax' || a.mediaType === 'pmax' ), }, { key: 'display', title: 'Display · Remarketing', hint: 'RDA / image ads · creatividades visuales', items: filtered.filter( (a) => a.channel === 'display' || a.mediaType === 'rda' || a.mediaType === 'image' ), }, { key: 'other', title: 'Otros Google', hint: 'Campañas sin clasificar', items: filtered.filter((a) => { const ch = a.channel; if ( ch === 'search_brand' || ch === 'search_generic' || ch === 'pmax' || ch === 'display' ) { return false; } if (!ch && (a.mediaType === 'rsa' || a.mediaType === 'pmax' || a.mediaType === 'rda' || a.mediaType === 'image')) { return false; } return true; }), }, ].filter((g) => g.items.length > 0) : []; return (

Creatividades · preview por anuncio

{platform === 'meta' ? 'Meta Ads' : 'Google Ads'} · creatividades

{platform === 'google' ? 'Agrupado por Search Brand / Generic, Performance Max y Display. Search muestra preview tipo SERP; PMax muestra todos los formatos (1:1, 4:5, 16:9).' : 'Meta: preview vertical 9:16 por defecto; carrusel en 1:1 (o 4:5). Pulsa Zoom para el resto de piezas.'}

{platformWarning ? (

{platformWarning}

) : null} {loading ? (

Cargando creatividades…

) : filtered.length === 0 ? (

Sin creatividades para este periodo.

) : platform === 'google' && googleGroups.length > 0 ? (
{googleGroups.map((g) => (

{g.title}

{g.hint} · {g.items.length}

{g.items.map((ad) => ( onZoom(ad)} /> ))}
))}
) : (
{filtered.map((ad) => ( onZoom(ad)} /> ))}
)}
); } function CreativeCard({ ad, onZoom, }: { ad: AdCreativeCard; onZoom: () => void; }) { const { url: preview, aspect: frameAspect } = previewFor(ad); const isVideo = ad.mediaType === 'video' || Boolean(ad.youtubeId); const headlines = adHeadlines(ad); const bodies = adBodies(ad); const linkDescs = adLinkDescriptions(ad); const longTitles = ad.longHeadlines || []; const googleDescs = ad.platform === 'google' ? ad.descriptions?.length ? ad.descriptions : bodies : []; const assets = adAssets(ad); const extraCount = Math.max(0, assets.length - 1); const pmaxFormats = formatThumbs(ad); const isSearch = ad.platform === 'google' && (ad.mediaType === 'rsa' || ad.channel === 'search_brand' || ad.channel === 'search_generic'); return (
{pmaxFormats.length > 0 ? (
{pmaxFormats.slice(0, 12).map((f, i) => ( ))} {pmaxFormats.length > 12 ? (

+{pmaxFormats.length - 12} en Zoom

) : null}
) : null}

{ad.adName || 'Sin nombre'}

{ad.status || ad.platform}

{ad.campaignName || '—'}

{ad.adsetName ? (

Conjunto: {ad.adsetName}

) : null}
{(headlines.length > 0 || longTitles.length > 0 || bodies.length > 0 || linkDescs.length > 0 || googleDescs.length > 0) && (
{ad.platform === 'google' ? ( <> {headlines.length > 0 ? ( ) : null} {longTitles.length > 0 ? ( ) : null} {googleDescs.length > 0 ? ( ) : null} ) : ( <> {headlines.length > 0 ? ( ) : null} {bodies.length > 0 ? ( ) : null} {linkDescs.length > 0 ? ( ) : null} )} {ad.cta ? (

CTA · {ad.cta}

) : null}
)}
); } export function CreativeZoomModal({ ad, onClose, }: { ad: AdCreativeCard; onClose: () => void; }) { const headlines = adHeadlines(ad); const bodies = adBodies(ad); const linkDescs = adLinkDescriptions(ad); const longH = ad.longHeadlines || []; const googleDescs = ad.platform === 'google' ? ad.descriptions?.length ? ad.descriptions : bodies : []; const isGoogle = ad.platform === 'google'; const assets = adAssets(ad); const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo'); const videos = assets.filter((a) => a.kind === 'youtube' || a.kind === 'video'); const { url: main, aspect: mainAspect } = previewFor(ad); const zoomImages = ad.platform === 'google' ? images.filter((a) => a.url) : images.filter((a) => { if (!a.url) return false; const previewAssetUrl = main.replace( /^\/api\/ad-tracker\/media\?url=/, '' ); if (!previewAssetUrl) return true; try { return ( decodeURIComponent(previewAssetUrl) !== a.url && a.url !== main ); } catch { return a.url !== main; } }); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev; }; }, [onClose]); return (
e.stopPropagation()} >

Zoom creatividades ·{' '} {ad.platform === 'meta' ? 'Meta Ads' : 'Google Ads'}

{ad.adName || 'Anuncio'}

{ad.campaignName} {ad.adsetName ? ` · ${ad.adsetName}` : ''}

{main ? ( // eslint-disable-next-line @next/next/no-img-element {ad.adName} ) : (
Sin preview
)}

Preview {mainAspect}

Impresiones:{' '} {formatNum(ad.impressions)}

Formato:{' '} {ad.mediaType || '—'}

{ad.cta ? (

CTA: {ad.cta}

) : null} {ad.linkUrl ? ( {ad.linkUrl} ) : null}
{zoomImages.length > 0 ? (

{ad.platform === 'google' ? `Imágenes y logos (${zoomImages.length})` : `Otras piezas (${zoomImages.length})`}

{zoomImages.map((asset, i) => { const aspect = resolveAspect(asset); return (
{/* eslint-disable-next-line @next/next/no-img-element */} {asset.label
{aspect !== 'unknown' ? aspect : asset.label || 'imagen'} {asset.kind === 'logo' ? ' · logo' : ''}
); })}
) : null} {videos.length > 0 ? (

Vídeos ({videos.length})

{videos.map((v, i) => (
{v.youtubeId ? (