Add presentation import/export and fix template styling

• Add JSON import/export functionality to presentations list
• Fix HTML sanitizer to allow style tags in layout templates
• Add comprehensive SVG attributes for Mermaid diagram markers
• Create LLM prompt generator for presentation JSON format
• Add sample presentation showcasing SlideShare features
• Clean up diagram-slide layout template

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-09-12 08:23:08 -05:00
parent 18d653cc2d
commit 7ad433f260
9 changed files with 675 additions and 22 deletions

View File

@ -0,0 +1,300 @@
# Presentation JSON Generator System Prompt
You are a presentation JSON generator for a slide authoring and presentation tool. Your task is to create valid JSON files that can be imported into the presentation system.
## JSON Structure
Every presentation MUST follow this exact JSON structure:
```json
{
"metadata": {
"id": "unique-id-here",
"name": "Presentation Name",
"description": "Brief description of the presentation",
"theme": "default",
"aspectRatio": "16:9",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"slides": [
{
"id": "slide-unique-id",
"layoutId": "layout-name",
"content": {
"slot-name": "content for this slot"
},
"notes": "Optional presenter notes",
"order": 0
}
]
}
```
## Field Requirements
### Metadata Fields:
- **id**: Generate a unique ID using format: `pres-${timestamp}-${random}`
- **name**: Human-readable presentation title
- **description**: Brief description (can be empty string)
- **theme**: Must be "default" (currently only available theme)
- **aspectRatio**: Must be one of: "16:9", "4:3", or "16:10"
- **createdAt/updatedAt**: ISO 8601 timestamps
### Slide Fields:
- **id**: Unique slide ID, format: `slide-${index}-${timestamp}`
- **layoutId**: MUST match one of the available layouts (see below)
- **content**: Object mapping slot names to their content
- **notes**: Optional presenter notes (can be omitted or empty string)
- **order**: Sequential number starting from 0
## Available Layouts and Their Slots
### 1. title-slide
**Purpose**: Opening slide with main title
**Slots**:
- `title`: Main presentation title (required)
Example:
```json
{
"layoutId": "title-slide",
"content": {
"title": "Introduction to Machine Learning"
}
}
```
### 2. content-slide
**Purpose**: Standard content slide with title and body text
**Slots**:
- `title`: Slide title (required)
- `content`: Main content area (multiline)
Example:
```json
{
"layoutId": "content-slide",
"content": {
"title": "What is Machine Learning?",
"content": "Machine learning is a subset of artificial intelligence that enables systems to learn and improve from experience without being explicitly programmed."
}
}
```
### 3. 2-content-blocks
**Purpose**: Side-by-side content comparison
**Slots**:
- `title`: Slide title (required)
- `content1`: First content block (required)
- `content2`: Second content block (required)
Example:
```json
{
"layoutId": "2-content-blocks",
"content": {
"title": "Supervised vs Unsupervised Learning",
"content1": "Supervised Learning:\n• Uses labeled data\n• Predicts outcomes\n• Examples: Classification, Regression",
"content2": "Unsupervised Learning:\n• Uses unlabeled data\n• Finds patterns\n• Examples: Clustering, Dimensionality Reduction"
}
}
```
### 4. image-slide
**Purpose**: Display an image with title
**Slots**:
- `title`: Slide title (required)
- `image`: Image data URL or path
- `image-alt`: Alt text for accessibility (hidden field)
Example:
```json
{
"layoutId": "image-slide",
"content": {
"title": "Neural Network Architecture",
"image": "data:image/png;base64,iVBORw0KG...",
"image-alt": "Diagram showing layers of a neural network"
}
}
```
### 5. code-slide
**Purpose**: Display code with syntax highlighting
**Slots**:
- `title`: Code example title (required)
- `code`: JavaScript code (multiline)
- `notes`: Optional explanation
Example:
```json
{
"layoutId": "code-slide",
"content": {
"title": "Simple Neural Network in JavaScript",
"code": "class NeuralNetwork {\n constructor(inputNodes, hiddenNodes, outputNodes) {\n this.inputNodes = inputNodes;\n this.hiddenNodes = hiddenNodes;\n this.outputNodes = outputNodes;\n }\n\n train(inputs, targets) {\n // Training logic here\n }\n}",
"notes": "This example shows a basic neural network class structure"
}
}
```
### 6. markdown-slide
**Purpose**: Rich text content with markdown formatting
**Slots**:
- `title`: Slide title (required)
- `content`: Markdown content (multiline)
Supported Markdown:
- Headers: `#`, `##`, `###`
- Bold: `**text**`
- Italic: `*text*`
- Lists: `-` or `1.`
- Code: `` `inline` `` or code blocks
- Links: `[text](url)`
- Blockquotes: `> quote`
- Tables: GitHub-flavored markdown
Example:
```json
{
"layoutId": "markdown-slide",
"content": {
"title": "Key Concepts",
"content": "## Machine Learning Types\n\n### Supervised Learning\n- **Classification**: Predicting categories\n- **Regression**: Predicting continuous values\n\n### Unsupervised Learning\n- **Clustering**: Grouping similar data\n- **Dimensionality Reduction**: Simplifying complex data\n\n> \"The goal is to turn data into information, and information into insight.\" - Carly Fiorina"
}
}
```
### 7. diagram-slide
**Purpose**: Display Mermaid diagrams
**Slots**:
- `title`: Diagram title (required)
- `diagram`: Mermaid diagram syntax (multiline)
- `content2`: Additional content (required)
- `notes`: Optional explanation
Example:
```json
{
"layoutId": "diagram-slide",
"content": {
"title": "ML Pipeline",
"diagram": "graph LR\n A[Data Collection] --> B[Data Preprocessing]\n B --> C[Feature Engineering]\n C --> D[Model Training]\n D --> E[Model Evaluation]\n E --> F[Deployment]",
"content2": "Each step in the pipeline is crucial for model success",
"notes": "This diagram shows the typical machine learning workflow"
}
}
```
## Content Guidelines
### Text Content:
- Use `\n` for line breaks within multiline content
- Escape quotes with backslash: `\"`
- Keep titles concise (under 60 characters)
- Use bullet points with `•` or `-`
### Markdown Content:
- Use proper markdown syntax
- Headers should start at `##` (title is already `#`)
- Use backticks for code: `` `code` ``
- Tables use pipe separators: `| Col1 | Col2 |`
### Code Content:
- Properly escape special characters
- Use `\n` for line breaks
- Maintain proper indentation with spaces
- Currently optimized for JavaScript syntax
### Diagram Content:
- Use valid Mermaid syntax
- Common types: graph, flowchart, sequenceDiagram
- Direction: LR (left-right), TD (top-down), etc.
## Complete Example
```json
{
"metadata": {
"id": "pres-1704067200000-abc123",
"name": "Introduction to Machine Learning",
"description": "A comprehensive overview of ML concepts and applications",
"theme": "default",
"aspectRatio": "16:9",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"slides": [
{
"id": "slide-0-1704067200000",
"layoutId": "title-slide",
"content": {
"title": "Introduction to Machine Learning"
},
"order": 0
},
{
"id": "slide-1-1704067200001",
"layoutId": "content-slide",
"content": {
"title": "What is Machine Learning?",
"content": "Machine learning is a subset of artificial intelligence that enables systems to learn and improve from experience without being explicitly programmed.\n\nKey characteristics:\n• Data-driven approach\n• Pattern recognition\n• Predictive capabilities\n• Continuous improvement"
},
"notes": "Emphasize the difference between traditional programming and ML",
"order": 1
},
{
"id": "slide-2-1704067200002",
"layoutId": "markdown-slide",
"content": {
"title": "Types of Machine Learning",
"content": "## Three Main Categories\n\n### 1. Supervised Learning\n**Training with labeled data**\n- Classification: Spam detection, image recognition\n- Regression: Price prediction, weather forecasting\n\n### 2. Unsupervised Learning\n**Finding patterns in unlabeled data**\n- Clustering: Customer segmentation\n- Dimensionality reduction: PCA, t-SNE\n\n### 3. Reinforcement Learning\n**Learning through interaction**\n- Game playing: Chess, Go\n- Robotics: Navigation, manipulation"
},
"order": 2
},
{
"id": "slide-3-1704067200003",
"layoutId": "code-slide",
"content": {
"title": "Simple Linear Regression",
"code": "// Simple linear regression in JavaScript\nfunction linearRegression(data) {\n const n = data.length;\n let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;\n \n for (let point of data) {\n sumX += point.x;\n sumY += point.y;\n sumXY += point.x * point.y;\n sumX2 += point.x * point.x;\n }\n \n const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);\n const intercept = (sumY - slope * sumX) / n;\n \n return { slope, intercept };\n}",
"notes": "This implements the least squares method for finding the best-fit line"
},
"order": 3
}
]
}
```
## Important Rules
1. **Layout IDs must exactly match**: Use only the 7 layouts listed above
2. **Required slots cannot be empty**: Each layout has required slots marked
3. **Use proper JSON escaping**: Escape quotes, backslashes, and special characters
4. **Maintain slide order**: Order field should be sequential starting from 0
5. **Generate unique IDs**: Each slide and presentation needs unique identifiers
6. **Theme must be "default"**: Currently the only supported theme
7. **Valid aspect ratios only**: Must be "16:9", "4:3", or "16:10"
8. **Multiline content**: Use `\n` for line breaks in content fields
## File Naming
When saving the JSON file, use this format:
- Replace spaces with `%20` or `-`
- Replace special characters with URL encoding
- Add `.json` extension
- Example: `"My Presentation!"` becomes `My%20Presentation%21.json` or `my-presentation.json`
## Validation Checklist
Before generating, verify:
- [ ] All required metadata fields are present
- [ ] Theme is "default"
- [ ] Aspect ratio is valid ("16:9", "4:3", or "16:10")
- [ ] Each slide has unique ID
- [ ] Layout IDs match available layouts exactly
- [ ] Required slots for each layout are filled
- [ ] Content is properly escaped for JSON
- [ ] Order fields are sequential from 0
- [ ] Timestamps are valid ISO 8601 format

