fix: previews Meta vía proxy y creatividades Google PMax/RSA

Las imágenes Meta dejan de romperse en el navegador; Google trae assets en vivo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-23 18:25:48 +02:00
parent 86225a701e
commit d05df8c371
5 changed files with 697 additions and 57 deletions

View File

@@ -304,6 +304,12 @@ function CampaignTable({
);
}
function proxiedMediaUrl(raw: string): string {
if (!raw) return '';
if (raw.startsWith('/api/')) return raw;
return `/api/ad-tracker/media?url=${encodeURIComponent(raw)}`;
}
function CreativesGallery({
platform,
ads,
@@ -316,23 +322,42 @@ function CreativesGallery({
warning?: string | null;
}) {
const filtered = ads.filter((a) => a.platform === platform);
const platformWarning =
warning
?.split(' · ')
.find((w) =>
platform === 'meta'
? /meta|token meta|sin ads meta/i.test(w)
: /google|pmax|rsa|sin ads google|sin assets/i.test(w)
) || (platform === 'meta' || platform === 'google' ? null : null);
// Prefer full warning on matching platform; otherwise only platform-relevant part
const shownWarning =
filtered.length === 0 && warning
? platformWarning ||
(ads.length === 0 ? warning : null)
: platformWarning && filtered.length === 0
? platformWarning
: null;
return (
<section className="space-y-3">
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-500">
Creatividades · estilo Brava Ad Tracker
Creatividades · preview por anuncio
</p>
<h3 className="text-lg font-bold text-white">
{platform === 'meta' ? 'Meta Ads' : 'Google Ads'} · preview por anuncio
{platform === 'meta' ? 'Meta Ads' : 'Google Ads'} · creatividades
</h3>
<p className="text-xs text-gray-500 mt-1">
Preview serving / asset + KPIs del rango. Meta tira creatividades en
vivo; Google muestra ficha (assets RSA en siguiente iteración).
{platform === 'meta'
? 'Imagen / vídeo desde Meta Graph (proxy same-origin).'
: 'Assets PMax e RSA en vivo desde Google Ads API.'}
</p>
</div>
{warning ? (
{shownWarning ? (
<p className="text-xs text-amber-400/90 border border-amber-500/20 rounded-lg px-3 py-2">
{warning}
{shownWarning}
</p>
) : null}
{loading ? (
@@ -355,8 +380,21 @@ function CreativesGallery({
}
function CreativeCard({ ad }: { ad: AdCreativeCard }) {
const preview = ad.imageUrl || ad.thumbnailUrl;
const isVideo = ad.mediaType === 'video';
const raw = ad.imageUrl || ad.thumbnailUrl;
const ytPoster = ad.youtubeId
? proxiedMediaUrl(`https://i.ytimg.com/vi/${ad.youtubeId}/hqdefault.jpg`)
: '';
const preview = raw ? proxiedMediaUrl(raw) : ytPoster;
const isVideo = ad.mediaType === 'video' || Boolean(ad.youtubeId);
const headlines = (ad.headline || '')
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
const bodies = (ad.primaryText || '')
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
return (
<article className="grid gap-4 rounded-2xl border border-gray-800 bg-[#111827] p-4 md:grid-cols-[minmax(220px,320px)_1fr]">
<div className="relative overflow-hidden rounded-xl bg-black/40 border border-gray-800 min-h-[220px] flex items-center justify-center">
@@ -367,19 +405,23 @@ function CreativeCard({ ad }: { ad: AdCreativeCard }) {
alt={ad.adName}
className="w-full h-full object-cover max-h-[420px]"
loading="lazy"
referrerPolicy="no-referrer"
/>
) : ad.platform === 'google' ? (
<div className="w-full p-4 space-y-2 text-left">
) : ad.platform === 'google' && headlines.length ? (
<div className="w-full p-4 space-y-2 text-left bg-gradient-to-b from-[#1a2332] to-[#0d121f]">
<p className="text-[10px] uppercase tracking-wider text-sky-400">
Google Ads · {ad.mediaType || 'rsa'}
</p>
<p className="text-sm font-semibold text-blue-300 leading-snug">
{(ad.headline || ad.adName).split('\n')[0]}
</p>
<p className="text-xs text-gray-400 line-clamp-3">
{(ad.primaryText || ad.campaignName || '').split('\n')[0]}
</p>
{headlines.slice(0, 3).map((h) => (
<p
key={h}
className="text-sm font-semibold text-blue-300 leading-snug"
>
{h}
</p>
))}
{bodies[0] ? (
<p className="text-xs text-gray-400 line-clamp-3">{bodies[0]}</p>
) : null}
</div>
) : (
<div className="text-center text-gray-500 text-xs px-4">
@@ -424,17 +466,15 @@ function CreativeCard({ ad }: { ad: AdCreativeCard }) {
) : null}
</div>
</div>
{(ad.headline || ad.primaryText) && (
{(headlines.length > 0 || bodies.length > 0) && (
<div className="space-y-1 text-xs">
{ad.headline ? (
{headlines[0] ? (
<p className="text-gray-200 font-medium line-clamp-2">
{ad.headline.split('\n')[0]}
{headlines[0]}
</p>
) : null}
{ad.primaryText ? (
<p className="text-gray-500 line-clamp-3">
{ad.primaryText.split('\n')[0]}
</p>
{bodies[0] ? (
<p className="text-gray-500 line-clamp-3">{bodies[0]}</p>
) : null}
</div>
)}
@@ -731,6 +771,19 @@ export default function AdTrackerClient({
</div>
</section>
<CreativesGallery
platform="meta"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
<CreativesGallery
platform="google"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4 md:p-5">
<div className="flex flex-wrap items-start justify-between gap-3 mb-3">
<div>
@@ -809,19 +862,6 @@ export default function AdTrackerClient({
empty="Sin campañas Google activas en este periodo"
/>
</div>
<CreativesGallery
platform="meta"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
<CreativesGallery
platform="google"
ads={creatives}
loading={false}
warning={null}
/>
</div>
)}
@@ -836,6 +876,12 @@ export default function AdTrackerClient({
value={meta.cpl != null ? formatEur(meta.cpl) : '—'}
/>
</div>
<CreativesGallery
platform="meta"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4">
<h3 className="text-lg font-bold text-white mb-3">
Conversiones y CPL · Meta
@@ -853,12 +899,6 @@ export default function AdTrackerClient({
rows={metaCampaigns}
empty="Sin campañas Meta activas en este periodo"
/>
<CreativesGallery
platform="meta"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
</div>
)}
@@ -873,6 +913,12 @@ export default function AdTrackerClient({
value={google.cpl != null ? formatEur(google.cpl) : '—'}
/>
</div>
<CreativesGallery
platform="google"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4">
<h3 className="text-lg font-bold text-white mb-3">
Conversiones y CPL · Google
@@ -890,12 +936,6 @@ export default function AdTrackerClient({
rows={googleCampaigns}
empty="Sin campañas Google activas en este periodo"
/>
<CreativesGallery
platform="google"
ads={creatives}
loading={creativesLoading}
warning={creativesWarning}
/>
</div>
)}
</div>