308 lines
13 KiB
TypeScript
308 lines
13 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { Save, AlertCircle, Crown, FolderOpen, Sparkles, Image as ImageIcon, Film, Volume2, Music } from 'lucide-react';
|
|
import { CustomVideoPlayer } from './ui/CustomVideoPlayer';
|
|
import { useMediaResolver } from '../hooks/useMediaResolver';
|
|
import { DesignMD, CompanyProfile, BrandAsset } from '../types';
|
|
import { BrandTabGeneral } from './brand/BrandTabGeneral';
|
|
import { BrandTabVisual } from './brand/BrandTabVisual';
|
|
import { BrandTabTypography } from './brand/BrandTabTypography';
|
|
import { BrandTabMedia } from './brand/BrandTabMedia';
|
|
import { BrandTabVoice } from './brand/BrandTabVoice';
|
|
import { BrandPreview } from './brand/BrandPreview';
|
|
import { Toast } from './ui/Toast';
|
|
import { UnifiedMediaItem } from './content-grid/UnifiedMediaLibrary';
|
|
|
|
interface BrandArchitectureProps {
|
|
company: CompanyProfile;
|
|
handleCompanyChange: (company: CompanyProfile) => void;
|
|
designMD: DesignMD;
|
|
handleDesignChange: (key: keyof DesignMD, value: string | number | string[] | boolean) => void;
|
|
onContinue: () => void;
|
|
onEditAsset?: (type: keyof DesignMD, url: string) => void;
|
|
}
|
|
|
|
const TABS = [
|
|
{ id: 'general', label: 'Información', icon: '📋' },
|
|
{ id: 'visual', label: 'Visual y Colores', icon: '🎨' },
|
|
{ id: 'typography', label: 'Tipografía', icon: '🔤' },
|
|
{ id: 'voice', label: 'Voz de Marca', icon: '🎙️' },
|
|
{ id: 'media', label: 'Librería', icon: '📁' },
|
|
] as const;
|
|
|
|
type TabId = typeof TABS[number]['id'];
|
|
|
|
export const BrandArchitecture: React.FC<BrandArchitectureProps> = ({ company, handleCompanyChange, designMD, handleDesignChange, onContinue, onEditAsset }) => {
|
|
const [zoom, setZoom] = useState(1);
|
|
const [aspectRatio, setAspectRatio] = useState<'16:9'|'1:1'|'9:16'>('9:16');
|
|
const [activeTab, setActiveTab] = useState<TabId>('general');
|
|
const [showToast, setShowToast] = useState(false);
|
|
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
const [selectedMediaItem, setSelectedMediaItem] = useState<BrandAsset | null>(null);
|
|
|
|
const { getMediaUrl } = useMediaResolver();
|
|
const validate = useCallback((): string[] => {
|
|
const errors: string[] = [];
|
|
if (!company?.name || company.name.trim().length < 2) {
|
|
errors.push('El nombre de la marca es requerido (mín. 2 caracteres)');
|
|
}
|
|
if (!designMD.logoUrl || designMD.logoUrl.trim().length === 0) {
|
|
errors.push('El logo de la marca es requerido');
|
|
}
|
|
return errors;
|
|
}, [company, designMD]);
|
|
|
|
const handleSave = () => {
|
|
const errors = validate();
|
|
if (errors.length > 0) {
|
|
setValidationErrors(errors);
|
|
setTimeout(() => setValidationErrors([]), 5000);
|
|
return;
|
|
}
|
|
setValidationErrors([]);
|
|
setShowToast(true);
|
|
setTimeout(() => {
|
|
onContinue();
|
|
}, 800);
|
|
};
|
|
|
|
|
|
const handleOpenFolder = async () => {
|
|
if (window.electronAPI && company?.id) {
|
|
const wp = await window.electronAPI.fs.getWorkspacePath();
|
|
const folderPath = `${wp}/${company.id}`;
|
|
await window.electronAPI.fs.openFolder(folderPath);
|
|
}
|
|
};
|
|
return (
|
|
<div className="flex-1 flex flex-col w-full overflow-hidden">
|
|
{/* ═══ Sticky Header: Title + Brand Identity ═══ */}
|
|
<div className="shrink-0 sticky top-0 z-20 bg-neutral-950/95 backdrop-blur-md border-b border-neutral-800/60">
|
|
<div className="px-8 pt-6 pb-4">
|
|
<div className="flex items-start justify-between gap-6">
|
|
{/* Left: Title + Description */}
|
|
<div className="min-w-0">
|
|
<h2 className="text-xl font-bold text-white tracking-tight">Reglas de tu Marca (Design MD)</h2>
|
|
<p className="text-sm text-neutral-400 leading-relaxed mt-1">
|
|
Establece el plano arquitectónico visual de la empresa. Todos los videos y renders
|
|
futuros adoptarán estrictamente estos parámetros sin intervención de IA.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Right: Brand Identity Card + Save */}
|
|
<div className="shrink-0 flex items-center gap-3">
|
|
{/* Brand Identity Card */}
|
|
<div className="flex items-center gap-3 bg-neutral-900/80 border border-neutral-800 rounded-xl px-4 py-2.5 backdrop-blur-sm">
|
|
{/* Logo / Avatar */}
|
|
<div className="w-9 h-9 rounded-lg overflow-hidden bg-neutral-800 border border-neutral-700 flex items-center justify-center shrink-0">
|
|
{designMD.logoUrl ? (
|
|
<img
|
|
src={designMD.logoUrl}
|
|
alt={company.name}
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
) : (
|
|
<span className="text-lg font-bold text-neutral-500">
|
|
{company.name?.charAt(0)?.toUpperCase() || '?'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{/* Name + Plan */}
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-semibold text-white truncate max-w-[140px]">
|
|
{company.name || 'Sin nombre'}
|
|
</p>
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<Crown size={10} className="text-amber-400 shrink-0" />
|
|
<span className="text-[10px] font-medium text-amber-400/80 tracking-wide uppercase">
|
|
{company.industry || 'Marca'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/* Brand color dot indicator */}
|
|
<div className="flex flex-col gap-1 ml-2">
|
|
<div
|
|
className="w-3 h-3 rounded-full border border-white/10 shadow-sm"
|
|
style={{ backgroundColor: designMD.primaryColor }}
|
|
title={`Primario: ${designMD.primaryColor}`}
|
|
/>
|
|
<div
|
|
className="w-3 h-3 rounded-full border border-white/10 shadow-sm"
|
|
style={{ backgroundColor: designMD.secondaryColor }}
|
|
title={`Secundario: ${designMD.secondaryColor}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleOpenFolder}
|
|
title="Abrir carpeta local"
|
|
className="flex items-center gap-2 px-3 py-2.5 rounded-xl bg-neutral-800/80 hover:bg-neutral-700/80 border border-neutral-700/50 text-neutral-300 text-sm font-medium transition-all"
|
|
>
|
|
<FolderOpen size={16} />
|
|
</button>
|
|
|
|
{/* Save Button */}
|
|
<button
|
|
onClick={handleSave}
|
|
title="Guardar marca"
|
|
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white text-sm font-semibold transition-all shadow-lg shadow-emerald-900/30"
|
|
>
|
|
<Save size={14} />
|
|
Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ═══ Full-Width Tabbar ═══ */}
|
|
<div className="px-8 pb-0">
|
|
<div className="flex bg-neutral-900/60 border border-neutral-800 rounded-t-xl overflow-hidden">
|
|
{TABS.map((tab, idx) => {
|
|
const isActive = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex-1 flex items-center justify-center gap-2 px-3 py-3 text-[13px] font-medium transition-all relative ${
|
|
isActive
|
|
? 'bg-neutral-800/80 text-white'
|
|
: 'text-neutral-500 hover:text-neutral-200 hover:bg-neutral-800/30'
|
|
} ${idx > 0 ? 'border-l border-neutral-800/50' : ''}`}
|
|
>
|
|
<span className="text-sm">{tab.icon}</span>
|
|
<span className="hidden xl:inline">{tab.label}</span>
|
|
<span className="xl:hidden text-xs">{tab.label.split(' ')[0]}</span>
|
|
{/* Active indicator bar */}
|
|
{isActive && (
|
|
<div className="absolute bottom-0 left-2 right-2 h-0.5 rounded-full bg-gradient-to-r from-violet-500 to-fuchsia-500" />
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ═══ Main Content: Form + Preview Split ═══ */}
|
|
<div className="flex-1 flex overflow-hidden min-h-0">
|
|
{/* Form Column */}
|
|
<div className="w-1/2 overflow-y-auto border-r border-neutral-800 bg-neutral-950/80 backdrop-blur-sm">
|
|
<div className="max-w-xl mx-auto p-8 space-y-6">
|
|
{/* Validation Errors */}
|
|
{validationErrors.length > 0 && (
|
|
<div className="bg-rose-950/30 border border-rose-800/50 rounded-xl p-4 space-y-1.5">
|
|
{validationErrors.map((err, i) => (
|
|
<p key={i} className="text-sm text-rose-300 flex items-center gap-2">
|
|
<AlertCircle size={14} className="shrink-0" />
|
|
{err}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'general' && (
|
|
<BrandTabGeneral company={company} handleCompanyChange={handleCompanyChange} />
|
|
)}
|
|
{activeTab === 'visual' && (
|
|
<BrandTabVisual
|
|
company={company}
|
|
designMD={designMD}
|
|
handleDesignChange={handleDesignChange}
|
|
onEditAsset={onEditAsset}
|
|
/>
|
|
)}
|
|
{activeTab === 'typography' && (
|
|
<BrandTabTypography designMD={designMD} handleDesignChange={handleDesignChange} />
|
|
)}
|
|
{activeTab === 'voice' && (
|
|
<BrandTabVoice company={company} handleCompanyChange={handleCompanyChange} />
|
|
)}
|
|
{activeTab === 'media' && (
|
|
<BrandTabMedia
|
|
company={company}
|
|
designMD={designMD}
|
|
handleDesignChange={handleDesignChange}
|
|
onEditAsset={onEditAsset}
|
|
onSelectAsset={setSelectedMediaItem}
|
|
selectedAssetId={selectedMediaItem?.id}
|
|
/>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preview Column */}
|
|
{activeTab === 'media' ? (
|
|
<div className="flex-1 bg-neutral-950 flex flex-col items-center justify-center p-8">
|
|
{selectedMediaItem ? (
|
|
<div className="w-full h-full flex flex-col items-center justify-center">
|
|
<div className="w-full max-h-[80%] flex items-center justify-center bg-black/40 rounded-xl overflow-hidden border border-neutral-800 shadow-2xl">
|
|
{selectedMediaItem.type === 'video' ? (
|
|
<CustomVideoPlayer
|
|
src={getMediaUrl(selectedMediaItem.url || selectedMediaItem.path || '')}
|
|
autoPlay
|
|
className="w-full h-full"
|
|
/>
|
|
) : selectedMediaItem.type === 'audio' ? (
|
|
<div className="flex flex-col items-center gap-6 p-12">
|
|
<div className="w-24 h-24 rounded-full bg-rose-500/10 flex items-center justify-center">
|
|
<Music className="w-12 h-12 text-rose-500" />
|
|
</div>
|
|
<audio
|
|
src={getMediaUrl(selectedMediaItem.url || selectedMediaItem.path || '')}
|
|
controls
|
|
autoPlay
|
|
className="w-64"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<img
|
|
src={getMediaUrl(selectedMediaItem.url || selectedMediaItem.path || '')}
|
|
alt={selectedMediaItem.id}
|
|
className="max-w-full max-h-full object-contain"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="mt-6 text-center space-y-1">
|
|
<h3 className="text-lg font-medium text-white font-mono">
|
|
{selectedMediaItem.id}
|
|
</h3>
|
|
<p className="text-sm text-neutral-400">
|
|
{'date' in selectedMediaItem && selectedMediaItem.date ? new Date((selectedMediaItem as any).date).toLocaleString() : 'Asset Base'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center text-neutral-500">
|
|
<Sparkles className="w-12 h-12 mb-4 opacity-50" />
|
|
<p>Selecciona un archivo para previsualizarlo.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<BrandPreview
|
|
designMD={designMD}
|
|
company={company}
|
|
activeTab={activeTab}
|
|
zoom={zoom}
|
|
setZoom={setZoom}
|
|
aspectRatio={aspectRatio}
|
|
setAspectRatio={setAspectRatio}
|
|
handleDesignChange={handleDesignChange}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Success Toast */}
|
|
{showToast && (
|
|
<Toast
|
|
message="Marca guardada exitosamente ✓"
|
|
type="success"
|
|
onDismiss={() => setShowToast(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|