import React, { useState, useCallback, useMemo } from 'react'; import { ArrowLeft, Save, Video, Image as ImageIcon, Eye, FileText, Hash, Briefcase, FlaskConical, Zap, } from 'lucide-react'; import { ExpressTemplate, ExpressScene, DesignMD, CompanyProfile, TemplateField, ExpressField, } from '../../../types'; import { TemplateBuilderProvider, useTemplateBuilder, useSceneFieldsMap, TemplateMeta, migrateExpressFields, } from '../../../context/TemplateBuilderContext'; import { FieldSchemaPanel } from './FieldSchemaPanel'; import { FieldConfigPanel } from './FieldConfigPanel'; import { BuilderCanvas } from './BuilderCanvas'; import { FormPreviewPanel } from './FormPreviewPanel'; import { SceneComposer } from './SceneComposer'; import { TemplateFieldInput } from '../../shared/TemplateFieldInput'; import { LivePreviewCanvas } from '../../shared/LivePreviewCanvas'; interface TemplateBuilderProps { company?: CompanyProfile; designMD?: DesignMD; availableBrands?: CompanyProfile[]; onSave: (template: ExpressTemplate) => void; onBack: () => void; editingTemplate?: ExpressTemplate | null; initialFormat?: 'video' | 'image'; initialAspect?: ExpressTemplate['aspectRatio']; } const CATEGORIES: { value: ExpressTemplate['category']; label: string; icon: string }[] = [ { value: 'social', label: 'Social', icon: 'πŸ“±' }, { value: 'ad', label: 'Publicidad', icon: '🎯' }, { value: 'promo', label: 'Promo', icon: 'πŸš€' }, { value: 'story', label: 'Historia', icon: 'πŸ’¬' }, { value: 'announcement', label: 'Anuncio', icon: 'πŸ“’' }, ]; function createDefaultScene(format: 'video' | 'image'): ExpressScene { const bgType = format === 'video' ? 'video' : 'image'; const bgLabel = format === 'video' ? 'Video de fondo' : 'Imagen de fondo'; const now = Date.now(); return { id: `scene-${now}`, type: 'content', name: 'Nueva Escena', durationSeconds: 5, layout: 'overlay', editableFields: [], fields: [ // Background β€” always index 0 (bottom z-index) { id: `field-bg-${now}`, nature: 'editable-slot' as const, type: bgType, label: bgLabel, required: true, content: bgLabel, position: { x: 50, y: 50, w: 100, h: 100 }, style: { opacity: 100 }, formOrder: 0, isBackground: true, }, // Title β€” on top { id: `field-title-${now + 1}`, nature: 'editable-slot' as const, type: 'text' as const, label: 'TΓ­tulo', required: true, content: 'Escribe aquΓ­', position: { x: 50, y: 45, w: 80, h: 15 }, style: { fontSize: 36, fontWeight: 700, textAlign: 'center' as const, opacity: 100 }, formOrder: 1, }, ], background: { type: 'brand' }, transition: { type: 'fade', duration: 10 }, }; } /** * TemplateBuilder β€” Redesigned visual template editor. * * Uses TemplateBuilderContext instead of EditorProvider. * Layout: FieldSchemaPanel (left) | Canvas or FormPreview (center) | FieldConfigPanel (right) */ export const TemplateBuilder: React.FC = (props) => { const format = props.editingTemplate?.format || props.initialFormat || 'video'; const initialScenes = useMemo(() => { if (props.editingTemplate?.scenes?.length) return props.editingTemplate.scenes; return [createDefaultScene(format)]; }, []); const initialMeta: TemplateMeta = useMemo(() => ({ name: props.editingTemplate?.name || '', description: props.editingTemplate?.description || '', category: props.editingTemplate?.category || 'social', aspectRatio: props.editingTemplate?.aspectRatio || props.initialAspect || '9:16', format, usesBrandAudio: props.editingTemplate?.usesBrandAudio ?? true, }), []); return ( ); }; /* ═══════════════════════════════════════════════════════════════ * Inner component β€” lives inside TemplateBuilderProvider * ═══════════════════════════════════════════════════════════════ */ interface InnerProps { onSave: (template: ExpressTemplate) => void; onBack: () => void; editingTemplate?: ExpressTemplate | null; } const TemplateBuilderInner: React.FC = ({ onSave, onBack, editingTemplate, }) => { const { scenes, setScenes, activeSceneId, setActiveSceneId, activeScene, viewMode, setViewMode, templateMeta, setTemplateMeta, editableSlotCount, totalFieldCount, selectedFieldId, previewBrand, setPreviewBrand, availableBrands, resolvedDesignMD, resolvedCompany, fields, testFieldData, setTestFieldData, testMediaFits, setTestMediaFits, testContainBgColors, setTestContainBgColors, // Segment management addSegment, removeSegment, updateSegment, introScene, outroScene, } = useTemplateBuilder(); const sceneFieldsMap = useSceneFieldsMap(); const [nameError, setNameError] = useState(false); // ── Scene callbacks ── const handleAddScene = useCallback(() => { const newScene = createDefaultScene(templateMeta.format); setScenes(prev => [...prev, newScene]); setActiveSceneId(newScene.id); }, [setScenes, setActiveSceneId, templateMeta.format]); const handleRemoveScene = useCallback((sceneId: string) => { setScenes(prev => { const next = prev.filter(s => s.id !== sceneId); if (activeSceneId === sceneId) { setActiveSceneId(next[0]?.id || null); } return next; }); }, [activeSceneId, setScenes, setActiveSceneId]); const handleUpdateScene = useCallback((updated: ExpressScene) => { setScenes(prev => prev.map(s => s.id === updated.id ? updated : s)); }, [setScenes]); // ── Save ── const handleSave = useCallback(() => { if (!templateMeta.name.trim()) { setNameError(true); setTimeout(() => setNameError(false), 3000); return; } // Convert all scene fields back to ExpressField format for backward compat const updatedScenes = scenes.map(scene => { const templateFields = sceneFieldsMap[scene.id] || []; return { ...scene, fields: templateFields, editableFields: templateFieldsToExpressFields(templateFields), }; }); const template: ExpressTemplate = { id: editingTemplate?.id || `tpl-${Date.now()}`, name: templateMeta.name, description: templateMeta.description, category: templateMeta.category, icon: CATEGORIES.find(c => c.value === templateMeta.category)?.icon || 'πŸ“', aspectRatio: templateMeta.aspectRatio, format: templateMeta.format, scenes: updatedScenes, usesBrandAudio: templateMeta.format === 'video', isCustom: true, createdAt: editingTemplate?.createdAt || new Date().toISOString(), }; onSave(template); }, [templateMeta, scenes, sceneFieldsMap, editingTemplate, onSave]); // ── Build a temporary ExpressTemplate from current state (for LivePreviewCanvas) ── const buildCurrentTemplate = useCallback((): ExpressTemplate => { const updatedScenes = scenes.map(scene => { const templateFields = sceneFieldsMap[scene.id] || []; return { ...scene, fields: templateFields, editableFields: templateFieldsToExpressFields(templateFields), }; }); return { id: editingTemplate?.id || 'tpl-preview', name: templateMeta.name || 'Preview', description: templateMeta.description, category: templateMeta.category, icon: CATEGORIES.find(c => c.value === templateMeta.category)?.icon || 'πŸ“', aspectRatio: templateMeta.aspectRatio, format: templateMeta.format, scenes: updatedScenes, usesBrandAudio: false, isCustom: true, }; }, [templateMeta, scenes, sceneFieldsMap, editingTemplate]); return (
{/* ── Left: Field Schema Panel (full height) ── */} {/* ── Center: Canvas + Scene Composer ── */}
{/* Top bar β€” all metadata inline */}
{/* Left: back */} {/* Divider */}
{/* Name β€” inline editable */} { setTemplateMeta(prev => ({ ...prev, name: e.target.value })); setNameError(false); }} placeholder="Nombre de plantilla..." className={`bg-transparent border-none text-sm font-semibold text-white placeholder-neutral-600 focus:outline-none min-w-0 w-40 truncate transition-colors ${ nameError ? 'text-red-400 placeholder-red-500/50' : '' }`} /> {/* Counter badge */}
{editableSlotCount}/{totalFieldCount}
{/* Spacer */}
{/* Center: View mode toggle + aspect + format */}
{/* View mode toggle */}
{/* Brand preview selector */}
{/* Aspect ratio */} {templateMeta.aspectRatio} {/* Format badge */}
{templateMeta.format === 'video' ?
{/* Divider */}
{/* Right: category + save */}
{/* Category pills (compact) */} {CATEGORIES.map(c => ( ))} {/* Save button */}
{/* Canvas row: canvas + optional config panel */}
{/* Canvas / Form Preview / Test Data */}
{viewMode === 'design' ? ( ) : viewMode === 'form-preview' ? ( ) : ( /* test-data mode: split form + live preview */
{/* Test data form */} {/* Live Remotion preview */}
{/* Subtle grid background */}
)}
{/* Right: Field Config Panel (design/form-preview modes only, not in segment mode) */} {selectedFieldId && viewMode !== 'test-data' && !activeScene?.segmentSource && ( )}
{/* Scene Composer (video only) β€” always full width below canvas row */} {templateMeta.format === 'video' && (
)}
); }; // ── Helper: Convert TemplateField[] to legacy ExpressField[] for backward compat ── function templateFieldsToExpressFields(fields: TemplateField[]): ExpressField[] { return fields.map((f): ExpressField => ({ id: f.id, type: f.type === 'video' ? 'media' : f.type === 'image' ? (f.brandSource === 'logo' ? 'logo' : 'media') : f.type === 'shape' ? 'shape' : 'text', label: f.label, placeholder: f.content || f.label, required: f.required, brandSource: f.brandSource, brandAssetId: f.brandAssetId, position: { x: f.position.x, y: f.position.y, w: f.position.w, h: f.position.h }, style: { fontSize: f.style.fontSize, fontWeight: f.style.fontWeight, fontFamily: f.style.fontFamily, textAlign: f.style.textAlign, color: f.style.color, opacity: f.style.opacity, shapeType: f.style.shapeType, shapeFill: f.style.shapeFill, shapeStroke: f.style.shapeStroke, shapeStrokeWidth: f.style.shapeStrokeWidth, shapeCornerRadius: f.style.shapeCornerRadius, }, })); } // ── TestDataFormPanel β€” Form for entering test data in test-data view mode ── /** Resolve brand variable preview for read-only display */ function resolveBrandTestValue(field: TemplateField, company: CompanyProfile, designMD: DesignMD): string { if (!field.brandSource) return ''; switch (field.brandSource) { case 'brand-name': return company.name || designMD.brandName || ''; case 'tagline': return company.tagline || ''; case 'logo': return '(Logo de marca)'; case 'instagram': return company.socialLinks?.instagram || ''; case 'tiktok': return company.socialLinks?.tiktok || ''; case 'twitter': return company.socialLinks?.x || ''; case 'youtube': return company.socialLinks?.youtube || ''; case 'website': return company.socialLinks?.website || ''; default: return ''; } } const TestDataFormPanel: React.FC = () => { const { fields, scenes, activeSceneId, setActiveSceneId, resolvedDesignMD: designMD, resolvedCompany: company, testFieldData, setTestFieldData, testMediaFits, setTestMediaFits, testContainBgColors, setTestContainBgColors, } = useTemplateBuilder(); const sceneFieldsMap = useSceneFieldsMap(); // Get all editable slots across all scenes const allEditableSlots = useMemo(() => { const slots: { field: TemplateField; sceneId: string; sceneName: string }[] = []; for (const scene of scenes) { const sceneFields = sceneFieldsMap[scene.id] || []; for (const f of sceneFields) { if (f.nature === 'editable-slot') { slots.push({ field: f, sceneId: scene.id, sceneName: scene.name }); } } } return slots.sort((a, b) => a.field.formOrder - b.field.formOrder); }, [scenes, sceneFieldsMap]); const brandVars = useMemo(() => { const vars: TemplateField[] = []; for (const scene of scenes) { const sceneFields = sceneFieldsMap[scene.id] || []; for (const f of sceneFields) { if (f.nature === 'brand-variable') vars.push(f); } } return vars; }, [scenes, sceneFieldsMap]); // Group by scene const sceneGroups = useMemo(() => { const groups: { sceneId: string; sceneName: string; fields: typeof allEditableSlots }[] = []; const seen = new Set(); for (const slot of allEditableSlots) { if (!seen.has(slot.sceneId)) { seen.add(slot.sceneId); groups.push({ sceneId: slot.sceneId, sceneName: slot.sceneName, fields: allEditableSlots.filter(s => s.sceneId === slot.sceneId), }); } } return groups; }, [allEditableSlots]); const isMultiScene = sceneGroups.length > 1; return (
{/* Header */}

Datos de prueba

{allEditableSlots.length} campo{allEditableSlots.length !== 1 ? 's' : ''}

Llena los campos para ver cΓ³mo se verΓ­a tu plantilla con datos reales.

{/* Scrollable fields */}
{allEditableSlots.length === 0 ? (

No hay campos editables para probar.

) : isMultiScene ? ( sceneGroups.map(group => (
{group.fields.map(({ field }) => ( setTestFieldData(prev => ({ ...prev, [field.id]: v }))} designMD={designMD} mediaFit={testMediaFits[field.id]} onMediaFitChange={(fit) => setTestMediaFits(prev => ({ ...prev, [field.id]: fit }))} containBgColor={testContainBgColors[field.id] ?? null} onContainBgColorChange={(color) => setTestContainBgColors(prev => ({ ...prev, [field.id]: color }))} /> ))}
)) ) : ( allEditableSlots.map(({ field }) => ( setTestFieldData(prev => ({ ...prev, [field.id]: v }))} designMD={designMD} mediaFit={testMediaFits[field.id]} onMediaFitChange={(fit) => setTestMediaFits(prev => ({ ...prev, [field.id]: fit }))} containBgColor={testContainBgColors[field.id] ?? null} onContainBgColorChange={(color) => setTestContainBgColors(prev => ({ ...prev, [field.id]: color }))} /> )) )} {/* Brand variables (read-only) */} {brandVars.length > 0 && (

Auto-completados desde {company.name}

{brandVars.map(field => (
{field.label} {resolveBrandTestValue(field, company, designMD) || '(no configurado)'}
auto
))}
)}
); };