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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user