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:
@@ -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' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user