| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Slide Deck Editor</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/lucide@latest"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| * { |
| font-family: 'Inter', sans-serif; |
| } |
| |
| .slide-thumb { |
| transition: all 0.2s ease; |
| cursor: pointer; |
| } |
| |
| .slide-thumb:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); |
| } |
| |
| .slide-thumb.active { |
| ring: 2px solid #6366f1; |
| transform: scale(1.02); |
| } |
| |
| .editor-input { |
| transition: all 0.2s ease; |
| } |
| |
| .editor-input:focus { |
| transform: translateY(-1px); |
| box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); |
| } |
| |
| .preview-container { |
| aspect-ratio: 16/9; |
| transition: transform 0.3s ease; |
| } |
| |
| .tool-btn { |
| transition: all 0.2s ease; |
| } |
| |
| .tool-btn:hover { |
| transform: translateY(-1px); |
| } |
| |
| .tool-btn.active { |
| background: #e0e7ff; |
| color: #4338ca; |
| } |
| |
| |
| ::-webkit-scrollbar { |
| width: 6px; |
| height: 6px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: transparent; |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: #cbd5e1; |
| border-radius: 3px; |
| } |
| |
| ::-webkit-scrollbar-thumb:hover { |
| background: #94a3b8; |
| } |
| |
| .gradient-text { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 h-screen w-screen overflow-hidden flex text-gray-900"> |
|
|
| |
| <aside class="w-72 bg-white border-r border-gray-200 flex flex-col h-full shrink-0"> |
| |
| <div class="p-4 border-b border-gray-200 flex items-center justify-between bg-gray-50/50"> |
| <div class="flex items-center gap-2"> |
| <div class="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center text-white"> |
| <i data-lucide="layers" class="w-4 h-4"></i> |
| </div> |
| <span class="font-semibold text-gray-900">Deck Editor</span> |
| </div> |
| <button onclick="editor.addSlide()" class="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center hover:bg-indigo-100 transition-colors" title="Add Slide"> |
| <i data-lucide="plus" class="w-4 h-4"></i> |
| </button> |
| </div> |
|
|
| |
| <div id="slides-list" class="flex-1 overflow-y-auto p-4 space-y-3"> |
| |
| </div> |
|
|
| |
| <div class="p-4 border-t border-gray-200 space-y-2 bg-gray-50/50"> |
| <button onclick="editor.preview()" class="w-full py-2.5 px-4 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors flex items-center justify-center gap-2"> |
| <i data-lucide="play" class="w-4 h-4"></i> |
| Preview Deck |
| </button> |
| <div class="flex gap-2"> |
| <button onclick="editor.exportJSON()" class="flex-1 py-2 px-3 border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors flex items-center justify-center gap-1"> |
| <i data-lucide="download" class="w-4 h-4"></i> |
| Export |
| </button> |
| <label class="flex-1 py-2 px-3 border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors flex items-center justify-center gap-1 cursor-pointer"> |
| <i data-lucide="upload" class="w-4 h-4"></i> |
| Import |
| <input type="file" id="import-file" accept=".json" class="hidden" onchange="editor.importJSON(this)"> |
| </label> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <main class="flex-1 flex flex-col h-full overflow-hidden"> |
| |
| <div class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shrink-0"> |
| <div class="flex items-center gap-4"> |
| <div class="relative"> |
| <button onclick="editor.toggleLayoutMenu()" id="layout-dropdown-btn" class="tool-btn px-4 py-1.5 rounded-lg text-sm font-medium text-gray-700 flex items-center gap-2 bg-white border border-gray-300 shadow-sm hover:bg-gray-50"> |
| <i data-lucide="layout" class="w-4 h-4"></i> |
| <span id="current-layout-label">Title</span> |
| <i data-lucide="chevron-down" class="w-4 h-4 ml-1"></i> |
| </button> |
| <div id="layout-menu" class="hidden absolute top-full left-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 py-2 z-50"> |
| <div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">Content</div> |
| <button onclick="editor.setLayout('title')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="title"> |
| <i data-lucide="type" class="w-4 h-4"></i> |
| Title |
| </button> |
| <button onclick="editor.setLayout('split')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="split"> |
| <i data-lucide="columns" class="w-4 h-4"></i> |
| Split |
| </button> |
| <button onclick="editor.setLayout('image')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="image"> |
| <i data-lucide="image" class="w-4 h-4"></i> |
| Image |
| </button> |
| <div class="border-t border-gray-100 my-1"> |
| <div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-1">Grid</div> |
| </div> |
| <button onclick="editor.setLayout('grid')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="grid"> |
| <i data-lucide="layout-grid" class="w-4 h-4"></i> |
| Grid Cards |
| </button> |
| <div class="border-t border-gray-100 my-1"> |
| <div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-1">Compare</div> |
| </div> |
| <button onclick="editor.setLayout('comparison')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="comparison"> |
| <i data-lucide="table" class="w-4 h-4"></i> |
| 2-Column Table |
| </button> |
| <button onclick="editor.setLayout('compare3')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="compare3"> |
| <i data-lucide="columns-3" class="w-4 h-4"></i> |
| 3-Column Table |
| </button> |
| <div class="border-t border-gray-100 my-1"> |
| <div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-1">Advanced</div> |
| </div> |
| <button onclick="editor.setLayout('custom-html')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="custom-html"> |
| <i data-lucide="code-2" class="w-4 h-4"></i> |
| Custom HTML |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="flex items-center gap-1 bg-indigo-50 rounded-lg p-1 ml-4"> |
| <button onclick="editor.setViewMode('visual')" id="view-visual" class="view-btn active px-3 py-1.5 rounded-md text-sm font-medium text-indigo-700 flex items-center gap-1"> |
| <i data-lucide="eye" class="w-4 h-4"></i> |
| Visual |
| </button> |
| <button onclick="editor.setViewMode('json')" id="view-json" class="view-btn px-3 py-1.5 rounded-md text-sm font-medium text-gray-600 flex items-center gap-1"> |
| <i data-lucide="code" class="w-4 h-4"></i> |
| JSON |
| </button> |
| </div> |
| </div> |
|
|
| <div class="flex items-center gap-2"> |
| <button onclick="editor.deleteSlide()" class="w-9 h-9 rounded-lg text-red-600 hover:bg-red-50 transition-colors flex items-center justify-center" title="Delete Slide"> |
| <i data-lucide="trash-2" class="w-4 h-4"></i> |
| </button> |
| <button onclick="editor.duplicateSlide()" class="w-9 h-9 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors flex items-center justify-center" title="Duplicate Slide"> |
| <i data-lucide="copy" class="w-4 h-4"></i> |
| </button> |
| <button onclick="editor.moveSlide(-1)" class="w-9 h-9 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors flex items-center justify-center" title="Move Up"> |
| <i data-lucide="arrow-up" class="w-4 h-4"></i> |
| </button> |
| <button onclick="editor.moveSlide(1)" class="w-9 h-9 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors flex items-center justify-center" title="Move Down"> |
| <i data-lucide="arrow-down" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 overflow-y-auto p-8"> |
| <div class="max-w-4xl mx-auto space-y-6"> |
| |
| |
| <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> |
| <div class="bg-gray-100 px-4 py-2 border-b border-gray-200 flex items-center justify-between"> |
| <span class="text-xs font-medium text-gray-500 uppercase tracking-wider">Preview</span> |
| <span class="text-xs text-gray-400" id="resolution-display">1920 × 1080</span> |
| </div> |
| <div class="p-6 bg-gray-50 flex items-center justify-center"> |
| <div id="slide-preview" class="preview-container w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden relative"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="edit-form" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6"> |
| |
| </div> |
|
|
| </div> |
| </div> |
| </main> |
|
|
| |
| <div id="preview-modal" class="fixed inset-0 bg-black/90 z-50 hidden items-center justify-center"> |
| <div class="w-full h-full max-w-6xl max-h-screen p-4 flex flex-col"> |
| <div class="flex items-center justify-between mb-4"> |
| <h3 class="text-white font-semibold">Presentation Preview</h3> |
| <button onclick="editor.closePreview()" class="text-white/70 hover:text-white transition-colors"> |
| <i data-lucide="x" class="w-6 h-6"></i> |
| </button> |
| </div> |
| <div class="flex-1 flex items-center justify-center"> |
| <div id="preview-container" class="w-full aspect-video bg-white rounded-lg overflow-hidden shadow-2xl"> |
| |
| </div> |
| </div> |
| <div class="mt-4 flex items-center justify-center gap-4"> |
| <button onclick="editor.previewPrev()" class="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"> |
| <i data-lucide="chevron-left" class="w-5 h-5"></i> |
| </button> |
| <span id="preview-counter" class="text-white font-medium">1 / 1</span> |
| <button onclick="editor.previewNext()" class="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"> |
| <i data-lucide="chevron-right" class="w-5 h-5"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| // Initialize Lucide icons |
| lucide.createIcons(); |
| |
| class SlideEditor { |
| constructor() { |
| this.validationPaused = false; |
| this.pendingJSON = null; |
| this.slides = [ |
| { |
| id: 1, |
| layout: 'title', |
| title: 'Digital Innovation', |
| subtitle: 'Transforming ideas into reality through modern design and cutting-edge technology', |
| theme: 'gradient', |
| badge: 'Presentation Deck', |
| metadata: ['2024', '5 min read'] |
| }, |
| { |
| id: 2, |
| layout: 'split', |
| title: 'Strategic Vision', |
| content: 'Our approach combines data-driven insights with creative excellence. We believe in building sustainable solutions that stand the test of time.', |
| points: [ |
| 'User-centered design methodology', |
| 'Agile development processes', |
| 'Continuous integration & delivery' |
| ], |
| image: 'office' |
| }, |
| { |
| id: 3, |
| layout: 'grid', |
| title: 'Core Features', |
| subtitle: 'Everything you need to succeed', |
| items: [ |
| { icon: 'zap', title: 'Lightning Fast', desc: 'Optimized performance', color: 'blue' }, |
| { icon: 'shield', title: 'Secure by Design', desc: 'Enterprise-grade security', color: 'purple' }, |
| { icon: 'heart', title: 'User Friendly', desc: 'Intuitive interfaces', color: 'pink' } |
| ] |
| } |
| ]; |
| this.currentSlide = 0; |
| this.previewIndex = 0; |
| this.viewMode = 'visual'; |
| this.jsonError = null; |
| |
| this.init(); |
| } |
| |
| init() { |
| this.renderSlidesList(); |
| this.renderEditor(); |
| this.updatePreview(); |
| |
| // Close layout dropdown when clicking outside |
| document.addEventListener('click', (e) => { |
| const menu = document.getElementById('layout-menu'); |
| const btn = document.getElementById('layout-dropdown-btn'); |
| if (menu && btn && !menu.contains(e.target) && !btn.contains(e.target)) { |
| menu.classList.add('hidden'); |
| } |
| }); |
| } |
| |
| toggleLayoutMenu() { |
| document.getElementById('layout-menu').classList.toggle('hidden'); |
| } |
| |
| generateId() { |
| return Date.now(); |
| } |
| |
| addSlide() { |
| const newSlide = { |
| id: this.generateId(), |
| layout: 'title', |
| title: 'New Slide', |
| subtitle: 'Add your subtitle here', |
| theme: 'default', |
| badge: '', |
| metadata: [] |
| }; |
| this.slides.push(newSlide); |
| this.currentSlide = this.slides.length - 1; |
| this.renderSlidesList(); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| duplicateSlide() { |
| const slide = this.slides[this.currentSlide]; |
| const newSlide = { ...slide, id: this.generateId() }; |
| this.slides.splice(this.currentSlide + 1, 0, newSlide); |
| this.currentSlide++; |
| this.renderSlidesList(); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| deleteSlide() { |
| if (this.slides.length <= 1) { |
| alert('You must have at least one slide'); |
| return; |
| } |
| if (confirm('Delete this slide?')) { |
| this.slides.splice(this.currentSlide, 1); |
| this.currentSlide = Math.max(0, this.currentSlide - 1); |
| this.renderSlidesList(); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| } |
| |
| moveSlide(direction) { |
| const newIndex = this.currentSlide + direction; |
| if (newIndex >= 0 && newIndex < this.slides.length) { |
| [this.slides[this.currentSlide], this.slides[newIndex]] = |
| [this.slides[newIndex], this.slides[this.currentSlide]]; |
| this.currentSlide = newIndex; |
| this.renderSlidesList(); |
| } |
| } |
| |
| selectSlide(index) { |
| this.currentSlide = index; |
| this.renderSlidesList(); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| setLayout(layout) { |
| this.slides[this.currentSlide].layout = layout; |
| |
| // Add default data for new layouts |
| if (layout === 'grid' && !this.slides[this.currentSlide].items) { |
| this.slides[this.currentSlide].items = [ |
| { icon: 'zap', title: 'Feature 1', desc: 'Description here', color: 'blue' } |
| ]; |
| } |
| if (layout === 'image' && !this.slides[this.currentSlide].imageTitle) { |
| this.slides[this.currentSlide].imageTitle = 'Image Slide'; |
| this.slides[this.currentSlide].image = 'technology'; |
| } |
| if (layout === 'comparison' && !this.slides[this.currentSlide].columns) { |
| this.slides[this.currentSlide].columns = ['Feature', 'Our Solution', 'Competitors']; |
| this.slides[this.currentSlide].rows = [ |
| { feature: 'Performance', col1: '✓ Superior', col2: '✗ Limited' }, |
| { feature: 'Pricing', col1: '✓ Affordable', col2: '✗ Expensive' }, |
| { feature: 'Support', col1: '✓ 24/7', col2: '✗ Business hours' } |
| ]; |
| } |
| if (layout === 'compare3' && !this.slides[this.currentSlide].headers) { |
| this.slides[this.currentSlide].headers = ['Option A', 'Option B', 'Option C']; |
| this.slides[this.currentSlide].rows = [ |
| { feature: 'Speed', col1: 'Fast', col2: 'Medium', col3: 'Slow' }, |
| { feature: 'Price', col1: '$99/mo', col2: '$149/mo', col3: '$199/mo' }, |
| { feature: 'Support', col1: '24/7', col2: 'Business', col3: 'Email' } |
| ]; |
| } |
| if (layout === 'custom-html' && !this.slides[this.currentSlide].htmlContent) { |
| this.slides[this.currentSlide].htmlContent = '<div class="flex items-center justify-center h-full bg-gradient-to-br from-indigo-50 to-purple-50">\n <div class="text-center p-8">\n <h2 class="text-4xl font-bold text-indigo-600 mb-4">Custom HTML Slide</h2>\n <p class="text-lg text-gray-600">Edit this content in the HTML editor below</p>\n </div>\n</div>'; |
| } |
| |
| this.renderEditor(); |
| this.updatePreview(); |
| |
| // Update layout button label |
| const layoutLabels = { |
| 'title': 'Title', |
| 'split': 'Split', |
| 'image': 'Image', |
| 'grid': 'Grid Cards', |
| 'comparison': '2-Column Table', |
| 'compare3': '3-Column Table', |
| 'custom-html': 'Custom HTML' |
| }; |
| document.getElementById('current-layout-label').textContent = layoutLabels[layout] || layout; |
| |
| // Close menu if open |
| document.getElementById('layout-menu').classList.add('hidden'); |
| } |
| |
| updateField(field, value) { |
| this.slides[this.currentSlide][field] = value; |
| this.updatePreview(); |
| } |
| |
| updatePoint(index, value) { |
| if (!this.slides[this.currentSlide].points) { |
| this.slides[this.currentSlide].points = []; |
| } |
| this.slides[this.currentSlide].points[index] = value; |
| this.updatePreview(); |
| } |
| |
| addPoint() { |
| if (!this.slides[this.currentSlide].points) { |
| this.slides[this.currentSlide].points = []; |
| } |
| this.slides[this.currentSlide].points.push('New point'); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| removePoint(index) { |
| this.slides[this.currentSlide].points.splice(index, 1); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| renderSlidesList() { |
| const container = document.getElementById('slides-list'); |
| container.innerHTML = this.slides.map((slide, index) => ` |
| <div onclick="editor.selectSlide(${index})" |
| class="slide-thumb ${index === this.currentSlide ? 'active' : ''} bg-white rounded-lg border border-gray-200 p-3 ${index === this.currentSlide ? 'ring-2 ring-indigo-600' : ''}"> |
| <div class="flex items-center gap-3 mb-2"> |
| <span class="text-xs font-medium text-gray-400 w-5">${index + 1}</span> |
| <span class="font-medium text-sm text-gray-900 truncate flex-1">${slide.title || 'Untitled'}</span> |
| </div> |
| <div class="h-16 bg-gray-100 rounded border border-gray-200 overflow-hidden flex items-center justify-center text-xs text-gray-400"> |
| ${slide.layout} |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| setViewMode(mode) { |
| this.viewMode = mode; |
| document.querySelectorAll('.view-btn').forEach(btn => { |
| btn.classList.remove('active', 'bg-white', 'shadow-sm', 'text-indigo-700'); |
| btn.classList.add('text-gray-600'); |
| }); |
| document.getElementById(`view-${mode}`).classList.add('active', 'bg-white', 'shadow-sm', 'text-indigo-700'); |
| document.getElementById(`view-${mode}`).classList.remove('text-gray-600'); |
| this.renderEditor(); |
| } |
| |
| renderEditor() { |
| const slide = this.slides[this.currentSlide]; |
| const form = document.getElementById('edit-form'); |
| |
| // JSON View Mode |
| if (this.viewMode === 'json') { |
| form.innerHTML = ` |
| <div class="space-y-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h3 class="text-sm font-semibold text-gray-900">JSON Editor</h3> |
| <p class="text-xs text-gray-500 mt-1">Edit the raw JSON data for this slide</p> |
| </div> |
| <div class="flex items-center gap-2"> |
| <button onclick="editor.toggleValidationPause()" class="px-3 py-1.5 text-xs font-medium ${this.validationPaused ? 'text-orange-600 bg-orange-50 hover:bg-orange-100' : 'text-gray-600 bg-gray-100 hover:bg-gray-200'} rounded-lg transition-colors flex items-center gap-1" title="${this.validationPaused ? 'Resume real-time validation' : 'Pause real-time validation'}"> |
| <i data-lucide="${this.validationPaused ? 'play' : 'pause'}" class="w-3 h-3"></i> |
| ${this.validationPaused ? 'Resume' : 'Pause'} |
| </button> |
| <button onclick="editor.formatJSON()" class="px-3 py-1.5 text-xs font-medium text-indigo-600 bg-indigo-50 rounded-lg hover:bg-indigo-100 transition-colors flex items-center gap-1"> |
| <i data-lucide="sparkles" class="w-3 h-3"></i> |
| Format |
| </button> |
| </div> |
| </div> |
| <div class="relative"> |
| <textarea id="json-editor" |
| oninput="editor.validateJSON(this.value)" |
| class="w-full h-96 px-4 py-3 font-mono text-xs leading-relaxed border ${this.jsonError ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : 'border-gray-300 focus:ring-indigo-500 focus:border-indigo-500'} rounded-lg outline-none resize-none" |
| spellcheck="false">${JSON.stringify(slide, null, 2)}</textarea> |
| ${this.jsonError ? `<div class="absolute bottom-3 left-3 right-3 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-xs text-red-600 flex items-center gap-2"><i data-lucide="alert-circle" class="w-4 h-4"></i>${this.jsonError}</div>` : ''} |
| </div> |
| <button onclick="editor.applyJSON()" class="w-full py-3 px-4 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors flex items-center justify-center gap-2"> |
| <i data-lucide="save" class="w-4 h-4"></i> |
| Apply Changes |
| </button> |
| </div> |
| `; |
| lucide.createIcons(); |
| return; |
| } |
| |
| // Visual View Mode |
| let html = ` |
| <div class="grid grid-cols-2 gap-4"> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Slide Title</label> |
| <input type="text" value="${slide.title || ''}" |
| oninput="editor.updateField('title', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| </div> |
| `; |
| |
| if (slide.layout === 'title') { |
| html += ` |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label> |
| <textarea oninput="editor.updateField('subtitle', this.value)" rows="3" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">${slide.subtitle || ''}</textarea> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Badge</label> |
| <input type="text" value="${slide.badge || ''}" |
| oninput="editor.updateField('badge', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Theme</label> |
| <select onchange="editor.updateField('theme', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| <option value="default" ${slide.theme === 'default' ? 'selected' : ''}>Default</option> |
| <option value="gradient" ${slide.theme === 'gradient' ? 'selected' : ''}>Gradient</option> |
| <option value="dark" ${slide.theme === 'dark' ? 'selected' : ''}>Dark</option> |
| </select> |
| </div> |
| `; |
| } else if (slide.layout === 'split') { |
| html += ` |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Content</label> |
| <textarea oninput="editor.updateField('content', this.value)" rows="4" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">${slide.content || ''}</textarea> |
| </div> |
| <div class="col-span-2"> |
| <div class="flex items-center justify-between mb-2"> |
| <label class="block text-sm font-medium text-gray-700">Bullet Points</label> |
| <button onclick="editor.addPoint()" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium">+ Add Point</button> |
| </div> |
| <div class="space-y-2"> |
| ${(slide.points || []).map((point, i) => ` |
| <div class="flex items-center gap-2"> |
| <i data-lucide="check-circle-2" class="w-4 h-4 text-indigo-600 shrink-0"></i> |
| <input type="text" value="${point}" |
| oninput="editor.updatePoint(${i}, this.value)" |
| class="editor-input flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none text-sm"> |
| <button onclick="editor.removePoint(${i})" class="text-red-500 hover:text-red-700"> |
| <i data-lucide="x" class="w-4 h-4"></i> |
| </button> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Image Category</label> |
| <select onchange="editor.updateField('image', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| <option value="office" ${slide.image === 'office' ? 'selected' : ''}>Office</option> |
| <option value="technology" ${slide.image === 'technology' ? 'selected' : ''}>Technology</option> |
| <option value="people" ${slide.image === 'people' ? 'selected' : ''}>People</option> |
| <option value="nature" ${slide.image === 'nature' ? 'selected' : ''}>Nature</option> |
| </select> |
| </div> |
| `; |
| } else if (slide.layout === 'image') { |
| html += ` |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Image Title</label> |
| <input type="text" value="${slide.imageTitle || ''}" |
| oninput="editor.updateField('imageTitle', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| </div> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Image Description</label> |
| <textarea oninput="editor.updateField('imageDesc', this.value)" rows="3" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">${slide.imageDesc || ''}</textarea> |
| </div> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Image Category</label> |
| <select onchange="editor.updateField('image', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| <option value="technology" ${slide.image === 'technology' ? 'selected' : ''}>Technology</option> |
| <option value="office" ${slide.image === 'office' ? 'selected' : ''}>Office</option> |
| <option value="cityscape" ${slide.image === 'cityscape' ? 'selected' : ''}>Cityscape</option> |
| <option value="nature" ${slide.image === 'nature' ? 'selected' : ''}>Nature</option> |
| </select> |
| </div> |
| `; |
| } else if (slide.layout === 'grid') { |
| html += ` |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label> |
| <input type="text" value="${slide.subtitle || ''}" |
| oninput="editor.updateField('subtitle', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| </div> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Grid Items (first 3 shown)</label> |
| <div class="grid grid-cols-3 gap-4"> |
| ${(slide.items || []).slice(0, 3).map((item, i) => ` |
| <div class="space-y-2 p-3 bg-gray-50 rounded-lg border border-gray-200"> |
| <input type="text" value="${item.title}" placeholder="Title" |
| oninput="editor.updateItem(${i}, 'title', this.value)" |
| class="editor-input w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <input type="text" value="${item.desc}" placeholder="Description" |
| oninput="editor.updateItem(${i}, 'desc', this.value)" |
| class="editor-input w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'comparison') { |
| html += ` |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label> |
| <input type="text" value="${slide.subtitle || ''}" |
| oninput="editor.updateField('subtitle', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| </div> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Column Headers</label> |
| <div class="grid grid-cols-3 gap-3"> |
| ${(slide.columns || ['Feature', 'Our Solution', 'Competitors']).map((col, i) => ` |
| <input type="text" value="${col}" |
| oninput="editor.updateColumn(${i}, this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"> |
| `).join('')} |
| </div> |
| </div> |
| <div class="col-span-2"> |
| <div class="flex items-center justify-between mb-2"> |
| <label class="block text-sm font-medium text-gray-700">Comparison Rows</label> |
| <button onclick="editor.addComparisonRow()" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium">+ Add Row</button> |
| </div> |
| <div class="space-y-2"> |
| ${(slide.rows || []).map((row, i) => ` |
| <div class="grid grid-cols-3 gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200"> |
| <input type="text" value="${row.feature}" placeholder="Feature name" |
| oninput="editor.updateComparisonRow(${i}, 'feature', this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <input type="text" value="${row.col1}" placeholder="Option 1" |
| oninput="editor.updateComparisonRow(${i}, 'col1', this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <div class="flex items-center gap-2"> |
| <input type="text" value="${row.col2}" placeholder="Option 2" |
| oninput="editor.updateComparisonRow(${i}, 'col2', this.value)" |
| class="editor-input flex-1 px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <button onclick="editor.removeComparisonRow(${i})" class="text-red-500 hover:text-red-700 p-1"> |
| <i data-lucide="x" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'compare3') { |
| html += ` |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label> |
| <input type="text" value="${slide.subtitle || ''}" |
| oninput="editor.updateField('subtitle', this.value)" |
| class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"> |
| </div> |
| <div class="col-span-2"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Three Column Headers</label> |
| <div class="grid grid-cols-3 gap-3"> |
| ${(slide.headers || ['Option A', 'Option B', 'Option C']).map((header, i) => ` |
| <input type="text" value="${header}" |
| oninput="editor.updateHeader(${i}, this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"> |
| `).join('')} |
| </div> |
| </div> |
| <div class="col-span-2"> |
| <div class="flex items-center justify-between mb-2"> |
| <label class="block text-sm font-medium text-gray-700">Comparison Rows</label> |
| <button onclick="editor.addCompare3Row()" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium">+ Add Row</button> |
| </div> |
| <div class="space-y-2"> |
| ${(slide.rows || []).map((row, i) => ` |
| <div class="grid grid-cols-4 gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200"> |
| <input type="text" value="${row.feature}" placeholder="Feature name" |
| oninput="editor.updateCompare3Row(${i}, 'feature', this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <input type="text" value="${row.col1}" placeholder="Column 1" |
| oninput="editor.updateCompare3Row(${i}, 'col1', this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <input type="text" value="${row.col2}" placeholder="Column 2" |
| oninput="editor.updateCompare3Row(${i}, 'col2', this.value)" |
| class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <div class="flex items-center gap-2"> |
| <input type="text" value="${row.col3}" placeholder="Column 3" |
| oninput="editor.updateCompare3Row(${i}, 'col3', this.value)" |
| class="editor-input flex-1 px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none"> |
| <button onclick="editor.removeCompare3Row(${i})" class="text-red-500 hover:text-red-700 p-1"> |
| <i data-lucide="x" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'custom-html') { |
| html += ` |
| <div class="col-span-2"> |
| <div class="flex items-center justify-between mb-2"> |
| <label class="block text-sm font-medium text-gray-700">HTML Content</label> |
| <span class="text-xs text-gray-500">Supports Tailwind CSS classes</span> |
| </div> |
| <textarea id="html-editor" oninput="editor.updateField('htmlContent', this.value)" |
| class="editor-input w-full px-4 py-3 font-mono text-xs leading-relaxed border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none resize-none" |
| rows="12" |
| spellcheck="false">${slide.htmlContent || ''}</textarea> |
| <p class="text-xs text-gray-500 mt-1">Write raw HTML. The content will be rendered inside a 16:9 slide container.</p> |
| </div> |
| `; |
| } |
| |
| html += `</div>`; |
| form.innerHTML = html; |
| lucide.createIcons(); |
| } |
| |
| updateItem(index, field, value) { |
| this.slides[this.currentSlide].items[index][field] = value; |
| this.updatePreview(); |
| } |
| |
| updateColumn(index, value) { |
| if (!this.slides[this.currentSlide].columns) { |
| this.slides[this.currentSlide].columns = ['Feature', 'Our Solution', 'Competitors']; |
| } |
| this.slides[this.currentSlide].columns[index] = value; |
| this.updatePreview(); |
| } |
| |
| updateComparisonRow(index, field, value) { |
| if (!this.slides[this.currentSlide].rows) { |
| this.slides[this.currentSlide].rows = []; |
| } |
| if (!this.slides[this.currentSlide].rows[index]) { |
| this.slides[this.currentSlide].rows[index] = { feature: '', col1: '', col2: '' }; |
| } |
| this.slides[this.currentSlide].rows[index][field] = value; |
| this.updatePreview(); |
| } |
| |
| addComparisonRow() { |
| if (!this.slides[this.currentSlide].rows) { |
| this.slides[this.currentSlide].rows = []; |
| } |
| this.slides[this.currentSlide].rows.push({ feature: 'New Feature', col1: '✓ Yes', col2: '✗ No' }); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| removeComparisonRow(index) { |
| this.slides[this.currentSlide].rows.splice(index, 1); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| updateHeader(index, value) { |
| if (!this.slides[this.currentSlide].headers) { |
| this.slides[this.currentSlide].headers = ['Option A', 'Option B', 'Option C']; |
| } |
| this.slides[this.currentSlide].headers[index] = value; |
| this.updatePreview(); |
| } |
| |
| updateCompare3Row(index, field, value) { |
| if (!this.slides[this.currentSlide].rows) { |
| this.slides[this.currentSlide].rows = []; |
| } |
| if (!this.slides[this.currentSlide].rows[index]) { |
| this.slides[this.currentSlide].rows[index] = { feature: '', col1: '', col2: '', col3: '' }; |
| } |
| this.slides[this.currentSlide].rows[index][field] = value; |
| this.updatePreview(); |
| } |
| |
| addCompare3Row() { |
| if (!this.slides[this.currentSlide].rows) { |
| this.slides[this.currentSlide].rows = []; |
| } |
| this.slides[this.currentSlide].rows.push({ feature: 'New Feature', col1: '-', col2: '-', col3: '-' }); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| removeCompare3Row(index) { |
| this.slides[this.currentSlide].rows.splice(index, 1); |
| this.renderEditor(); |
| this.updatePreview(); |
| } |
| |
| validateJSON(jsonString) { |
| // Skip validation if paused (but still track for manual validation) |
| if (this.validationPaused) { |
| this.pendingJSON = jsonString; |
| return; |
| } |
| |
| try { |
| JSON.parse(jsonString); |
| this.jsonError = null; |
| document.getElementById('json-editor').classList.remove('border-red-300', 'focus:ring-red-500', 'focus:border-red-500'); |
| document.getElementById('json-editor').classList.add('border-gray-300', 'focus:ring-indigo-500', 'focus:border-indigo-500'); |
| } catch (err) { |
| this.jsonError = err.message; |
| document.getElementById('json-editor').classList.add('border-red-300', 'focus:ring-red-500', 'focus:border-red-500'); |
| document.getElementById('json-editor').classList.remove('border-gray-300', 'focus:ring-indigo-500', 'focus:border-indigo-500'); |
| } |
| // Re-render to show/hide error message |
| const textarea = document.getElementById('json-editor'); |
| const cursorPosition = textarea.selectionStart; |
| this.renderEditor(); |
| // Restore cursor position |
| const newTextarea = document.getElementById('json-editor'); |
| if (newTextarea) { |
| newTextarea.focus(); |
| newTextarea.setSelectionRange(cursorPosition, cursorPosition); |
| } |
| } |
| |
| toggleValidationPause() { |
| this.validationPaused = !this.validationPaused; |
| // When unpausing, validate the pending content |
| if (!this.validationPaused && this.pendingJSON) { |
| this.validateJSON(this.pendingJSON); |
| this.pendingJSON = null; |
| } |
| this.renderEditor(); |
| } |
| |
| formatJSON() { |
| const textarea = document.getElementById('json-editor'); |
| try { |
| const parsed = JSON.parse(textarea.value); |
| textarea.value = JSON.stringify(parsed, null, 2); |
| this.jsonError = null; |
| this.renderEditor(); |
| } catch (err) { |
| this.jsonError = err.message; |
| this.renderEditor(); |
| } |
| } |
| |
| applyJSON() { |
| const textarea = document.getElementById('json-editor'); |
| try { |
| const parsed = JSON.parse(textarea.value); |
| // Preserve the ID |
| parsed.id = this.slides[this.currentSlide].id; |
| this.slides[this.currentSlide] = parsed; |
| this.jsonError = null; |
| this.renderSlidesList(); |
| this.updatePreview(); |
| // Show success feedback |
| const btn = document.querySelector('button[onclick="editor.applyJSON()"]'); |
| const originalText = btn.innerHTML; |
| btn.innerHTML = '<i data-lucide="check" class="w-4 h-4"></i> Saved!'; |
| btn.classList.add('bg-green-600', 'hover:bg-green-700'); |
| btn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700'); |
| lucide.createIcons(); |
| setTimeout(() => { |
| btn.innerHTML = originalText; |
| btn.classList.remove('bg-green-600', 'hover:bg-green-700'); |
| btn.classList.add('bg-indigo-600', 'hover:bg-indigo-700'); |
| lucide.createIcons(); |
| }, 2000); |
| } catch (err) { |
| this.jsonError = err.message; |
| this.renderEditor(); |
| } |
| } |
| |
| updatePreview() { |
| const slide = this.slides[this.currentSlide]; |
| const preview = document.getElementById('slide-preview'); |
| |
| let content = ''; |
| |
| if (slide.layout === 'title') { |
| const isGradient = slide.theme === 'gradient'; |
| content = ` |
| <div class="w-full h-full flex flex-col items-center justify-center p-8 ${isGradient ? 'bg-gradient-to-br from-white to-gray-50' : 'bg-white'}"> |
| <div class="text-center space-y-4"> |
| ${slide.badge ? `<div class="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-indigo-50 text-indigo-600 text-xs font-medium"><i data-lucide="sparkles" class="w-3 h-3"></i><span>${slide.badge}</span></div>` : ''} |
| <h1 class="text-3xl font-bold ${isGradient ? 'gradient-text' : 'text-gray-900'} leading-tight">${slide.title}</h1> |
| <p class="text-sm text-gray-600 max-w-md mx-auto">${slide.subtitle}</p> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'split') { |
| content = ` |
| <div class="w-full h-full flex flex-col md:flex-row"> |
| <div class="w-full md:w-1/2 p-6 flex flex-col justify-center bg-white"> |
| <div class="space-y-4"> |
| <h2 class="text-2xl font-bold text-gray-900">${slide.title}</h2> |
| <p class="text-sm text-gray-600 leading-relaxed">${slide.content}</p> |
| <ul class="space-y-2"> |
| ${(slide.points || []).map(point => `<li class="flex items-start gap-2 text-xs text-gray-600"><i data-lucide="check-circle-2" class="w-4 h-4 text-indigo-600 mt-0.5 shrink-0"></i><span>${point}</span></li>`).join('')} |
| </ul> |
| </div> |
| </div> |
| <div class="w-full md:w-1/2 bg-gray-100 flex items-center justify-center text-gray-400"> |
| <i data-lucide="image" class="w-12 h-12"></i> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'image') { |
| content = ` |
| <div class="w-full h-full relative bg-gray-900 flex items-center justify-center overflow-hidden"> |
| <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div> |
| <div class="absolute bottom-0 left-0 right-0 p-6 text-white z-10"> |
| <h2 class="text-2xl font-bold mb-2">${slide.title}</h2> |
| <p class="text-sm text-gray-200">${slide.subtitle}</p> |
| </div> |
| <i data-lucide="image" class="w-16 h-16 text-white/20"></i> |
| </div> |
| `; |
| } else if (slide.layout === 'grid') { |
| content = ` |
| <div class="w-full h-full p-6 bg-gray-50 flex flex-col justify-center"> |
| <div class="text-center space-y-1 mb-4"> |
| <h2 class="text-xl font-bold text-gray-900">${slide.title}</h2> |
| <p class="text-xs text-gray-600">${slide.subtitle}</p> |
| </div> |
| <div class="grid grid-cols-3 gap-3"> |
| ${(slide.items || []).slice(0, 3).map(item => ` |
| <div class="bg-white p-3 rounded-lg shadow-sm border border-gray-100"> |
| <div class="w-8 h-8 rounded-lg bg-${item.color}-100 text-${item.color}-600 flex items-center justify-center mb-2"> |
| <i data-lucide="${item.icon}" class="w-4 h-4"></i> |
| </div> |
| <h3 class="font-semibold text-gray-900 text-xs mb-1">${item.title}</h3> |
| <p class="text-xs text-gray-600 leading-tight">${item.desc}</p> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'comparison') { |
| const cols = slide.columns || ['Feature', 'Our Solution', 'Competitors']; |
| content = ` |
| <div class="w-full h-full p-6 bg-white flex flex-col justify-center"> |
| <div class="text-center space-y-1 mb-4"> |
| <h2 class="text-xl font-bold text-gray-900">${slide.title}</h2> |
| ${slide.subtitle ? `<p class="text-xs text-gray-600">${slide.subtitle}</p>` : ''} |
| </div> |
| <div class="overflow-hidden rounded-lg border border-gray-200"> |
| <table class="w-full text-xs"> |
| <thead> |
| <tr class="bg-indigo-50"> |
| <th class="px-3 py-2 text-left font-semibold text-gray-900 border-b border-indigo-100">${cols[0]}</th> |
| <th class="px-3 py-2 text-center font-semibold text-indigo-700 border-b border-indigo-100">${cols[1]}</th> |
| <th class="px-3 py-2 text-center font-semibold text-gray-600 border-b border-indigo-100">${cols[2]}</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${(slide.rows || []).map((row, i) => ` |
| <tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}"> |
| <td class="px-3 py-2 font-medium text-gray-900 border-b border-gray-100">${row.feature}</td> |
| <td class="px-3 py-2 text-center text-indigo-600 border-b border-gray-100">${row.col1}</td> |
| <td class="px-3 py-2 text-center text-gray-500 border-b border-gray-100">${row.col2}</td> |
| </tr> |
| `).join('')} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'compare3') { |
| const headers = slide.headers || ['Option A', 'Option B', 'Option C']; |
| content = ` |
| <div class="w-full h-full p-6 bg-white flex flex-col justify-center"> |
| <div class="text-center space-y-1 mb-4"> |
| <h2 class="text-xl font-bold text-gray-900">${slide.title}</h2> |
| ${slide.subtitle ? `<p class="text-xs text-gray-600">${slide.subtitle}</p>` : ''} |
| </div> |
| <div class="overflow-hidden rounded-lg border border-gray-200"> |
| <table class="w-full text-xs"> |
| <thead> |
| <tr class="bg-gradient-to-r from-indigo-50 via-purple-50 to-pink-50"> |
| <th class="px-2 py-3 text-left font-semibold text-gray-900 border-b border-gray-200 w-1/4"></th> |
| <th class="px-2 py-3 text-center font-semibold text-indigo-700 border-b border-gray-200">${headers[0]}</th> |
| <th class="px-2 py-3 text-center font-semibold text-purple-700 border-b border-gray-200">${headers[1]}</th> |
| <th class="px-2 py-3 text-center font-semibold text-pink-700 border-b border-gray-200">${headers[2]}</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${(slide.rows || []).map((row, i) => ` |
| <tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}"> |
| <td class="px-2 py-3 font-medium text-gray-900 border-b border-gray-100">${row.feature}</td> |
| <td class="px-2 py-3 text-center text-sm text-gray-700 border-b border-gray-100">${row.col1}</td> |
| <td class="px-2 py-3 text-center text-sm text-gray-700 border-b border-gray-100">${row.col2}</td> |
| <td class="px-2 py-3 text-center text-sm text-gray-700 border-b border-gray-100">${row.col3}</td> |
| </tr> |
| `).join('')} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'custom-html') { |
| content = slide.htmlContent || '<div class="flex items-center justify-center h-full text-gray-400">No HTML content</div>'; |
| } |
| |
| preview.innerHTML = content; |
| lucide.createIcons(); |
| } |
| |
| preview() { |
| this.previewIndex = 0; |
| document.getElementById('preview-modal').classList.remove('hidden'); |
| document.getElementById('preview-modal').classList.add('flex'); |
| this.renderFullPreview(); |
| } |
| |
| closePreview() { |
| document.getElementById('preview-modal').classList.add('hidden'); |
| document.getElementById('preview-modal').classList.remove('flex'); |
| } |
| |
| renderFullPreview() { |
| const slide = this.slides[this.previewIndex]; |
| const container = document.getElementById('preview-container'); |
| |
| // Use same rendering logic but full size |
| let content = ''; |
| |
| if (slide.layout === 'title') { |
| const isGradient = slide.theme === 'gradient'; |
| content = ` |
| <div class="w-full h-full flex flex-col items-center justify-center p-12 ${isGradient ? 'bg-gradient-to-br from-white to-gray-50' : 'bg-white'}"> |
| <div class="text-center space-y-6"> |
| ${slide.badge ? `<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-indigo-50 text-indigo-600 text-sm font-medium mb-4"><i data-lucide="sparkles" class="w-4 h-4"></i><span>${slide.badge}</span></div>` : ''} |
| <h1 class="text-5xl font-bold ${isGradient ? 'gradient-text' : 'text-gray-900'} leading-tight">${slide.title}</h1> |
| <p class="text-xl text-gray-600 max-w-2xl mx-auto">${slide.subtitle}</p> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'split') { |
| content = ` |
| <div class="w-full h-full flex"> |
| <div class="w-1/2 p-12 flex flex-col justify-center bg-white"> |
| <div class="space-y-6"> |
| <h2 class="text-4xl font-bold text-gray-900">${slide.title}</h2> |
| <p class="text-lg text-gray-600 leading-relaxed">${slide.content}</p> |
| <ul class="space-y-3"> |
| ${(slide.points || []).map(point => `<li class="flex items-start gap-3 text-gray-600"><i data-lucide="check-circle-2" class="w-5 h-5 text-indigo-600 mt-0.5 shrink-0"></i><span>${point}</span></li>`).join('')} |
| </ul> |
| </div> |
| </div> |
| <div class="w-1/2 bg-gray-100 flex items-center justify-center"> |
| <img src="http://static.photos/${slide.image || 'office'}/800x450/${slide.id}" class="w-full h-full object-cover" alt=""> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'image') { |
| content = ` |
| <div class="w-full h-full relative"> |
| <img src="http://static.photos/${slide.image || 'technology'}/1200x630/${slide.id}" class="w-full h-full object-cover" alt=""> |
| <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div> |
| <div class="absolute bottom-0 left-0 right-0 p-12 text-white"> |
| <h2 class="text-4xl font-bold mb-4">${slide.title}</h2> |
| <p class="text-xl text-gray-200">${slide.subtitle}</p> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'grid') { |
| content = ` |
| <div class="w-full h-full p-12 bg-gray-50 flex flex-col justify-center"> |
| <div class="text-center space-y-2 mb-8"> |
| <h2 class="text-3xl font-bold text-gray-900">${slide.title}</h2> |
| <p class="text-gray-600">${slide.subtitle}</p> |
| </div> |
| <div class="grid grid-cols-3 gap-6 max-w-4xl mx-auto w-full"> |
| ${(slide.items || []).slice(0, 3).map(item => ` |
| <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> |
| <div class="w-12 h-12 rounded-xl bg-${item.color}-100 text-${item.color}-600 flex items-center justify-center mb-4"> |
| <i data-lucide="${item.icon}" class="w-6 h-6"></i> |
| </div> |
| <h3 class="font-semibold text-gray-900 mb-2">${item.title}</h3> |
| <p class="text-sm text-gray-600">${item.desc}</p> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'comparison') { |
| const cols = slide.columns || ['Feature', 'Our Solution', 'Competitors']; |
| content = ` |
| <div class="w-full h-full p-12 bg-white flex flex-col justify-center"> |
| <div class="text-center space-y-2 mb-8"> |
| <h2 class="text-4xl font-bold text-gray-900">${slide.title}</h2> |
| ${slide.subtitle ? `<p class="text-xl text-gray-600">${slide.subtitle}</p>` : ''} |
| </div> |
| <div class="max-w-4xl mx-auto w-full"> |
| <div class="overflow-hidden rounded-2xl border border-gray-200 shadow-sm"> |
| <table class="w-full"> |
| <thead> |
| <tr class="bg-indigo-50"> |
| <th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 border-b border-indigo-100 w-1/3">${cols[0]}</th> |
| <th class="px-6 py-4 text-center text-sm font-bold text-indigo-700 border-b border-indigo-100 w-1/3">${cols[1]}</th> |
| <th class="px-6 py-4 text-center text-sm font-semibold text-gray-600 border-b border-indigo-100 w-1/3">${cols[2]}</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${(slide.rows || []).map((row, i) => ` |
| <tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'} hover:bg-indigo-50/30 transition-colors"> |
| <td class="px-6 py-4 text-sm font-medium text-gray-900 border-b border-gray-100">${row.feature}</td> |
| <td class="px-6 py-4 text-center text-sm font-semibold text-indigo-600 border-b border-gray-100">${row.col1}</td> |
| <td class="px-6 py-4 text-center text-sm text-gray-500 border-b border-gray-100">${row.col2}</td> |
| </tr> |
| `).join('')} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'compare3') { |
| const headers = slide.headers || ['Option A', 'Option B', 'Option C']; |
| content = ` |
| <div class="w-full h-full p-12 bg-white flex flex-col justify-center"> |
| <div class="text-center space-y-2 mb-8"> |
| <h2 class="text-4xl font-bold text-gray-900">${slide.title}</h2> |
| ${slide.subtitle ? `<p class="text-xl text-gray-600">${slide.subtitle}</p>` : ''} |
| </div> |
| <div class="max-w-5xl mx-auto w-full"> |
| <div class="overflow-hidden rounded-2xl border border-gray-200 shadow-sm"> |
| <table class="w-full"> |
| <thead> |
| <tr class="bg-gradient-to-r from-indigo-50 via-purple-50 to-pink-50"> |
| <th class="px-4 py-4 text-left text-sm font-semibold text-gray-900 border-b border-gray-200 w-1/4"></th> |
| <th class="px-4 py-4 text-center text-sm font-bold text-indigo-700 border-b border-gray-200">${headers[0]}</th> |
| <th class="px-4 py-4 text-center text-sm font-bold text-purple-700 border-b border-gray-200">${headers[1]}</th> |
| <th class="px-4 py-4 text-center text-sm font-bold text-pink-700 border-b border-gray-200">${headers[2]}</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${(slide.rows || []).map((row, i) => ` |
| <tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'} hover:bg-gray-50/50 transition-colors"> |
| <td class="px-4 py-4 text-sm font-medium text-gray-900 border-b border-gray-100">${row.feature}</td> |
| <td class="px-4 py-4 text-center text-sm text-gray-700 border-b border-gray-100">${row.col1}</td> |
| <td class="px-4 py-4 text-center text-sm text-gray-700 border-b border-gray-100">${row.col2}</td> |
| <td class="px-4 py-4 text-center text-sm text-gray-700 border-b border-gray-100">${row.col3}</td> |
| </tr> |
| `).join('')} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| `; |
| } else if (slide.layout === 'custom-html') { |
| content = slide.htmlContent || '<div class="flex items-center justify-center h-full text-gray-400 text-xl">No HTML content defined</div>'; |
| } |
| |
| container.innerHTML = content; |
| document.getElementById('preview-counter').textContent = `${this.previewIndex + 1} / ${this.slides.length}`; |
| lucide.createIcons(); |
| } |
| |
| previewNext() { |
| if (this.previewIndex < this.slides.length - 1) { |
| this.previewIndex++; |
| this.renderFullPreview(); |
| } |
| } |
| |
| previewPrev() { |
| if (this.previewIndex > 0) { |
| this.previewIndex--; |
| this.renderFullPreview(); |
| } |
| } |
| |
| exportJSON() { |
| const data = JSON.stringify(this.slides, null, 2); |
| const blob = new Blob([data], { type: 'application/json' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'presentation.json'; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
| |
| importJSON(input) { |
| const file = input.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| try { |
| this.slides = JSON.parse(e.target.result); |
| this.currentSlide = 0; |
| this.renderSlidesList(); |
| this.renderEditor(); |
| this.updatePreview(); |
| } catch (err) { |
| alert('Invalid JSON file'); |
| } |
| }; |
| reader.readAsText(file); |
| input.value = ''; |
| } |
| } |
| |
| // Initialize editor |
| const editor = new SlideEditor(); |
| |
| // Keyboard shortcuts |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { |
| if (editor.currentSlide < editor.slides.length - 1) { |
| editor.selectSlide(editor.currentSlide + 1); |
| } |
| } |
| if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { |
| if (editor.currentSlide > 0) { |
| editor.selectSlide(editor.currentSlide - 1); |
| } |
| } |
| }); |
| </script> |
| </body> |
| </html> |