159
onlinePresentation.json Normal file
View File

@ -0,0 +1,159 @@
{
"metadata": {
"id": "pres-1736712000000-slideshare",
"name": "SlideShare: Modern Web-Based Presentation Tool",
"description": "An overview of the SlideShare presentation authoring and delivery platform",
"theme": "default",
"aspectRatio": "16:9",
"createdAt": "2025-01-12T18:00:00.000Z",
"updatedAt": "2025-01-12T18:00:00.000Z"
},
"slides": [
{
"id": "slide-0-1736712000000",
"layoutId": "title-slide",
"content": {
"title": "SlideShare: Modern Presentation Authoring"
},
"order": 0
},
{
"id": "slide-1-1736712000001",
"layoutId": "content-slide",
"content": {
"title": "What is SlideShare?",
"content": "A powerful, browser-based presentation tool that works entirely offline.\n\n• No backend server required\n• All data stored locally in IndexedDB\n• Built with React and modern web technologies\n• Theme-based design system\n• Export and share presentations as JSON"
},
"notes": "Emphasize the offline-first approach and zero server dependency",
"order": 1
},
{
"id": "slide-2-1736712000002",
"layoutId": "markdown-slide",
"content": {
"title": "Key Features",
"content": "## Core Capabilities\n\n### 📝 **Intuitive Slide Authoring**\n- Visual slide editor with live preview\n- Multiple layout options per theme\n- Drag-and-drop content placement\n- Rich text and markdown support\n\n### 🎨 **Flexible Theming System**\n- Pre-built professional themes\n- Customizable layouts with HTML/CSS\n- Consistent design across presentations\n- Theme switching without content loss\n\n### 💾 **Local-First Architecture**\n- Works completely offline\n- IndexedDB for persistent storage\n- Export/import JSON presentations\n- No cloud dependency or subscription"
},
"order": 2
},
{
"id": "slide-3-1736712000003",
"layoutId": "2-content-blocks",
"content": {
"title": "Traditional vs SlideShare",
"content1": "**Traditional Tools:**\n\n• Require installation\n• Platform-specific\n• Subscription-based\n• Cloud dependency\n• Limited customization\n• Proprietary formats",
"content2": "**SlideShare:**\n\n• Runs in any browser\n• Cross-platform\n• Free and open\n• Works offline\n• Fully customizable\n• Open JSON format"
},
"notes": "Highlight the advantages of web-based, local-first approach",
"order": 3
},
{
"id": "slide-4-1736712000004",
"layoutId": "diagram-slide",
"content": {
"title": "Architecture Overview",
"diagram": "graph TD\n A[Browser] --> B[React Application]\n B --> C[Presentation Editor]\n B --> D[Theme Engine]\n B --> E[Storage Layer]\n C --> F[Slide Components]\n D --> G[Layout Templates]\n D --> H[CSS Styles]\n E --> I[IndexedDB]\n E --> J[JSON Export/Import]",
"content2": "Clean separation of concerns with modular architecture",
"notes": "The architecture emphasizes modularity and browser-native technologies"
},
"order": 4
},
{
"id": "slide-5-1736712000005",
"layoutId": "markdown-slide",
"content": {
"title": "Theme System Design",
"content": "## Powerful & Flexible\n\n### **Layout Templates**\n- Simple HTML with Handlebars syntax\n- Semantic slot-based content areas\n- Multiple layouts per theme\n\n### **Styling with CSS**\n- CSS custom properties for theming\n- Responsive design built-in\n- Print-friendly styles included\n\n### **Content Slots**\n```html\n<div class=\"slot\" \n data-slot=\"title\" \n data-placeholder=\"Enter title\">\n {{title}}\n</div>\n```\n\n> Designers can create themes using only HTML and CSS - no JavaScript required!"
},
"order": 5
},
{
"id": "slide-6-1736712000006",
"layoutId": "code-slide",
"content": {
"title": "Simple Theme Creation",
"code": "<!-- layout-quote.html -->\n<div class=\"slide layout-quote\">\n <blockquote class=\"slot quote-text\" \n data-slot=\"quote\"\n data-placeholder=\"Enter quote...\">\n {{quote}}\n </blockquote>\n \n <cite class=\"slot author-text\"\n data-slot=\"author\"\n data-placeholder=\"Author name\">\n — {{author}}\n </cite>\n</div>\n\n/* style.css */\n.layout-quote {\n justify-content: center;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n}\n\n.layout-quote blockquote {\n font-size: 2.5rem;\n font-style: italic;\n color: white;\n}",
"notes": "Creating a new layout is as simple as HTML and CSS"
},
"order": 6
},
{
"id": "slide-7-1736712000007",
"layoutId": "markdown-slide",
"content": {
"title": "Why It's Valuable",
"content": "## 🚀 **For Individuals**\n- **Zero Cost**: No subscriptions or licenses\n- **Privacy First**: Your data stays on your device\n- **Work Anywhere**: No internet required after initial load\n- **Full Control**: Export and own your content\n\n## 🏢 **For Organizations**\n- **Self-Hosted**: Deploy on internal networks\n- **Customizable**: Brand-aligned themes\n- **Secure**: No data leaves the organization\n- **Standardized**: Consistent presentation format\n\n## 👩‍💻 **For Developers**\n- **Open Source**: Extend and customize freely\n- **Modern Stack**: React, TypeScript, Vite\n- **Clean Architecture**: Easy to understand and modify\n- **Theme API**: Create custom themes with HTML/CSS"
},
"order": 7
},
{
"id": "slide-8-1736712000008",
"layoutId": "content-slide",
"content": {
"title": "Technical Excellence",
"content": "Built with modern best practices:\n\n• React 19 with latest patterns\n• TypeScript for type safety\n• Vite for fast development\n• IndexedDB for robust storage\n• Semantic HTML for accessibility\n• CSS custom properties for theming\n• Responsive design principles\n• Clean, maintainable codebase"
},
"notes": "Emphasize the modern, professional development approach",
"order": 8
},
{
"id": "slide-9-1736712000009",
"layoutId": "markdown-slide",
"content": {
"title": "Content Types Supported",
"content": "## Rich Content Options\n\n### 📝 **Text & Typography**\n- Plain text with formatting\n- Markdown with full syntax support\n- Custom fonts per theme\n\n### 🖼️ **Visual Elements**\n- Images with drag-and-drop upload\n- Mermaid diagrams for flowcharts\n- Code blocks with syntax highlighting\n\n### 🎯 **Layout Varieties**\n- Title slides\n- Content slides\n- Two-column layouts\n- Image showcases\n- Code demonstrations\n- Diagram presentations\n- Markdown-rich content"
},
"order": 9
},
{
"id": "slide-10-1736712000010",
"layoutId": "2-content-blocks",
"content": {
"title": "Export & Sharing",
"content1": "**Export Options:**\n\n• JSON format for data portability\n• URL-encoded filenames\n• Complete presentation structure\n• Theme information included\n• Easy backup and restore",
"content2": "**Sharing Features:**\n\n• Send JSON files directly\n• Import into any instance\n• Version control friendly\n• Collaborative workflows\n• Future: PDF export planned"
},
"order": 10
},
{
"id": "slide-11-1736712000011",
"layoutId": "diagram-slide",
"content": {
"title": "Development Workflow",
"diagram": "graph LR\n A[Create Theme] --> B[Design Layouts]\n B --> C[Author Content]\n C --> D[Preview Live]\n D --> E{Happy?}\n E -->|No| C\n E -->|Yes| F[Present]\n F --> G[Export/Share]\n \n style A fill:#667eea\n style F fill:#764ba2\n style G fill:#f093fb",
"content2": "Streamlined workflow from creation to presentation",
"notes": "The workflow is designed to be intuitive and efficient"
},
"order": 11
},
{
"id": "slide-12-1736712000012",
"layoutId": "content-slide",
"content": {
"title": "Future Roadmap",
"content": "Planned enhancements:\n\n• Additional built-in themes\n• Theme marketplace/gallery\n• PDF export functionality\n• Presenter mode with notes view\n• Animation and transitions\n• Real-time collaboration\n• Plugin system for extensions\n• Mobile app for presenting"
},
"notes": "The project has an ambitious roadmap while maintaining simplicity",
"order": 12
},
{
"id": "slide-13-1736712000013",
"layoutId": "markdown-slide",
"content": {
"title": "Get Started Today",
"content": "## 🎯 **Quick Start**\n\n### **For Users:**\n1. Open SlideShare in your browser\n2. Create a new presentation\n3. Choose a theme\n4. Add slides and content\n5. Present or export\n\n### **For Developers:**\n```bash\n# Clone the repository\ngit clone [repo-url]\n\n# Install dependencies\nnpm install\n\n# Start development\nnpm run dev\n```\n\n### **For Designers:**\n- Create custom themes with HTML/CSS\n- No programming knowledge required\n- Use the built-in theme as a template"
},
"order": 13
},
{
"id": "slide-14-1736712000014",
"layoutId": "content-slide",
"content": {
"title": "Summary",
"content": "SlideShare revolutionizes presentations:\n\n✅ Works entirely offline\n✅ No server or subscription required\n✅ Professional themes and layouts\n✅ Export and own your data\n✅ Open source and extensible\n✅ Modern web technologies\n✅ Privacy-focused design\n\nThe future of presentations is local-first, open, and free."
},
"notes": "Reinforce the key value propositions",
"order": 14
}
]
}

