import React, { useState, useCallback, useMemo } from 'react'; import { DesignMD, TimelineElement, TimelineLayer, CompanyProfile, Project, ContentPiece, ContentPillar, ExpressTemplate } from './types'; import { TopHeader } from './components/TopHeader'; import { BrandArchitecture } from './components/BrandArchitecture'; import { Dashboard } from './components/Dashboard'; import { ProductionForm } from './components/dashboard/ProductionForm'; import { StudioEditor } from './components/studio/StudioEditor'; import { ExpressEditor } from './components/express/ExpressEditor'; import { StudioTopBar } from './components/studio/StudioTopBar'; import { EditorProvider, useEditor } from './context/EditorContext'; import { DEFAULT_DESIGN_MD, PREDEFINED_COMPANIES, DEFAULT_PILLARS } from './data/defaults'; import { useCustomTooltips } from './hooks/useCustomTooltips'; import { ToastProvider } from './components/ui/ToastProvider'; import { usePersistence, loadCompanies, useTemplatePersistence, loadTemplates } from './hooks/usePersistence'; import { ContentGridView } from './components/content-grid/ContentGridView'; import { TemplateBuilder } from './components/express/builder/TemplateBuilder'; import { EXPRESS_TEMPLATES } from './config/expressTemplates'; import { compileExpressToTimeline } from './utils/expressCompiler'; import { FullscreenToggle } from './components/ui/FullscreenToggle'; type Step = 'dashboard' | 'brand' | 'studio' | 'express' | 'content-grid' | 'template-builder' | 'production-form'; // ── Content persistence ── const CONTENT_STORAGE_KEY = 'remix-content-data'; function loadContentData(): Record | null { try { const raw = localStorage.getItem(CONTENT_STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } function saveContentData(data: Record): void { try { localStorage.setItem(CONTENT_STORAGE_KEY, JSON.stringify(data)); } catch {} } export default function App() { const [companies, setCompanies] = useState(() => { return loadCompanies() ?? PREDEFINED_COMPANIES; }); const [currentCompanyId, setCurrentCompanyId] = useState(null); const [currentProjectId, setCurrentProjectId] = useState(null); const [currentStep, setCurrentStep] = useState('dashboard'); const [designMD, setDesignMD] = useState(DEFAULT_DESIGN_MD); const [outputFormat, setOutputFormat] = useState<'video' | 'image'>('video'); // Global templates (decoupled from brands) — persisted const [globalTemplates, setGlobalTemplates] = useState(() => { return loadTemplates() ?? []; }); const [templateBuilderFormat, setTemplateBuilderFormat] = useState<'video' | 'image'>('image'); const [templateBuilderAspect, setTemplateBuilderAspect] = useState('9:16'); const [editingGlobalTemplate, setEditingGlobalTemplate] = useState(null); // Production form state const [productionTemplate, setProductionTemplate] = useState(null); const [productionBrand, setProductionBrand] = useState(null); // Merge preset + custom templates for the dashboard const allTemplates = useMemo(() => [ ...EXPRESS_TEMPLATES, ...globalTemplates, ], [globalTemplates]); const handleSaveGlobalTemplate = useCallback((template: ExpressTemplate) => { setGlobalTemplates(prev => { const existing = prev.findIndex(t => t.id === template.id); if (existing >= 0) { const next = [...prev]; next[existing] = template; return next; } return [...prev, template]; }); setEditingGlobalTemplate(null); setCurrentStep('dashboard'); }, []); // Content grid state (per company) const [contentData, setContentData] = useState>(() => { return loadContentData() ?? {}; }); const getContentForCompany = useCallback((companyId: string) => { return contentData[companyId] ?? { pieces: [], pillars: [...DEFAULT_PILLARS] }; }, [contentData]); const updateContentPieces = useCallback((companyId: string, pieces: ContentPiece[]) => { setContentData(prev => { const next = { ...prev, [companyId]: { ...prev[companyId] ?? { pillars: [...DEFAULT_PILLARS] }, pieces } }; saveContentData(next); return next; }); }, []); const updateContentPillars = useCallback((companyId: string, pillars: ContentPillar[]) => { setContentData(prev => { const next = { ...prev, [companyId]: { ...prev[companyId] ?? { pieces: [] }, pillars } }; saveContentData(next); return next; }); }, []); // Studio initial data (passed to EditorProvider when entering studio) const [studioInitialElements, setStudioInitialElements] = useState([]); const [studioInitialLayers, setStudioInitialLayers] = useState([ { id: 'layer-1', name: 'Capa Gráfica 1', type: 'visual' } ]); // Key to force remount EditorProvider when switching projects const [editorKey, setEditorKey] = useState(0); useCustomTooltips(); usePersistence(companies); useTemplatePersistence(globalTemplates); const handleDesignChange = (key: keyof DesignMD, value: string | number | string[] | boolean) => { setDesignMD((prev) => { const newDesign = { ...prev, [key]: value }; if (currentCompanyId) { setCompanies(prev2 => prev2.map(c => c.id === currentCompanyId ? { ...c, design: newDesign } : c)); } return newDesign; }); }; const saveCurrentProject = (elements: TimelineElement[], layers: TimelineLayer[]) => { if (currentCompanyId) { setCompanies(prev => prev.map(c => { if (c.id !== currentCompanyId) return c; const projs = c.projects || []; if (currentProjectId) { return { ...c, projects: projs.map(p => p.id === currentProjectId ? { ...p, elements, layers } : p) }; } else { const newId = `proj-${Date.now()}`; const newProject: Project = { id: newId, name: `Proyecto ${outputFormat === 'video' ? 'Video' : 'Imagen'} ${projs.length + 1}`, format: outputFormat, elements, layers }; setCurrentProjectId(newId); return { ...c, projects: [...projs, newProject] }; } })); } }; const enterStudio = (design: DesignMD, format: 'video' | 'image', elements: TimelineElement[], layers: TimelineLayer[], companyId?: string, projectId?: string | null) => { if (companyId) setCurrentCompanyId(companyId); if (projectId !== undefined) setCurrentProjectId(projectId); setDesignMD(design); setOutputFormat(format); setStudioInitialElements(elements); setStudioInitialLayers(layers); setEditorKey(prev => prev + 1); setCurrentStep('studio'); }; // ── Blank canvas editors (no brand) ── const handleStartExpressBlank = useCallback(() => { setCurrentCompanyId(null); setDesignMD(DEFAULT_DESIGN_MD); setOutputFormat('video'); setCurrentStep('express'); }, []); const handleStartProBlank = useCallback(() => { const initialElements: TimelineElement[] = [{ id: `el-content-${Date.now()}`, layerId: 'layer-1', type: 'text', content: 'Inserta tu contenido aquí', startFrame: 0, endFrame: 180, x: 50, y: 50, fontSize: 48, color: '#FFFFFF', fontFamily: DEFAULT_DESIGN_MD.baseFont, }]; const initialLayers: TimelineLayer[] = [{ id: 'layer-1', name: 'Capa Gráfica 1', type: 'visual' }]; enterStudio(DEFAULT_DESIGN_MD, 'video', initialElements, initialLayers, undefined, null); }, []); // ── Production flow: template × brand → form → editor ── const handleGenerate = useCallback((template: ExpressTemplate, brand: CompanyProfile) => { setProductionTemplate(template); setProductionBrand(brand); setCurrentStep('production-form'); }, []); // ── Template management (edit / duplicate / delete) ── const handleEditTemplate = useCallback((template: ExpressTemplate) => { setEditingGlobalTemplate(template); setTemplateBuilderFormat(template.format); setCurrentStep('template-builder'); }, []); const handleDuplicateTemplate = useCallback((template: ExpressTemplate) => { const copy: ExpressTemplate = { ...template, id: `tpl-${Date.now()}`, name: `${template.name} (Copia)`, isCustom: true, createdAt: new Date().toISOString(), scenes: template.scenes.map(s => ({ ...s, id: `scene-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` })), }; setGlobalTemplates(prev => [...prev, copy]); }, []); const handleDeleteTemplate = useCallback((id: string) => { setGlobalTemplates(prev => prev.filter(t => t.id !== id)); }, []); const handleProducePro = useCallback((fieldData: Record) => { if (!productionTemplate || !productionBrand) return; // Compile template + brand + fieldData → TimelineElement[] const compiled = compileExpressToTimeline(productionTemplate, fieldData, productionBrand.design, productionBrand); enterStudio(productionBrand.design, productionTemplate.format, compiled.elements, compiled.layers, productionBrand.id, null); }, [productionTemplate, productionBrand]); return (
{currentStep !== 'studio' && ( { setCurrentStep(step); }} outputFormat={outputFormat} onStartExpressBlank={handleStartExpressBlank} onStartProBlank={handleStartProBlank} /> )}
{currentStep === 'dashboard' && ( { const newAppId = Date.now().toString(); const newBrand: CompanyProfile = { id: newAppId, name, industry, design: { ...DEFAULT_DESIGN_MD }, projects: [] }; setCompanies(prev => [...prev, newBrand]); setCurrentCompanyId(newAppId); setDesignMD(newBrand.design); setCurrentStep('brand'); }} onDeleteBrand={(id) => { setCompanies(prev => prev.filter(c => c.id !== id)); }} onDuplicateBrand={(id) => { const original = companies.find(c => c.id === id); if (!original) return; const newId = Date.now().toString(); const duplicate: CompanyProfile = { ...original, id: newId, name: `${original.name} (Copia)`, projects: [], design: { ...original.design, brandStickers: [...(original.design.brandStickers || [])] }, socialLinks: original.socialLinks ? { ...original.socialLinks } : undefined, }; setCompanies(prev => [...prev, duplicate]); }} onEditBrand={(design) => { const comp = companies.find(c => c.design === design); if (comp) setCurrentCompanyId(comp.id); setDesignMD(design); setCurrentStep('brand'); }} onOpenContentGrid={(companyId) => { setCurrentCompanyId(companyId); setCurrentStep('content-grid'); }} onCreateTemplate={(format, aspect) => { setTemplateBuilderFormat(format); setTemplateBuilderAspect(aspect); setEditingGlobalTemplate(null); setCurrentStep('template-builder'); }} onEditTemplate={handleEditTemplate} onDuplicateTemplate={handleDuplicateTemplate} onDeleteTemplate={handleDeleteTemplate} onGenerate={handleGenerate} /> )} {currentStep === 'production-form' && productionTemplate && productionBrand && ( setCurrentStep('dashboard')} onProducePro={handleProducePro} /> )} {currentStep === 'brand' && ( c.id === currentCompanyId)!} handleCompanyChange={(company) => { setCompanies(prev => prev.map(c => c.id === company.id ? company : c)); }} designMD={designMD} handleDesignChange={handleDesignChange} onContinue={() => setCurrentStep('dashboard')} /> )} {currentStep === 'content-grid' && currentCompanyId && ( c.id === currentCompanyId)!} pieces={getContentForCompany(currentCompanyId).pieces} pillars={getContentForCompany(currentCompanyId).pillars} onPiecesChange={(pieces) => updateContentPieces(currentCompanyId, pieces)} onPillarsChange={(pillars) => updateContentPillars(currentCompanyId, pillars)} onOpenProject={(projectId) => { const comp = companies.find(c => c.id === currentCompanyId); if (comp) { const proj = comp.projects.find(p => p.id === projectId); if (proj) { const layers = proj.layers.length > 0 ? proj.layers : [{ id: 'layer-1', name: 'Capa Gráfica 1', type: 'visual' as const }]; enterStudio(comp.design, proj.format, proj.elements, layers, comp.id, projectId); } } }} /> )} {currentStep === 'express' && ( c.id === currentCompanyId)} onBack={() => setCurrentStep('dashboard')} onUpgradeToPro={(elements, layers) => { const comp = companies.find(c => c.id === currentCompanyId); enterStudio(designMD, outputFormat, elements, layers, comp?.id, null); }} onExport={(elements, layers, format) => { const comp = companies.find(c => c.id === currentCompanyId); enterStudio(designMD, format, elements, layers, comp?.id, null); }} /> )} {currentStep === 'studio' && ( c.id === currentCompanyId)?.brandContent} >
)} {currentStep === 'template-builder' && ( setCurrentStep('dashboard')} editingTemplate={editingGlobalTemplate} initialFormat={templateBuilderFormat} initialAspect={templateBuilderAspect} /> )}
); }