import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import type { Presentation, SlideContent } from '../../types/presentation'; import type { Theme, SlideLayout } from '../../types/theme'; import { getPresentationById, updatePresentation } from '../../utils/presentationStorage'; import { getTheme } from '../../themes'; import { renderTemplateWithSampleData } from '../../utils/templateRenderer'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer'; 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 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'); console.error('Error saving slide:', 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

{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 === 'text' && slot.id.includes('content') ? (