feat: upload creativos, PMax formats, portada IG/FB y seguimiento con notas

Permite subir media sin URL; previews Google PMax por ratio; calendario anual estilo Miia con filtros IG/FB y LinkedIn; seguimiento con selector de fechas, KPIs y cuadro de notas.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-23 09:33:22 +02:00
parent a7704567bd
commit 0d286dc66c
12 changed files with 1295 additions and 150 deletions

View File

@@ -28,6 +28,12 @@ export type CreativeFormat =
| 'story'
| 'rsa'
| 'pmax'
| 'pmax_landscape'
| 'pmax_square'
| 'pmax_portrait'
| 'pmax_vertical'
| 'pmax_logo'
| 'youtube'
| 'other';
export type CreativePreviewData = {
@@ -350,13 +356,45 @@ function GoogleSearchPreview({ data }: { data: CreativePreviewData }) {
);
}
function GoogleDisplayPreview({ data }: { data: CreativePreviewData }) {
function GoogleDisplayPreview({
data,
aspect = 'landscape',
}: {
data: CreativePreviewData;
aspect?: 'landscape' | 'square' | 'portrait' | 'vertical' | 'logo';
}) {
const ratio =
aspect === 'square'
? 'aspect-square'
: aspect === 'portrait'
? 'aspect-[4/5]'
: aspect === 'vertical'
? 'aspect-[9/16]'
: aspect === 'logo'
? 'aspect-square max-w-[140px] mx-auto'
: 'aspect-[1.91/1]';
const label =
aspect === 'square'
? '1:1 · cuadrado'
: aspect === 'portrait'
? '4:5 · portrait'
: aspect === 'vertical'
? '9:16 · vertical'
: aspect === 'logo'
? 'Logo'
: '1.91:1 · landscape';
return (
<div className="rounded-2xl border border-gray-700 bg-[#111827] overflow-hidden">
<p className="text-[10px] font-bold uppercase tracking-wider text-sky-400 px-3 py-2 border-b border-gray-800">
Google Ads · display / PMax
Google Ads · Performance Max · {label}
</p>
<MediaBlock data={data} />
<div className={`${ratio} overflow-hidden bg-gray-900`}>
<MediaBlock
data={data}
tall={aspect === 'vertical' || aspect === 'portrait'}
/>
</div>
<div className="p-3 bg-white text-gray-900">
<p className="text-sm font-bold line-clamp-2">
{data.headline || data.title}
@@ -370,6 +408,30 @@ function GoogleDisplayPreview({ data }: { data: CreativePreviewData }) {
);
}
function YouTubePreview({ data }: { data: CreativePreviewData }) {
return (
<div className="rounded-2xl border border-gray-700 bg-[#111827] overflow-hidden">
<p className="text-[10px] font-bold uppercase tracking-wider text-red-400 px-3 py-2 border-b border-gray-800">
Google Ads · YouTube
</p>
<div className="aspect-video relative bg-black">
<MediaBlock data={data} tall />
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<Play className="w-12 h-12 text-white/80" />
</div>
</div>
<div className="p-3 bg-white text-gray-900">
<p className="text-sm font-bold line-clamp-2">
{data.headline || data.title}
</p>
<p className="text-[11px] text-gray-600 line-clamp-2 mt-1">
{data.description || data.primary_text}
</p>
</div>
</div>
);
}
/** Renders a placement mock like Ad Tracker phone / Google RSA cards. */
export default function CreativePlacementPreview({
data,
@@ -384,13 +446,31 @@ export default function CreativePlacementPreview({
let body: React.ReactNode;
if (channel === 'google_ads' || format === 'rsa') {
body =
format === 'pmax' || placement === 'display' ? (
<GoogleDisplayPreview data={data} />
) : (
<GoogleSearchPreview data={data} />
);
if (format === 'youtube') {
body = <YouTubePreview data={data} />;
} else if (
format === 'pmax_landscape' ||
format === 'pmax_square' ||
format === 'pmax_portrait' ||
format === 'pmax_vertical' ||
format === 'pmax_logo' ||
format === 'pmax' ||
(channel === 'google_ads' &&
(placement === 'display' || format.startsWith('pmax')))
) {
const aspect =
format === 'pmax_square' || format === 'pmax_logo'
? format === 'pmax_logo'
? 'logo'
: 'square'
: format === 'pmax_portrait'
? 'portrait'
: format === 'pmax_vertical'
? 'vertical'
: 'landscape';
body = <GoogleDisplayPreview data={data} aspect={aspect} />;
} else if (channel === 'google_ads' || format === 'rsa') {
body = <GoogleSearchPreview data={data} />;
} else if (
channel === 'stories' ||
placement === 'stories' ||
@@ -443,8 +523,14 @@ export const FORMAT_OPTIONS: { id: CreativeFormat; label: string }[] = [
{ id: 'carousel', label: 'Carrusel' },
{ id: 'reel', label: 'Reel' },
{ id: 'story', label: 'Story' },
{ id: 'rsa', label: 'RSA (búsqueda)' },
{ id: 'pmax', label: 'PMax / Display' },
{ id: 'rsa', label: 'Google RSA (búsqueda)' },
{ id: 'pmax', label: 'PMax (genérico)' },
{ id: 'pmax_landscape', label: 'PMax landscape 1.91:1' },
{ id: 'pmax_square', label: 'PMax cuadrado 1:1' },
{ id: 'pmax_portrait', label: 'PMax portrait 4:5' },
{ id: 'pmax_vertical', label: 'PMax vertical 9:16' },
{ id: 'pmax_logo', label: 'PMax logo' },
{ id: 'youtube', label: 'YouTube / vídeo in-stream' },
{ id: 'other', label: 'Otro' },
];
@@ -454,5 +540,9 @@ export const PLACEMENT_OPTIONS = [
{ id: 'reels', label: 'Reels' },
{ id: 'search', label: 'Búsqueda' },
{ id: 'display', label: 'Display' },
{ id: 'youtube', label: 'YouTube' },
{ id: 'discover', label: 'Discover' },
{ id: 'gmail', label: 'Gmail' },
{ id: 'maps', label: 'Maps' },
{ id: 'other', label: 'Otro' },
];