| | <!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> |