Show every Google PMax text, image and YouTube asset in Ads zoom.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,26 +17,29 @@ export function proxiedMediaUrl(raw: string): string {
|
||||
}
|
||||
|
||||
function adHeadlines(ad: AdCreativeCard): string[] {
|
||||
if (ad.headlines?.length) return ad.headlines.slice(0, 5);
|
||||
return (ad.headline || '')
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
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[] {
|
||||
if (ad.bodies?.length) return ad.bodies.slice(0, 5);
|
||||
// Legacy: primaryText used to hold bodies
|
||||
return (ad.primaryText || '')
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
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.slice(0, 5);
|
||||
if (ad.descriptions?.length) return ad.descriptions;
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -158,19 +161,19 @@ function previewFor(ad: AdCreativeCard): { url: string; aspect: CreativeAspect }
|
||||
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
|
||||
(a) =>
|
||||
(a.kind === 'image' || a.kind === 'logo' || a.kind === 'youtube') && a.url
|
||||
);
|
||||
const order: CreativeAspect[] = ['1:1', '4:5', '16:9', '9:16'];
|
||||
const out: { url: string; aspect: CreativeAspect; label: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const asp of order) {
|
||||
const hit = assets.find((a) => resolveAspect(a) === asp && !seen.has(a.url));
|
||||
if (!hit) continue;
|
||||
seen.add(hit.url);
|
||||
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(hit.url),
|
||||
aspect: asp,
|
||||
label: asp,
|
||||
url: proxiedMediaUrl(a.url),
|
||||
aspect: asp === 'unknown' ? '1:1' : asp,
|
||||
label: a.label || asp,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@@ -451,15 +454,15 @@ function CreativeCard({
|
||||
<Maximize2 className="w-3.5 h-3.5" /> Zoom
|
||||
</span>
|
||||
</button>
|
||||
{pmaxFormats.length > 1 ? (
|
||||
{pmaxFormats.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5 justify-center">
|
||||
{pmaxFormats.map((f) => (
|
||||
{pmaxFormats.slice(0, 12).map((f, i) => (
|
||||
<button
|
||||
key={f.label}
|
||||
key={`${f.url}-${i}`}
|
||||
type="button"
|
||||
onClick={onZoom}
|
||||
className="w-12 overflow-hidden rounded-md border border-gray-200 bg-gray-50"
|
||||
title={`Formato ${f.label}`}
|
||||
title={f.label}
|
||||
>
|
||||
<div className={`${aspectClass(f.aspect)} w-full`}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
@@ -475,6 +478,11 @@ function CreativeCard({
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
{pmaxFormats.length > 12 ? (
|
||||
<p className="w-full text-center text-[10px] text-gray-500">
|
||||
+{pmaxFormats.length - 12} en Zoom
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -585,21 +593,24 @@ export function CreativeZoomModal({
|
||||
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 previewAssetUrl = main.replace(/^\/api\/ad-tracker\/media\?url=/, '');
|
||||
const otherImages = images.filter((a) => {
|
||||
if (!a.url) return false;
|
||||
if (!previewAssetUrl) return true;
|
||||
try {
|
||||
return decodeURIComponent(previewAssetUrl) !== a.url && a.url !== main;
|
||||
} catch {
|
||||
return a.url !== main;
|
||||
}
|
||||
});
|
||||
// Prefer showing all format variants in zoom for Meta carousel / Google PMax
|
||||
const zoomImages =
|
||||
otherImages.length > 0
|
||||
? otherImages
|
||||
: images.filter((a) => a.url && proxiedMediaUrl(a.url) !== main);
|
||||
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) => {
|
||||
@@ -714,7 +725,9 @@ export function CreativeZoomModal({
|
||||
{zoomImages.length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Otras piezas ({zoomImages.length})
|
||||
{ad.platform === 'google'
|
||||
? `Imágenes y logos (${zoomImages.length})`
|
||||
: `Otras piezas (${zoomImages.length})`}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{zoomImages.map((asset, i) => {
|
||||
@@ -810,7 +823,7 @@ export function CreativeZoomModal({
|
||||
Titulares largos ({longH.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{longH.slice(0, 5).map((h) => (
|
||||
{longH.map((h) => (
|
||||
<li
|
||||
key={h}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
@@ -857,6 +870,89 @@ export function CreativeZoomModal({
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{ad.businessName ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Nombre de negocio
|
||||
</h3>
|
||||
<p className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-800">
|
||||
{ad.businessName}
|
||||
</p>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.ctas || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
CTA ({ad.ctas!.length})
|
||||
</h3>
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{ad.ctas!.map((c) => (
|
||||
<li
|
||||
key={c}
|
||||
className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-800"
|
||||
>
|
||||
{c}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.sitelinks || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Sitelinks ({ad.sitelinks!.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{ad.sitelinks!.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.callouts || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Callouts ({ad.callouts!.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{ad.callouts!.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.snippets || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Structured snippets ({ad.snippets!.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{ad.snippets!.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user