feat: Zona de marca tipo Estilo/Brief + brandbooks desde ads/RRSS

UI con nav lateral numerada y panel editable; parse de brandbook JSON string;
enrich desde Meta/Google/IG/FB/web/CSS para que app.4ventures.es muestre el kit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-15 03:23:23 +02:00
parent 102c100427
commit 3cb3ad1db1
10 changed files with 3967 additions and 69 deletions

View File

@@ -99,16 +99,18 @@ function PillRow({
}
function websiteFromBook(book: Brandbook): string | null {
const fromChannels =
book.channel_handles?.blog ||
book.channels?.blog ||
book.channels?.instagram ||
'';
const raw = String(fromChannels || '').trim();
if (!raw) return null;
if (/^https?:\/\//i.test(raw)) return raw;
if (raw.includes('.') && !raw.startsWith('@')) {
return `https://${raw.replace(/^\/+/, '')}`;
const candidates = [
book.website,
book.channel_handles?.blog,
book.channels?.blog,
];
for (const c of candidates) {
const raw = String(c || '').trim();
if (!raw) continue;
if (/^https?:\/\//i.test(raw)) return raw;
if (raw.includes('.') && !raw.startsWith('@')) {
return `https://${raw.replace(/^\/+/, '')}`;
}
}
return null;
}
@@ -120,6 +122,7 @@ export default function BrandOverview({
missing,
onEditSection,
onContinueEdit,
embedded = false,
}: {
book: Brandbook;
tenantLabel: string;
@@ -127,6 +130,8 @@ export default function BrandOverview({
missing: string[];
onEditSection: (id: BrandbookSectionId) => void;
onContinueEdit: () => void;
/** Cuando va dentro del panel de Zona de marca (sin card exterior). */
embedded?: boolean;
}) {
const logo = primaryLogo(book);
const site = websiteFromBook(book);
@@ -167,34 +172,44 @@ export default function BrandOverview({
).trim();
}, [book]);
return (
<div className="space-y-4">
<Surface className="!p-0 overflow-hidden border-gray-200">
<div className="border-b border-gray-200 bg-gradient-to-br from-gray-50 via-white to-brand-50/40 px-5 py-4 sm:px-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-[10px] font-bold uppercase tracking-[0.18em] text-brand-600">
Brand Overview
</p>
<h2 className="mt-1 text-xl font-extrabold tracking-tight text-gray-900 sm:text-2xl">
Perfil de marca
</h2>
<p className="mt-1 text-sm text-gray-600">
Resumen visual del brandbook · editable en Detalles del kit
</p>
</div>
<button
type="button"
onClick={onContinueEdit}
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50"
>
Editar brand kit
<ArrowRight className="h-3.5 w-3.5" />
</button>
</div>
</div>
const header = (
<div
className={
embedded
? 'flex flex-wrap items-start justify-between gap-3 pb-2'
: 'border-b border-gray-200 bg-gradient-to-br from-gray-50 via-white to-brand-50/40 px-5 py-4 sm:px-6'
}
>
<div className={embedded ? '' : undefined}>
<p className="text-[10px] font-bold uppercase tracking-[0.18em] text-brand-600">
Brand Overview
</p>
<h2
className={`mt-1 font-extrabold tracking-tight text-gray-900 ${
embedded ? 'text-base' : 'text-xl sm:text-2xl'
}`}
>
Perfil de marca
</h2>
{!embedded ? (
<p className="mt-1 text-sm text-gray-600">
Resumen visual del brandbook · editable por secciones
</p>
) : null}
</div>
<button
type="button"
onClick={onContinueEdit}
className="inline-flex items-center gap-1.5 rounded-xl border border-gray-200 bg-white px-3 py-2 text-xs font-bold text-gray-800 hover:bg-gray-50"
>
Editar identidad
<ArrowRight className="h-3.5 w-3.5" />
</button>
</div>
);
<div className="space-y-8 px-5 py-6 sm:px-6">
const content = (
<div className={embedded ? 'space-y-8' : 'space-y-8 px-5 py-6 sm:px-6'}>
{/* Identity */}
<section className="grid gap-6 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-3">
@@ -372,10 +387,70 @@ export default function BrandOverview({
)}
</div>
</section>
</div>
{/* Footer progress */}
<div className="flex flex-col gap-3 border-t border-gray-200 bg-gray-50 px-5 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
{/* Copy real ads / RRSS */}
<section className="space-y-3">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] font-bold uppercase tracking-[0.14em] text-gray-500">
Copy real (Meta / Google / RRSS)
</p>
<button
type="button"
onClick={() => onEditSection('ads')}
className="text-[11px] font-semibold text-brand-700 hover:underline"
>
Ver en Ads →
</button>
</div>
{(book.winning_hooks || []).length ||
(book.ads_usps || []).length ||
(book.ads_ctas || []).length ? (
<div className="space-y-3 rounded-2xl border border-amber-200 bg-amber-50/40 px-4 py-4">
{(book.ads_ctas || []).length ? (
<p className="text-xs text-gray-700">
<span className="font-semibold">CTAs:</span>{' '}
{(book.ads_ctas || []).slice(0, 6).join(' · ')}
</p>
) : null}
{(book.ads_usps || []).length ? (
<PillRow
title="USPs en anuncios"
items={(book.ads_usps || []).slice(0, 6)}
emptyHint=""
accent="amber"
/>
) : null}
{(book.winning_hooks || []).length ? (
<ul className="space-y-1.5 text-xs text-gray-700 list-disc pl-4">
{(book.winning_hooks || []).slice(0, 5).map((h) => (
<li key={h.slice(0, 40)} className="leading-snug">
{h}
</li>
))}
</ul>
) : null}
</div>
) : (
<button
type="button"
onClick={() => onEditSection('ads')}
className="w-full rounded-xl border border-dashed border-gray-300 bg-gray-50 px-3 py-4 text-xs text-gray-500 hover:border-brand-300"
>
Sin copy de ads aún · Auto-rellenar marca o conectar Meta/Google
</button>
)}
</section>
</div>
);
const footer = (
<div
className={`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between ${
embedded
? 'border-t border-gray-100 pt-4'
: 'border-t border-gray-200 bg-gray-50 px-5 py-4 sm:px-6'
}`}
>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3">
<p className="text-xs font-semibold text-gray-800">
@@ -407,6 +482,24 @@ export default function BrandOverview({
<ArrowRight className="h-4 w-4" />
</button>
</div>
);
if (embedded) {
return (
<div className="space-y-6">
{header}
{content}
{footer}
</div>
);
}
return (
<div className="space-y-4">
<Surface className="!p-0 overflow-hidden border-gray-200">
{header}
{content}
{footer}
</Surface>
</div>
);