diff --git a/src/components/BrandOverview.tsx b/src/components/BrandOverview.tsx new file mode 100644 index 0000000..3a68c67 --- /dev/null +++ b/src/components/BrandOverview.tsx @@ -0,0 +1,413 @@ +'use client'; + +import React, { useEffect, useMemo, useState } from 'react'; +import { + ArrowRight, + CheckCircle2, + ExternalLink, + ImageIcon, + Palette, + Type, +} from 'lucide-react'; +import { Surface } from '@/components/ui'; +import { + primaryLogo, + type Brandbook, + type BrandbookSectionId, +} from '@/lib/brandbook'; + +function LogoPreview({ + 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 ( +
+ Cargando logo… +
+ ); + } + return ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ); +} + +function PillRow({ + title, + items, + emptyHint, + accent = 'brand', +}: { + title: string; + items: string[]; + emptyHint: string; + accent?: 'brand' | 'sky' | 'violet' | 'amber'; +}) { + const tone = + accent === 'sky' + ? 'border-sky-200 bg-sky-50 text-sky-900' + : accent === 'violet' + ? 'border-violet-200 bg-violet-50 text-violet-900' + : accent === 'amber' + ? 'border-amber-200 bg-amber-50 text-amber-900' + : 'border-brand-200 bg-brand-50 text-brand-900'; + + return ( +
+

+ {title} +

+ {items.length ? ( +
+ {items.map((item) => ( + + {item} + + ))} +
+ ) : ( +

{emptyHint}

+ )} +
+ ); +} + +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(/^\/+/, '')}`; + } + return null; +} + +export default function BrandOverview({ + book, + tenantLabel, + score, + missing, + onEditSection, + onContinueEdit, +}: { + book: Brandbook; + tenantLabel: string; + score: number; + missing: string[]; + onEditSection: (id: BrandbookSectionId) => void; + onContinueEdit: () => void; +}) { + const logo = primaryLogo(book); + const site = websiteFromBook(book); + const fonts = book.fonts_kit.slice(0, 4); + const colors = book.colors.slice(0, 8); + const values = book.values.filter(Boolean).slice(0, 8); + const aesthetic = [ + ...book.personality, + book.photography_style, + book.illustration_style, + book.visual_notes, + ] + .flatMap((x) => + String(x || '') + .split(/[,;·|/]/) + .map((s) => s.trim()) + .filter(Boolean) + ) + .filter((v, i, arr) => arr.indexOf(v) === i) + .slice(0, 8); + const tonePills = [ + book.tone, + ...book.voice_do.slice(0, 3), + ...book.personality.slice(0, 2), + ] + .map((s) => String(s || '').trim()) + .filter(Boolean) + .filter((v, i, arr) => arr.indexOf(v) === i) + .slice(0, 6); + + const overviewText = useMemo(() => { + return ( + book.elevator_pitch || + book.brand_promise || + book.positioning || + book.mission || + '' + ).trim(); + }, [book]); + + return ( +
+ +
+
+
+

+ Brand Overview +

+

+ Perfil de marca +

+

+ Resumen visual del brandbook · editable en Detalles del kit +

+
+ +
+
+ +
+ {/* Identity */} +
+
+

+ Identidad +

+

+ {book.brand_name || tenantLabel} +

+ {site ? ( + + {site.replace(/^https?:\/\//i, '')} + + + ) : ( + + )} +
+ {logo ? ( + + ) : ( + + )} +
+
+ +
+
+

+ Tipografías +

+ {fonts.length ? ( +
+ {fonts.map((f) => ( +
+

+ Aa +

+

+ {f.family} +

+

+ {f.role} +

+
+ ))} +
+ ) : ( + + )} +
+ +
+

+ Colores +

+ {colors.length ? ( +
+ {colors.map((c) => ( +
+ + + {c.hex} + +
+ ))} +
+ ) : ( + + )} +
+
+
+ + {/* Tagline */} +
+

+ Tagline +

+ {book.tagline ? ( +

+ {book.tagline} +

+ ) : ( + + )} +
+ +
+ + + +
+ + {/* Business overview */} +
+

+ Business overview +

+
+ {overviewText ? ( + overviewText + ) : ( + + )} +
+
+
+ + {/* Footer progress */} +
+
+
+

+ {score}% completo +

+ {score >= 100 ? ( + + Kit sólido + + ) : missing[0] ? ( +

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

+ ) : null} +
+
+
+
+
+ +
+ +
+ ); +} diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx index be3f828..157de9e 100644 --- a/src/components/BrandZone.tsx +++ b/src/components/BrandZone.tsx @@ -14,7 +14,7 @@ import { Type, Palette, } from 'lucide-react'; -import { PageHeader, Surface, SectionTitle } from '@/components/ui'; +import { PageHeader, Surface, SectionTitle, SegmentedControl } from '@/components/ui'; import { ASSET_KIND_LABELS, BRANDBOOK_SECTIONS, @@ -31,6 +31,7 @@ import { type FontToken, } from '@/lib/brandbook'; import GoogleFontsGallery from '@/components/GoogleFontsGallery'; +import BrandOverview from '@/components/BrandOverview'; function Field({ label, @@ -94,6 +95,7 @@ function KitThumb({ assetFileId, alt }: { assetFileId: string; alt: string }) { } export default function BrandZone({ tenantLabel }: { tenantLabel: string }) { + const [viewMode, setViewMode] = useState<'overview' | 'edit'>('overview'); const [section, setSection] = useState('identidad'); const [book, setBook] = useState(() => emptyBrandbook(tenantLabel)); const [slug, setSlug] = useState(''); @@ -311,7 +313,7 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
); }