'use client'; import React, { useCallback, useEffect, useState } from 'react'; import { BookOpen, Loader2, Save, Sparkles, Upload, CheckCircle2, Plus, Trash2, ImageIcon, Type, Palette, } from 'lucide-react'; import { PageHeader, Surface, SectionTitle, SegmentedControl } from '@/components/ui'; import { ASSET_KIND_LABELS, BRANDBOOK_SECTIONS, emptyBrandbook, linesToList, listToLines, newId, normalizeHex, primaryLogo, type BrandAssetKind, type Brandbook, type BrandbookSectionId, type ColorToken, type FontToken, type CreativeB2BPersona, type CreativeB2CPersona, type CreativeObjection, } from '@/lib/brandbook'; import GoogleFontsGallery from '@/components/GoogleFontsGallery'; import BrandOverview from '@/components/BrandOverview'; function Field({ label, hint, children, className = '', }: { label: string; hint?: string; children: React.ReactNode; className?: string; }) { return ( ); } const inputCls = 'w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-brand-200'; const areaCls = `${inputCls} min-h-[88px] resize-y`; const selectCls = inputCls; function KitThumb({ assetFileId, alt }: { assetFileId: string; alt: string }) { const [url, setUrl] = useState(null); useEffect(() => { let cancelled = false; (async () => { const r = await fetch( `/api/assets/download?id=${encodeURIComponent(assetFileId)}` ); const j = await r.json(); if (!cancelled && r.ok && j.url) setUrl(j.url); })(); return () => { cancelled = true; }; }, [assetFileId]); if (!url) { return (
…
); } return ( // eslint-disable-next-line @next/next/no-img-element {alt} ); } export default function BrandZone({ tenantId, tenantLabel, tenantSlug, }: { tenantId?: string; tenantLabel: string; tenantSlug?: string; }) { const [viewMode, setViewMode] = useState<'overview' | 'edit'>('overview'); const [section, setSection] = useState('identidad'); const [book, setBook] = useState(() => emptyBrandbook(tenantLabel)); const [slug, setSlug] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [seeding, setSeeding] = useState(false); const [uploading, setUploading] = useState(false); const [kitUploading, setKitUploading] = useState(false); const [err, setErr] = useState(null); const [okMsg, setOkMsg] = useState(null); const [score, setScore] = useState(0); const [missing, setMissing] = useState([]); const [paste, setPaste] = useState(''); const [exists, setExists] = useState(false); const [kitKind, setKitKind] = useState('logo'); const [kitRole, setKitRole] = useState('primary'); const load = useCallback(async () => { setLoading(true); setErr(null); try { const query = tenantId ? `?tenant_id=${encodeURIComponent(tenantId)}` : ''; const r = await fetch(`/api/brandbook${query}`); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo cargar'); setBook(j.brandbook || emptyBrandbook(tenantLabel)); setSlug(j.slug || tenantSlug || ''); setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setExists(!!j.exists); } catch (e) { setErr(e instanceof Error ? e.message : 'Error'); } finally { setLoading(false); } }, [tenantId, tenantLabel, tenantSlug]); useEffect(() => { void load(); }, [load]); function patch(partial: Partial) { setBook((b) => ({ ...b, ...partial })); setOkMsg(null); } async function save() { setSaving(true); setErr(null); setOkMsg(null); try { const query = tenantId ? `?tenant_id=${encodeURIComponent(tenantId)}` : ''; const r = await fetch(`/api/brandbook${query}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ brandbook: book, tenant_id: tenantId }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo guardar'); setBook(j.brandbook); setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setExists(true); setOkMsg('Brand kit guardado y sincronizado'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error al guardar'); } finally { setSaving(false); } } async function seed() { setSeeding(true); setErr(null); try { const query = tenantId ? `?tenant_id=${encodeURIComponent(tenantId)}` : ''; const r = await fetch(`/api/brandbook${query}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seed: true, tenant_id: tenantId }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo sembrar'); setBook(j.brandbook); setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setExists(true); setOkMsg('Base experta creada — sube logos/fuentes y completa huecos'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error'); } finally { setSeeding(false); } } async function uploadSource(file: File | null) { if (!file && !paste.trim()) return; setUploading(true); setErr(null); try { const fd = new FormData(); if (file) fd.append('file', file); if (paste.trim()) { fd.append('text', paste); fd.append('filename', 'pegado.txt'); } const r = await fetch('/api/brandbook/upload', { method: 'POST', body: fd }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'Upload falló'); setBook(j.brandbook); setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setExists(true); setPaste(''); setOkMsg(`Fuente añadida: ${j.ingested}`); setSection('fuentes'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error upload'); } finally { setUploading(false); } } async function uploadKit(file: File) { setKitUploading(true); setErr(null); try { const fd = new FormData(); fd.append('file', file); fd.append('kind', kitKind); fd.append('role', kitRole); fd.append('label', file.name); const r = await fetch('/api/brandbook/kit', { method: 'POST', body: fd }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo subir al kit'); setBook(j.brandbook); setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setExists(true); setOkMsg(`${ASSET_KIND_LABELS[kitKind]} añadido: ${file.name}`); setSection('visual'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error kit'); } finally { setKitUploading(false); } } async function removeKit(kitId: string) { setErr(null); try { const r = await fetch( `/api/brandbook/kit?kitId=${encodeURIComponent(kitId)}`, { method: 'DELETE' } ); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo quitar'); setBook(j.brandbook); setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setOkMsg('Archivo quitado del kit'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error'); } } function addColor() { const c: ColorToken = { id: newId('col'), name: 'Nuevo color', hex: '#2071CF', role: 'primary', usage: '', }; patch({ colors: [...book.colors, c] }); } function updateColor(id: string, partial: Partial) { patch({ colors: book.colors.map((c) => (c.id === id ? { ...c, ...partial } : c)), }); } function addFont() { const f: FontToken = { id: newId('font'), family: 'Nueva fuente', role: 'body', weights: '400,700', source: 'google', url_or_notes: '', }; patch({ fonts_kit: [...book.fonts_kit, f] }); } function updateFont(id: string, partial: Partial) { patch({ fonts_kit: book.fonts_kit.map((f) => f.id === id ? { ...f, ...partial } : f ), }); } const logo = primaryLogo(book); const secMeta = BRANDBOOK_SECTIONS.find((s) => s.id === section); if (loading) { return (
Cargando zona de marca…
); } return (
} /> {err ? (

{err}

) : null} {okMsg ? (

{okMsg}

) : null} {viewMode === 'overview' ? ( setViewMode('edit')} onEditSection={(id) => { setSection(id); setViewMode('edit'); }} /> ) : null} {viewMode === 'edit' ? ( <> {/* Identity strip */}
{logo ? ( ) : (
Sin logo
)}

{book.brand_name || tenantLabel}

{book.tagline || book.elevator_pitch || 'Sin tagline todavía'}

{book.colors.slice(0, 8).map((c) => ( {c.hex} ))} {book.fonts_kit.slice(0, 3).map((f) => ( {f.family} ))}

Completitud

{score}%

{missing.length ? (

Falta: {missing.slice(0, 3).join(' · ')}

) : (

Kit sólido

)}
{/* Quick upload bar */}
Añadir al kit
setKitRole(e.target.value)} />

Logos, tipografías, patrones, iconos, PDFs de brandbook, moodboards…

{section === 'identidad' && (
patch({ brand_name: e.target.value })} /> patch({ tagline: e.target.value })} />