diff --git a/public/themes-manifest.json b/public/themes-manifest.json index 0a6e107..f15d1ad 100644 --- a/public/themes-manifest.json +++ b/public/themes-manifest.json @@ -12,5 +12,5 @@ "hasMasterSlide": true } }, - "generated": "2025-08-21T14:43:50.916Z" + "generated": "2025-08-21T16:18:27.749Z" } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 4d9b705..7e04051 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { NewPresentationPage } from './components/presentations/NewPresentationP import { PresentationViewer } from './components/presentations/PresentationViewer.tsx'; import { PresentationMode } from './components/presentations/PresentationMode.tsx'; import { PresentationEditor } from './components/presentations/PresentationEditor.tsx'; -import { SlideEditor } from './components/presentations/SlideEditor.tsx'; +import { SlideEditor } from './components/slide-editor/index.ts'; import { PresentationsList } from './components/presentations/PresentationsList.tsx'; import { AppHeader } from './components/AppHeader.tsx'; import { Welcome } from './components/Welcome.tsx'; diff --git a/src/components/presentations/SlideEditor.css b/src/components/presentations/SlideEditor.css deleted file mode 100644 index 3c5309a..0000000 --- a/src/components/presentations/SlideEditor.css +++ /dev/null @@ -1,740 +0,0 @@ -.slide-editor { - min-height: 100vh; - background: #f8fafc; - display: flex; - flex-direction: column; -} - -/* Header */ -.slide-editor-header { - background: white; - border-bottom: 1px solid #e2e8f0; - padding: 0.75rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; - gap: 1.5rem; - flex-wrap: wrap; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 20; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.editor-info { - display: flex; - align-items: center; - gap: 1rem; - flex: 1; - min-width: 0; -} - -.back-button { - background: none; - border: none; - color: #64748b; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - padding: 0.5rem 0.75rem; - border-radius: 0.375rem; - transition: all 0.2s ease; -} - -.back-button:hover { - background: #f1f5f9; - color: #334155; -} - -.editor-title { - flex: 1; - min-width: 0; -} - -.editor-title h1 { - margin: 0; - font-size: 1.25rem; - font-weight: 600; - color: #1e293b; -} - -.editor-title p { - margin: 0.125rem 0 0 0; - color: #64748b; - font-size: 0.75rem; -} - -.editor-actions { - display: flex; - gap: 0.75rem; - flex-shrink: 0; -} - -.preview-button { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.action-button { - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - font-size: 0.875rem; - border: none; - cursor: pointer; - transition: all 0.2s ease; - text-decoration: none; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.action-button.primary { - background: #3b82f6; - color: white; -} - -.action-button.primary:hover:not(:disabled) { - background: #2563eb; -} - -.action-button.secondary { - background: #f8fafc; - color: #64748b; - border: 1px solid #e2e8f0; -} - -.action-button.secondary:hover:not(:disabled) { - background: #f1f5f9; - color: #475569; -} - -.action-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Main Content */ -.slide-editor-content { - flex: 1; - padding: 2rem; -} - -/* Step Header */ -.step-header { - text-align: center; - margin-bottom: 1.5rem; -} - -.step-header h2 { - margin: 0 0 0.25rem 0; - font-size: 1.25rem; - font-weight: 600; - color: #1e293b; -} - -.step-header p { - margin: 0; - color: #64748b; - font-size: 0.875rem; -} - -/* Layout Selection */ -.layout-selection { - position: fixed; - top: 80px; - left: 0; - right: 0; - bottom: 0; - background: #f8fafc; - z-index: 10; - overflow-y: auto; - padding: 2rem; - box-sizing: border-box; -} - -.layouts-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); - gap: 1.5rem; -} - -.layout-card { - background: white; - border: 2px solid #e2e8f0; - border-radius: 0.75rem; - overflow: hidden; - cursor: pointer; - transition: all 0.2s ease; -} - -.layout-card:hover { - border-color: #cbd5e1; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); -} - -.layout-card.selected { - border-color: #3b82f6; - box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.2); -} - -.layout-preview { - height: 300px; - background: #f8fafc; - border-bottom: 1px solid #e2e8f0; - overflow: hidden; - position: relative; -} - -.layout-rendered { - transform: scale(0.4); - transform-origin: top left; - width: 250%; - height: 250%; - pointer-events: none; -} - -.layout-info { - padding: 1.5rem; -} - -.layout-info h3 { - margin: 0 0 0.5rem 0; - font-size: 1.125rem; - font-weight: 600; - color: #1e293b; -} - -.layout-info p { - margin: 0 0 1rem 0; - color: #64748b; - font-size: 0.875rem; - line-height: 1.4; -} - -.layout-meta { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; -} - -.slot-count { - font-size: 0.75rem; - font-weight: 500; - color: #6b7280; - background: #f1f5f9; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; -} - -.slot-types { - display: flex; - gap: 0.25rem; - flex-wrap: wrap; -} - -.slot-type-badge { - font-size: 0.625rem; - font-weight: 500; - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; - text-transform: capitalize; -} - -.slot-type-badge.title { - background-color: #fef3c7; - color: #92400e; -} - -.slot-type-badge.subtitle { - background-color: #e0e7ff; - color: #3730a3; -} - -.slot-type-badge.text { - background-color: #d1fae5; - color: #047857; -} - -.slot-type-badge.image { - background-color: #fce7f3; - color: #be185d; -} - -.slot-type-badge.video { - background-color: #ddd6fe; - color: #6b21a8; -} - -.slot-type-badge.list { - background-color: #fed7d7; - color: #c53030; -} - -/* Content Editing */ -.content-editing { - /* Use absolute positioning to break out of all DOM constraints */ - position: fixed; - top: 80px; - left: 0; - right: 0; - bottom: 0; - background: #f8fafc; - z-index: 10; - overflow: hidden; -} - -.editing-layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: stretch; - height: 100%; - width: 100%; - padding: 2rem; - box-sizing: border-box; - max-width: none; -} - -.content-form { - background: white; - border-radius: 0.75rem; - border: 1px solid #e2e8f0; - padding: 1.5rem; - overflow-y: auto; - height: 100%; - display: flex; - flex-direction: column; -} - -.content-fields { - display: flex; - flex-direction: column; - gap: 1rem; - flex: 1; - overflow-y: auto; - min-height: 0; -} - -.content-field { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.field-label { - font-weight: 500; - color: #374151; - font-size: 0.875rem; -} - -.required { - color: #dc2626; - margin-left: 0.25rem; -} - -.field-input, -.field-textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid #d1d5db; - border-radius: 0.5rem; - font-size: 0.875rem; - color: #374151; - background: white; - box-sizing: border-box; - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -.field-input:focus, -.field-textarea:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -.field-textarea { - resize: vertical; - min-height: 80px; - font-family: inherit; -} - -.content-actions { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - padding-top: 1rem; - border-top: 1px solid #e5e7eb; - margin-top: 1rem; -} - -.action-links { - display: flex; - align-items: center; -} - -.action-buttons { - display: flex; - gap: 1rem; -} - -.cancel-link { - background: none; - border: none; - color: #64748b; - font-size: 0.875rem; - font-weight: 400; - text-decoration: underline; - cursor: pointer; - padding: 0; - transition: color 0.2s ease; -} - -.cancel-link:hover:not(:disabled) { - color: #374151; - text-decoration: underline; -} - -.cancel-link:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.content-actions .action-button { - padding: 0.75rem 1.5rem; - font-size: 0.875rem; - font-weight: 500; - border-radius: 0.5rem; - border: none; - cursor: pointer; - transition: all 0.2s ease; - min-width: 120px; -} - -.content-actions .action-button.primary { - background: #3b82f6; - color: white; -} - -.content-actions .action-button.primary:hover:not(:disabled) { - background: #2563eb; -} - -.content-actions .action-button.secondary { - background: #f8fafc; - color: #64748b; - border: 1px solid #e2e8f0; -} - -.content-actions .action-button.secondary:hover:not(:disabled) { - background: #f1f5f9; - color: #475569; -} - -.content-actions .action-button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.field-hint { - margin: 0; - font-size: 0.75rem; - color: #6b7280; - font-style: italic; -} - -/* Content Preview */ -.content-preview { - background: white; - border-radius: 0.75rem; - border: 1px solid #e2e8f0; - padding: 1rem; - height: 100%; - display: flex; - flex-direction: column; -} - -.content-preview h3 { - margin: 0 0 0.5rem 0; - font-size: 1rem; - font-weight: 600; - color: #1e293b; - text-align: center; -} - -.preview-description { - margin: 0 0 1rem 0; - font-size: 0.75rem; - color: #64748b; - text-align: center; - font-style: italic; -} - -.preview-container { - border: 2px solid #e2e8f0; - border-radius: 0.75rem; - background: white; - overflow: hidden; - flex: 1; - display: flex; - flex-direction: column; -} - -.slide-preview-wrapper { - background: #f8fafc; - padding: 1rem; - display: flex; - align-items: center; - justify-content: center; - flex: 1; - position: relative; - /* Ensure this container doesn't exceed available space */ - max-height: 100%; - overflow: hidden; -} - -/* Use the global aspect ratio classes for proper slide display */ -.slide-preview-wrapper .slide-container { - /* Use a width-based approach and let aspect-ratio handle height */ - width: min(80%, 70vw); - max-height: 60vh; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); - border-radius: 0.5rem; - background: white; - border: 1px solid #e2e8f0; - overflow: hidden; -} - -/* Override global aspect ratio classes for preview context */ -.slide-preview-wrapper .slide-container.aspect-16-9 { - aspect-ratio: 16 / 9; - width: min(80%, min(70vw, 60vh * (16/9))); -} - -.slide-preview-wrapper .slide-container.aspect-4-3 { - aspect-ratio: 4 / 3; - width: min(80%, min(70vw, 60vh * (4/3))); -} - -.slide-preview-wrapper .slide-container.aspect-16-10 { - aspect-ratio: 16 / 10; - width: min(80%, min(70vw, 60vh * (16/10))); -} - -.preview-meta { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background: #f8fafc; - border-top: 1px solid #e2e8f0; - flex-wrap: wrap; - gap: 0.5rem; -} - -.layout-name { - font-size: 0.75rem; - font-weight: 500; - color: #374151; -} - -.aspect-ratio-info { - font-size: 0.625rem; - color: #3b82f6; - background: #eff6ff; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-weight: 500; -} - -.content-count { - font-size: 0.625rem; - color: #6b7280; - background: #e5e7eb; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; -} - -/* Error State */ -.editor-error { - background: #fef2f2; - border: 1px solid #fecaca; - border-radius: 0.5rem; - padding: 1rem; - margin-top: 1rem; - color: #dc2626; - text-align: center; -} - -.editor-error p { - margin: 0; - font-weight: 500; -} - -/* Loading and Error States */ -.loading-content, -.error-content, -.not-found-content { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 50vh; - text-align: center; - gap: 1rem; -} - -.loading-spinner { - color: #64748b; - font-size: 1.125rem; -} - -.error-content h2, -.not-found-content h2 { - color: #dc2626; - margin: 0; -} - -.error-content p, -.not-found-content p { - color: #64748b; - margin: 0.5rem 0 1.5rem 0; -} - -.back-link { - color: #3b82f6; - text-decoration: none; - font-weight: 500; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - border: 1px solid #e2e8f0; - transition: all 0.2s ease; -} - -.back-link:hover { - background: #eff6ff; - border-color: #3b82f6; -} - -/* Responsive Design */ -@media (max-width: 1024px) { - .content-editing { - top: 60px; - } - - .layout-selection { - top: 60px; - } - - .editing-layout { - grid-template-columns: 1fr; - gap: 1.5rem; - padding: 1rem; - } - - .content-preview { - height: 300px; - min-height: 300px; - } - - .slide-preview-wrapper { - padding: 0.5rem; - } - - /* Adjust viewport calculations for smaller screens */ - .slide-preview-wrapper .slide-container { - width: min(90%, 90vw); - max-height: 250px; - } - - .slide-preview-wrapper .slide-container.aspect-16-9, - .slide-preview-wrapper .slide-container.aspect-4-3, - .slide-preview-wrapper .slide-container.aspect-16-10 { - width: min(90%, min(90vw, 250px * var(--aspect-multiplier, 1.78))); - } -} - -@media (max-width: 768px) { - .content-editing { - top: 50px; - } - - .layout-selection { - top: 50px; - padding: 1rem; - } - - .slide-editor-header { - padding: 0.5rem 1rem; - flex-direction: column; - align-items: stretch; - gap: 0.5rem; - } - - .editor-info { - flex-direction: column; - align-items: flex-start; - gap: 1rem; - } - - .editor-actions { - justify-content: stretch; - } - - .action-button { - flex: 1; - } - - .slide-editor-content { - padding: 1rem; - } - - .layouts-grid { - grid-template-columns: 1fr; - gap: 1rem; - } - - .content-form { - padding: 1rem; - } - - .content-actions { - flex-direction: column; - align-items: stretch; - gap: 0.75rem; - } - - .action-links { - justify-content: center; - order: 2; - } - - .action-buttons { - justify-content: stretch; - order: 1; - } - - .action-buttons .action-button { - flex: 1; - } - - .preview-container { - min-height: 200px; - } -} - -/* Image slot field integration */ -.content-field .image-slot-field { - margin-top: 0.5rem; - border: 1px solid #e2e8f0; - background: white; -} - -.content-field .image-slot-field:focus-within { - border-color: #3b82f6; -} \ No newline at end of file diff --git a/src/components/presentations/SlideEditor.tsx b/src/components/presentations/SlideEditor.tsx deleted file mode 100644 index cc99992..0000000 --- a/src/components/presentations/SlideEditor.tsx +++ /dev/null @@ -1,470 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams, useNavigate, Link } from 'react-router-dom'; -import type { Presentation, SlideContent } from '../../types/presentation.ts'; -import type { Theme, SlideLayout } from '../../types/theme.ts'; -import { getPresentationById, updatePresentation } from '../../utils/presentationStorage.ts'; -import { getTheme } from '../../themes/index.ts'; -import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts'; -import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; -import { loggers } from '../../utils/logger.ts'; -import { SlidePreviewModal } from './SlidePreviewModal.tsx'; -import { ImageUploadField } from '../ui/ImageUploadField.tsx'; -import './SlideEditor.css'; - -export const SlideEditor: React.FC = () => { - const { presentationId, slideId } = useParams<{ - presentationId: string; - slideId: string; - }>(); - const navigate = useNavigate(); - - const [presentation, setPresentation] = useState(null); - const [theme, setTheme] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [saving, setSaving] = useState(false); - const [showPreview, setShowPreview] = useState(false); - - const isEditingExisting = slideId !== 'new'; - - // Editor state - const [selectedLayout, setSelectedLayout] = useState(null); - const [slideContent, setSlideContent] = useState>({}); - const [slideNotes, setSlideNotes] = useState(''); - const [currentStep, setCurrentStep] = useState<'layout' | 'content'>(isEditingExisting ? 'content' : 'layout'); - const existingSlide = isEditingExisting && presentation - ? presentation.slides.find(s => s.id === slideId) - : null; - - useEffect(() => { - const loadPresentationAndTheme = async () => { - if (!presentationId) { - setError('No presentation ID provided'); - setLoading(false); - return; - } - - try { - setLoading(true); - - // Load presentation - const presentationData = await getPresentationById(presentationId); - if (!presentationData) { - setError(`Presentation not found: ${presentationId}`); - return; - } - - setPresentation(presentationData); - - // Load theme - const themeData = await getTheme(presentationData.metadata.theme); - if (!themeData) { - setError(`Theme not found: ${presentationData.metadata.theme}`); - return; - } - - setTheme(themeData); - - // If editing existing slide, populate data - if (isEditingExisting && slideId !== 'new') { - const slide = presentationData.slides.find(s => s.id === slideId); - if (slide) { - const layout = themeData.layouts.find(l => l.id === slide.layoutId); - if (layout) { - setSelectedLayout(layout); - setSlideContent(slide.content); - setSlideNotes(slide.notes || ''); - // No need to set currentStep here since it's already 'content' for existing slides - } - } else { - setError(`Slide not found: ${slideId}`); - return; - } - } - - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load presentation'); - } finally { - setLoading(false); - } - }; - - loadPresentationAndTheme(); - }, [presentationId, slideId, isEditingExisting]); - - // Load theme CSS for layout previews - useEffect(() => { - if (theme) { - const themeStyleId = 'slide-editor-theme-style'; - const existingStyle = document.getElementById(themeStyleId); - - if (existingStyle) { - existingStyle.remove(); - } - - const link = document.createElement('link'); - link.id = themeStyleId; - link.rel = 'stylesheet'; - link.href = `${theme.basePath}/${theme.cssFile}`; - document.head.appendChild(link); - - return () => { - const styleToRemove = document.getElementById(themeStyleId); - if (styleToRemove) { - styleToRemove.remove(); - } - }; - } - }, [theme]); - - const selectLayout = (layout: SlideLayout) => { - setSelectedLayout(layout); - - // Initialize content with empty values for all slots - const initialContent: Record = {}; - layout.slots.forEach(slot => { - initialContent[slot.id] = slideContent[slot.id] || ''; - }); - setSlideContent(initialContent); - - // Automatically move to content editing after layout selection - setCurrentStep('content'); - }; - - const updateSlotContent = (slotId: string, content: string) => { - setSlideContent(prev => ({ - ...prev, - [slotId]: content - })); - }; - - const saveSlide = async () => { - if (!presentation || !selectedLayout) return; - - try { - setSaving(true); - setError(null); - - const slideData: SlideContent = { - id: isEditingExisting ? slideId! : `slide_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - layoutId: selectedLayout.id, - content: slideContent, - notes: slideNotes, - order: isEditingExisting - ? (existingSlide?.order ?? presentation.slides.length) - : presentation.slides.length - }; - - const updatedPresentation = { ...presentation }; - - if (isEditingExisting) { - // Update existing slide - const slideIndex = updatedPresentation.slides.findIndex(s => s.id === slideId); - if (slideIndex !== -1) { - updatedPresentation.slides[slideIndex] = slideData; - } - } else { - // Add new slide - updatedPresentation.slides.push(slideData); - } - - await updatePresentation(updatedPresentation); - - // Navigate back to editor with the updated slide - const slideNumber = isEditingExisting - ? (updatedPresentation.slides.findIndex(s => s.id === slideData.id) + 1) - : updatedPresentation.slides.length; - - navigate(`/presentations/${presentationId}/edit/slides/${slideNumber}`); - - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to save slide'); - loggers.presentation.error('Failed to save slide', err instanceof Error ? err : new Error(String(err))); - } finally { - setSaving(false); - } - }; - - const cancelEditing = () => { - if (isEditingExisting) { - const slideIndex = presentation?.slides.findIndex(s => s.id === slideId) ?? 0; - const slideNumber = slideIndex + 1; - navigate(`/presentations/${presentationId}/edit/slides/${slideNumber}`); - } else { - navigate(`/presentations/${presentationId}/edit/slides/1`); - } - }; - - if (loading) { - return ( -
-
-
Loading slide editor...
-
-
- ); - } - - if (error) { - return ( -
-
-

Error Loading Slide Editor

-

{error}

- ← Back to Themes -
-
- ); - } - - if (!presentation || !theme) { - return ( -
-
-

Presentation Not Found

-

The requested presentation could not be found.

- ← Back to Themes -
-
- ); - } - - return ( -
-
-
- -
-

{isEditingExisting ? 'Edit Slide' : 'Add New Slide'}

-

{presentation.metadata.name} • {theme.name} theme

-
-
- -
- {selectedLayout && currentStep === 'content' && ( - - )} - -
-
- -
- {currentStep === 'layout' && ( -
-
-

Choose a Layout

-

Select the layout that best fits your content

-
- -
- {theme.layouts.map((layout) => ( -
selectLayout(layout)} - > -
-
-
-
-

{layout.name}

-

{layout.description}

-
- {layout.slots.length} slots -
- {Array.from(new Set(layout.slots.map(slot => slot.type))).map(type => ( - - {type} - - ))} -
-
-
-
- ))} -
-
- )} - - {currentStep === 'content' && selectedLayout && presentation && ( -
-
-
-
-

Edit Slide Content

-

Fill in the content for your {selectedLayout.name} slide

-
- -
- {selectedLayout.slots.map((slot) => ( -
- - - {slot.type === 'image' ? ( - updateSlotContent(slot.id, value)} - placeholder={slot.placeholder || `Upload image or enter URL for ${slot.id}`} - className="image-slot-field" - /> - ) : slot.type === 'text' && slot.id.includes('content') ? ( -