slideshare/src/components/presentations/SlideEditor.tsx
Michael Mainguy d88ae6dcc3 Fix all ESLint errors and improve code quality
- Fix unused variable errors by removing unused parameters or using proper destructuring
- Fix 'prefer-const' violations by replacing 'let' with 'const' where appropriate
- Fix lexical declaration errors in switch cases by adding proper block scoping
- Replace explicit 'any' type with proper TypeScript interface for DOMPurify config
- Fix React hooks dependency warnings in useDialog hook
- Remove unused imports and variables throughout codebase

Specific fixes:
- Replace '_' parameters with proper destructuring syntax ([, value])
- Add block scopes to switch case statements in templateRenderer.ts
- Improve type safety in htmlSanitizer.ts with explicit DOMPurify interface
- Fix useCallback dependencies in useDialog hook
- Remove unused 'placeholder' parameter in generateSampleDataForSlot

All 15 ESLint errors have been resolved, improving code maintainability and consistency.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 17:50:23 -05:00

435 lines
15 KiB
TypeScript

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<Presentation | null>(null);
const [theme, setTheme] = useState<Theme | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const isEditingExisting = slideId !== 'new';
// Editor state
const [selectedLayout, setSelectedLayout] = useState<SlideLayout | null>(null);
const [slideContent, setSlideContent] = useState<Record<string, string>>({});
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<string, string> = {};
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 (
<div className="slide-editor">
<div className="loading-content">
<div className="loading-spinner">Loading slide editor...</div>
</div>
</div>
);
}
if (error) {
return (
<div className="slide-editor">
<div className="error-content">
<h2>Error Loading Slide Editor</h2>
<p>{error}</p>
<Link to="/themes" className="back-link"> Back to Themes</Link>
</div>
</div>
);
}
if (!presentation || !theme) {
return (
<div className="slide-editor">
<div className="not-found-content">
<h2>Presentation Not Found</h2>
<p>The requested presentation could not be found.</p>
<Link to="/themes" className="back-link"> Back to Themes</Link>
</div>
</div>
);
}
return (
<div className="slide-editor">
<header className="slide-editor-header">
<div className="editor-info">
<button
onClick={cancelEditing}
className="back-button"
type="button"
>
Back to Presentation
</button>
<div className="editor-title">
<h1>{isEditingExisting ? 'Edit Slide' : 'Add New Slide'}</h1>
<p>{presentation.metadata.name} {theme.name} theme</p>
</div>
</div>
<div className="editor-actions">
<button
type="button"
className="action-button primary"
onClick={saveSlide}
disabled={!selectedLayout || saving}
>
{saving ? 'Saving...' : (isEditingExisting ? 'Update Slide' : 'Add Slide')}
</button>
</div>
</header>
<main className="slide-editor-content">
{currentStep === 'layout' && (
<div className="layout-selection">
<div className="step-header">
<h2>Choose a Layout</h2>
<p>Select the layout that best fits your content</p>
</div>
<div className="layouts-grid">
{theme.layouts.map((layout) => (
<div
key={layout.id}
className={`layout-card ${selectedLayout?.id === layout.id ? 'selected' : ''}`}
onClick={() => selectLayout(layout)}
>
<div className="layout-preview">
<div
className="layout-rendered"
dangerouslySetInnerHTML={{
__html: sanitizeSlideTemplate(renderTemplateWithSampleData(layout.htmlTemplate, layout))
}}
/>
</div>
<div className="layout-info">
<h3>{layout.name}</h3>
<p>{layout.description}</p>
<div className="layout-meta">
<span className="slot-count">{layout.slots.length} slots</span>
<div className="slot-types">
{Array.from(new Set(layout.slots.map(slot => slot.type))).map(type => (
<span key={type} className={`slot-type-badge ${type}`}>
{type}
</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{currentStep === 'content' && selectedLayout && presentation && (
<div className="content-editing">
<div className="editing-layout">
<div className="content-form">
<div className="step-header">
<h2>Edit Slide Content</h2>
<p>Fill in the content for your {selectedLayout.name} slide</p>
</div>
<div className="content-fields">
{selectedLayout.slots.map((slot) => (
<div key={slot.id} className="content-field">
<label htmlFor={slot.id} className="field-label">
{slot.id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
{slot.required && <span className="required">*</span>}
</label>
{slot.type === 'text' && slot.id.includes('content') ? (
<textarea
id={slot.id}
value={slideContent[slot.id] || ''}
onChange={(e) => updateSlotContent(slot.id, e.target.value)}
placeholder={slot.placeholder || `Enter ${slot.id}`}
className="field-textarea"
rows={4}
/>
) : (
<input
id={slot.id}
type={slot.type === 'image' ? 'url' : 'text'}
value={slideContent[slot.id] || ''}
onChange={(e) => updateSlotContent(slot.id, e.target.value)}
placeholder={slot.placeholder || `Enter ${slot.id}`}
className="field-input"
/>
)}
{slot.placeholder && (
<p className="field-hint">{slot.placeholder}</p>
)}
</div>
))}
<div className="content-field">
<label htmlFor="slide-notes" className="field-label">
Speaker Notes
</label>
<textarea
id="slide-notes"
value={slideNotes}
onChange={(e) => setSlideNotes(e.target.value)}
placeholder="Add notes for this slide (optional)"
className="field-textarea"
rows={3}
/>
</div>
<div className="content-actions">
<div className="action-links">
<button
type="button"
className="cancel-link"
onClick={cancelEditing}
disabled={saving}
>
Cancel editing
</button>
</div>
<div className="action-buttons">
<button
type="button"
className="action-button primary"
onClick={saveSlide}
disabled={saving}
>
{saving ? 'Saving...' : (isEditingExisting ? 'Update Slide' : 'Save Slide')}
</button>
</div>
</div>
</div>
</div>
<div className="content-preview">
<h3>Live Preview</h3>
<p className="preview-description">
Updates automatically as you type
</p>
<div className="preview-container">
<div className="slide-preview-wrapper">
<div
className={`slide-container ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
dangerouslySetInnerHTML={{
__html: sanitizeSlideTemplate(renderTemplateWithContent(selectedLayout, slideContent))
}}
/>
</div>
<div className="preview-meta">
<span className="layout-name">{selectedLayout.name}</span>
<span className="aspect-ratio-info">
{presentation.metadata.aspectRatio || '16:9'} aspect ratio
</span>
<span className="content-count">
{Object.values(slideContent).filter(v => v.trim()).length} / {selectedLayout.slots.length} slots filled
</span>
</div>
</div>
</div>
</div>
</div>
)}
{error && (
<div className="editor-error">
<p>Error: {error}</p>
</div>
)}
</main>
</div>
);
};
// Helper function to render template with actual content
const renderTemplateWithContent = (layout: SlideLayout, content: Record<string, string>): string => {
let rendered = layout.htmlTemplate;
// Replace content placeholders
Object.entries(content).forEach(([slotId, value]) => {
const placeholder = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
rendered = rendered.replace(placeholder, value || '');
});
// Clean up any remaining placeholders
rendered = rendered.replace(/\{\{[^}]+\}\}/g, '');
return rendered;
};