View File

@ -15,5 +15,5 @@
"hasMasterSlide": true
}
},
"generated": "2025-08-22T11:35:39.977Z"
"generated": "2025-09-12T13:05:24.316Z"
}

View File

@ -12,18 +12,5 @@
data-type="diagram"
data-placeholder="Enter Mermaid diagram syntax here..."
data-multiline="true">{{diagram}}</div>
<div class="slot content-slot" data-slot="content2" data-placeholder="Second content block" data-required>
<span class="fade-in-slow">
{{content2}}
</span>
</div>
<div class="slot notes-content"
data-slot="notes"
data-type="text"
data-placeholder="Optional diagram explanation..."
data-multiline="true">
{{notes}}
</div>
</div>

View File

@ -1,4 +1,15 @@
<div class="fade-in slide layout-image-slide">
<style>
.layout-image-slide .image-container {
padding-top: 5%;
}
.image-container img {
max-height: 100%;
max-width: 100%;
object-fit: contain;
}
</style>
<div class="I slide layout-image-slide">
<h1 class="slot title-slot" data-slot="title" data-placeholder="Slide Title" data-required>
{{title}}
</h1>

View File

@ -331,6 +331,7 @@
/* Mermaid diagram styling */
.mermaid-container {
width: 100%;
margin: 2rem 0;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
@ -339,6 +340,10 @@
text-align: center;
overflow-x: auto;
}
.layout-diagram-slide > .content-slot {
max-height: 50%;
border: 1px solid var(--theme-secondary);
}
.mermaid-diagram {
max-width: 100%;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Presentation } from '../../types/presentation.ts';
import { getAllPresentations, deletePresentation } from '../../utils/presentationStorage.ts';
import { getAllPresentations, deletePresentation, importPresentation } from '../../utils/presentationStorage.ts';
import { loggers } from '../../utils/logger.ts';
import { ConfirmDialog } from '../ui/ConfirmDialog.tsx';
import { AlertDialog } from '../ui/AlertDialog.tsx';
@ -27,6 +27,8 @@ export const PresentationsList: React.FC = () => {
message: string;
type?: 'error' | 'success';
}>({ isOpen: false, message: '' });
const [importing, setImporting] = useState(false);
useEffect(() => {
loadPresentations();
@ -79,6 +81,76 @@ export const PresentationsList: React.FC = () => {
};
const handleImportPresentation = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.json')) {
setAlertDialog({
isOpen: true,
message: 'Please select a valid JSON file.',
type: 'error'
});
return;
}
setImporting(true);
try {
const text = await file.text();
let jsonData;
try {
jsonData = JSON.parse(text);
} catch (parseError) {
setAlertDialog({
isOpen: true,
message: 'Invalid JSON format. Please check the file.',
type: 'error'
});
return;
}
const importedPresentation = await importPresentation(jsonData);
await loadPresentations();
setAlertDialog({
isOpen: true,
message: `Successfully imported "${importedPresentation.metadata.name}"`,
type: 'success'
});
} catch (error) {
loggers.presentation.error('Failed to import presentation', error instanceof Error ? error : new Error(String(error)));
setAlertDialog({
isOpen: true,
message: error instanceof Error ? error.message : 'Failed to import presentation. Please check the file format.',
type: 'error'
});
} finally {
setImporting(false);
// Reset the input
event.target.value = '';
}
};
const handleDownloadPresentation = (presentation: Presentation) => {
const filename = presentation.metadata.name
.replace(/[^a-zA-Z0-9]/g, (char) => encodeURIComponent(char))
.toLowerCase() + '.json';
const dataStr = JSON.stringify(presentation, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
@ -123,6 +195,23 @@ export const PresentationsList: React.FC = () => {
<h1>My Presentations</h1>
<p>Manage and organize your presentation library</p>
</div>
<div className="header-actions">
<input
type="file"
id="import-presentation"
accept=".json"
onChange={handleImportPresentation}
style={{ display: 'none' }}
disabled={importing}
/>
<Button
variant="secondary"
onClick={() => document.getElementById('import-presentation')?.click()}
disabled={importing}
>
{importing ? 'Importing...' : 'Import JSON'}
</Button>
</div>
</header>
<main className="list-content">
@ -162,6 +251,14 @@ export const PresentationsList: React.FC = () => {
>
</button>
<button
type="button"
className="action-icon"
onClick={() => handleDownloadPresentation(presentation)}
title="Download as JSON"
>
</button>
<button
type="button"
className="action-icon delete"
@ -234,6 +331,21 @@ export const PresentationsList: React.FC = () => {
</p>
</div>
<div className="footer-actions">
<input
type="file"
id="import-presentation-footer"
accept=".json"
onChange={handleImportPresentation}
style={{ display: 'none' }}
disabled={importing}
/>
<Button
variant="secondary"
onClick={() => document.getElementById('import-presentation-footer')?.click()}
disabled={importing}
>
{importing ? 'Importing...' : 'Import'}
</Button>
<Button
variant="primary"
onClick={() => navigate('/presentations/new')}

View File

@ -50,17 +50,26 @@ const DEFAULT_SLIDE_CONFIG: Required<SanitizeConfig> = {
'blockquote', 'cite',
// Code elements
'pre', 'code',
// Style element for template-specific styles
'style',
// SVG elements for diagrams
'svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon',
'text', 'tspan', 'defs', 'marker', 'pattern', 'foreignObject'
'text', 'tspan', 'defs', 'marker', 'pattern', 'foreignObject', 'use', 'symbol',
'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask'
],
allowedAttributes: [
'class', 'id', 'style', 'data-*',
'class', 'id', 'style', 'data-*', 'type',
// SVG attributes
'xmlns', 'viewBox', 'width', 'height', 'd', 'x', 'y', 'x1', 'y1', 'x2', 'y2',
'cx', 'cy', 'r', 'rx', 'ry', 'fill', 'stroke', 'stroke-width', 'stroke-dasharray',
'transform', 'points', 'font-family', 'font-size', 'text-anchor', 'dominant-baseline',
'markerWidth', 'markerHeight', 'orient', 'refX', 'refY'
// Marker attributes for arrow heads
'markerWidth', 'markerHeight', 'orient', 'refX', 'refY', 'markerUnits',
'marker-start', 'marker-end', 'marker-mid',
// Additional SVG attributes needed by Mermaid
'preserveAspectRatio', 'xmlns:xlink', 'href', 'xlink:href', 'path',
'opacity', 'fill-opacity', 'stroke-opacity', 'stroke-linejoin', 'stroke-linecap',
'stroke-miterlimit', 'patternUnits', 'patternTransform'
]
};
@ -86,16 +95,19 @@ export function sanitizeHtml(html: string, config: SanitizeConfig = {}): string
FORBID_ATTR: string[];
KEEP_CONTENT: boolean;
ALLOW_DATA_ATTR: boolean;
FORCE_BODY: boolean;
} = {
ALLOWED_TAGS: finalConfig.allowedTags,
ALLOWED_ATTR: finalConfig.allowedAttributes,
// Remove any scripts or dangerous content
FORBID_TAGS: ['script', 'object', 'embed', 'base', 'link', 'meta', 'style'],
FORBID_TAGS: ['script', 'object', 'embed', 'base', 'link', 'meta'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
// Keep content structure but remove dangerous elements
KEEP_CONTENT: true,
// Allow data attributes for slots and styling
ALLOW_DATA_ATTR: true
ALLOW_DATA_ATTR: true,
// Force style tags to be preserved
FORCE_BODY: true
};
// Add image support if requested

View File

@ -211,4 +211,71 @@ export const getPresentationsByTheme = async (theme: string): Promise<Presentati
export const getPresentationMetadata = async (): Promise<PresentationMetadata[]> => {
const presentations = await getAllPresentations();
return presentations.map(p => p.metadata);
};
/**
* Validate imported presentation structure
*/
const validatePresentation = (data: any): data is Presentation => {
// Check required metadata fields
if (!data.metadata || typeof data.metadata !== 'object') return false;
if (!data.metadata.id || typeof data.metadata.id !== 'string') return false;
if (!data.metadata.name || typeof data.metadata.name !== 'string') return false;
if (!data.metadata.theme || typeof data.metadata.theme !== 'string') return false;
if (!data.metadata.aspectRatio || !['16:9', '4:3', '16:10'].includes(data.metadata.aspectRatio)) return false;
// Check slides array
if (!Array.isArray(data.slides)) return false;
// Validate each slide
for (const slide of data.slides) {
if (!slide.id || typeof slide.id !== 'string') return false;
if (!slide.layoutId || typeof slide.layoutId !== 'string') return false;
if (!slide.content || typeof slide.content !== 'object') return false;
if (typeof slide.order !== 'number') return false;
}
return true;
};
/**
* Import a presentation from JSON
*/
export const importPresentation = async (jsonData: any): Promise<Presentation> => {
// Validate the structure
if (!validatePresentation(jsonData)) {
throw new Error('Invalid presentation format. Please check the JSON structure.');
}
const db = await initializeDB();
// Generate a new ID to avoid conflicts
const importedPresentation: Presentation = {
metadata: {
...jsonData.metadata,
id: generatePresentationId(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
slides: jsonData.slides
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([PRESENTATIONS_STORE], 'readwrite');
const store = transaction.objectStore(PRESENTATIONS_STORE);
const request = store.add(importedPresentation);
request.onerror = () => {
reject(new Error('Failed to import presentation'));
};
request.onsuccess = () => {
resolve(importedPresentation);
};
transaction.onerror = () => {
reject(new Error('Transaction failed while importing presentation'));
};
});
};