Compare commits

...

3 Commits

Author SHA1 Message Date
7ad433f260 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>
2025-09-12 08:23:08 -05:00
18d653cc2d Fix Mermaid diagram text visibility and styling issues
- Add essential CSS for proper text display in diagram nodes and edges
- Fix foreignObject text container inheritance for proper styling
- Ensure node labels and edge labels are visible with correct fonts
- Add responsive sizing for mobile devices
- Remove complex styling that was causing text clipping issues
- Simplify diagram container styling for better compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 06:40:29 -05:00
72cce3af0f Add Mermaid diagram support with dedicated diagram-slide layout
- Implement complete Mermaid.js integration for rich diagram rendering
- Add diagram-slide.html layout with title, diagram, and notes slots
- Create diagramProcessor.ts with async rendering and error handling
- Add comprehensive SVG element support to HTML sanitizer
- Implement async template rendering system for diagram processing
- Add SlidePreview component with loading states for better UX
- Support all major Mermaid diagram types (flowchart, sequence, gantt, pie, etc.)
- Add dark theme integration with custom color scheme
- Include diagram-specific styling and responsive design
- Add diagram field editor with syntax highlighting styling

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 05:41:50 -05:00
14 changed files with 1182 additions and 44 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

@ -7,6 +7,7 @@
"2-content-blocks",
"code-slide",
"content-slide",
"diagram-slide",
"image-slide",
"markdown-slide",
"title-slide"
@ -14,5 +15,5 @@
"hasMasterSlide": true
}
},
"generated": "2025-08-22T02:19:52.970Z"
"generated": "2025-09-12T13:05:24.316Z"
}

View File

@ -0,0 +1,16 @@
<div class="slide layout-diagram-slide">
<h1 class="slot title-slot"
data-slot="title"
data-type="title"
data-placeholder="Diagram Title"
data-required>
{{title}}
</h1>
<div class="slot diagram-content"
data-slot="diagram"
data-type="diagram"
data-placeholder="Enter Mermaid diagram syntax here..."
data-multiline="true">{{diagram}}</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%;
@ -720,6 +725,186 @@ pre.code-content::before {
font-style: italic;
}
/* Diagram editor textarea styling */
.field-textarea.diagram-field {
font-family: var(--theme-font-code);
font-size: 0.9rem;
line-height: 1.4;
background: #0f1419;
color: #e6e6e6;
border: 1px solid var(--theme-accent);
border-radius: 4px;
padding: 0.75rem;
white-space: pre;
tab-size: 2;
}
.field-textarea.diagram-field::placeholder {
color: #6a6a6a;
font-style: italic;
}
/* Diagram slide layout */
.layout-diagram-slide,
.slide-container .layout-diagram-slide {
justify-content: flex-start;
align-items: stretch;
text-align: center;
padding: 2rem;
}
.layout-diagram-slide .slot[data-slot="title"] {
font-size: clamp(1.5rem, 4vw, 2rem);
margin-bottom: 1.5rem;
text-align: center;
color: var(--theme-text);
}
.layout-diagram-slide .slot[data-slot="diagram"] {
flex: 1;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.layout-diagram-slide .slot[data-slot="notes"] {
font-size: clamp(0.9rem, 2vw, 1rem);
color: var(--theme-text-secondary);
line-height: 1.5;
text-align: center;
max-height: 20vh;
overflow-y: auto;
}
/* Enhanced Mermaid diagram styling */
.mermaid-container {
}
.mermaid-diagram svg {
}
/* Minimal essential styling for Mermaid text visibility */
.mermaid-diagram svg .nodeLabel,
.mermaid-diagram svg .edgeLabel {
font-size: 14px !important;
font-family: var(--theme-font-body) !important;
font-weight: 600 !important;
}
/* Fix for foreignObject text containers */
.mermaid-diagram svg foreignObject div {
color: inherit !important;
font-size: inherit !important;
font-family: inherit !important;
}
.mermaid-diagram svg foreignObject .nodeLabel p,
.mermaid-diagram svg foreignObject .edgeLabel p {
color: inherit !important;
margin: 0 !important;
padding: 0 !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
}
/* Edge label background visibility */
.mermaid-diagram svg .edgeLabel .labelBkg {
}
.mermaid-diagram svg .edgeLabel .labelBkg p {
}
.mermaid-diagram {
}
/* Responsive diagram sizing */
@media (max-width: 768px) {
.mermaid-container {
padding: 1rem;
margin: 0.5rem 0;
}
.mermaid-diagram svg {
max-width: 100%;
height: auto;
}
.mermaid-diagram svg .nodeLabel,
.mermaid-diagram svg .edgeLabel {
font-size: 12px !important;
}
}
.mermaid-diagram {
width: 100%;
height: auto;
max-width: 100%;
margin: 0 auto;
}
.diagram-error {
color: #ff6b6b;
background: rgba(255, 107, 107, 0.1);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #ff6b6b;
font-family: var(--theme-font-code);
font-size: 0.9rem;
text-align: left;
}
.diagram-error strong {
display: block;
margin-bottom: 0.5rem;
color: #ff8a8a;
}
.error-content {
background: rgba(0, 0, 0, 0.2);
padding: 0.75rem;
border-radius: 4px;
margin-top: 0.75rem;
white-space: pre-wrap;
font-size: 0.8rem;
overflow-x: auto;
}
/* Preview loading states */
.preview-loading,
.slide-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
color: var(--theme-text-secondary);
font-size: 0.9rem;
z-index: 10;
}
.loading-spinner {
width: 2rem;
height: 2rem;
border: 2px solid var(--theme-secondary);
border-top: 2px solid var(--theme-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive adjustments for enhanced features */
@media (max-width: 768px) {
.layout-code-slide {

View File

@ -5,6 +5,7 @@ import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts';
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
import { highlightCode } from '../../utils/codeHighlighter.ts';
import { renderDiagram } from '../../utils/diagramProcessor.ts';
import { loggers } from '../../utils/logger.ts';
import './PresentationMode.css';
import {usePresentationLoader} from "./hooks/usePresentationLoader.ts";
@ -21,6 +22,8 @@ export const PresentationMode: React.FC = () => {
const [currentSlideIndex, setCurrentSlideIndex] = useState(
slideNumber ? parseInt(slideNumber, 10) - 1 : 0
);
const [renderedSlideContent, setRenderedSlideContent] = useState<string>('');
const [isRenderingSlide, setIsRenderingSlide] = useState(false);
// Load presentation and theme
const { presentation, theme, loading, error } = usePresentationLoader(presentationId);
@ -52,7 +55,31 @@ export const PresentationMode: React.FC = () => {
}
}, [slideNumber]);
const renderSlideContent = (slide: SlideContent): string => {
// Render current slide content when slide changes
useEffect(() => {
const renderCurrentSlide = async () => {
if (!presentation || !theme || currentSlideIndex < 0 || currentSlideIndex >= presentation.slides.length) {
setRenderedSlideContent('');
return;
}
setIsRenderingSlide(true);
try {
const slide = presentation.slides[currentSlideIndex];
const rendered = await renderSlideContent(slide);
setRenderedSlideContent(rendered);
} catch (error) {
console.error('Failed to render slide:', error);
setRenderedSlideContent('<div class="error">Failed to render slide</div>');
} finally {
setIsRenderingSlide(false);
}
};
renderCurrentSlide();
}, [presentation, theme, currentSlideIndex]);
const renderSlideContent = async (slide: SlideContent): Promise<string> => {
if (!theme) return '';
const layout = theme.layouts.find(l => l.id === slide.layoutId);
@ -64,7 +91,7 @@ export const PresentationMode: React.FC = () => {
let renderedTemplate = layout.htmlTemplate;
// Replace template variables with slide content
Object.entries(slide.content).forEach(([slotId, content]) => {
for (const [slotId, content] of Object.entries(slide.content)) {
const regex = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
// Find the corresponding slot to determine processing type
@ -77,13 +104,16 @@ export const PresentationMode: React.FC = () => {
// Handle code highlighting
const language = slot.attributes?.['data-language'] || 'javascript';
processedContent = highlightCode(content, language);
} else if (slot?.type === 'diagram') {
// Handle diagram rendering
processedContent = await renderDiagram(content);
} else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(content))) {
// Handle markdown processing
processedContent = renderSlideMarkdown(content, slot?.type);
}
renderedTemplate = renderedTemplate.replace(regex, processedContent);
});
}
// Handle conditional blocks and clean up remaining variables
renderedTemplate = renderTemplateWithSampleData(renderedTemplate, layout);
@ -140,16 +170,21 @@ export const PresentationMode: React.FC = () => {
);
}
const currentSlide = presentation.slides[currentSlideIndex];
const totalSlides = presentation.slides.length;
const renderedSlideContent = renderSlideContent(currentSlide);
return (
<div className="presentation-mode fullscreen">
<div className="slide-container">
{isRenderingSlide && (
<div className="slide-loading">
<div className="loading-spinner"></div>
<span>Rendering slide...</span>
</div>
)}
<div
className={`slide-content ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
dangerouslySetInnerHTML={{ __html: renderedSlideContent }}
style={{ opacity: isRenderingSlide ? 0.5 : 1 }}
/>
</div>

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

@ -1,11 +1,10 @@
import React from 'react';
import type { Presentation } from '../../types/presentation.ts';
import type { SlideLayout } from '../../types/theme.ts';
import { renderTemplateWithContent } from './utils.ts';
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
import { ImageUploadField } from '../ui/ImageUploadField.tsx';
import { CancelLink } from '../ui/buttons/CancelLink.tsx';
import { ActionButton } from '../ui/buttons/ActionButton.tsx';
import { SlidePreview } from './SlidePreview.tsx';
interface ContentEditorProps {
presentation: Presentation;
@ -45,11 +44,14 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
);
}
if (slot.type === 'code' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) {
if (slot.type === 'code' || slot.type === 'diagram' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) {
const getPlaceholder = () => {
if (slot.type === 'code') {
return slot.placeholder || 'Enter your JavaScript code here...';
}
if (slot.type === 'diagram') {
return slot.placeholder || 'Enter Mermaid diagram syntax here...';
}
if (slot.type === 'markdown') {
return slot.placeholder || 'Enter markdown content (e.g., **bold**, *italic*, - list items)';
}
@ -58,6 +60,7 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
const getRows = () => {
if (slot.type === 'code') return 8;
if (slot.type === 'diagram') return 10;
if (slot.type === 'markdown') return 6;
return 4;
};
@ -68,7 +71,11 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
value={slideContent[slot.id] || ''}
onChange={(e) => onSlotContentChange(slot.id, e.target.value)}
placeholder={getPlaceholder()}
className={`field-textarea ${slot.type === 'code' ? 'code-field' : slot.type === 'markdown' ? 'markdown-field' : ''}`}
className={`field-textarea ${
slot.type === 'code' ? 'code-field' :
slot.type === 'diagram' ? 'diagram-field' :
slot.type === 'markdown' ? 'markdown-field' : ''
}`}
rows={getRows()}
style={slot.type === 'code' ? { fontFamily: 'var(--theme-font-code)' } : undefined}
/>
@ -149,31 +156,11 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
</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>
<SlidePreview
presentation={presentation}
selectedLayout={selectedLayout}
slideContent={slideContent}
/>
</div>
</div>
);

View File

@ -0,0 +1,71 @@
import React, { useState, useEffect } from 'react';
import type { Presentation } from '../../types/presentation.ts';
import type { SlideLayout } from '../../types/theme.ts';
import { renderTemplateWithContent } from './utils.ts';
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
interface SlidePreviewProps {
presentation: Presentation;
selectedLayout: SlideLayout;
slideContent: Record<string, string>;
}
export const SlidePreview: React.FC<SlidePreviewProps> = ({
presentation,
selectedLayout,
slideContent,
}) => {
const [previewHtml, setPreviewHtml] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const updatePreview = async () => {
setIsLoading(true);
try {
const rendered = await renderTemplateWithContent(selectedLayout, slideContent);
const sanitized = sanitizeSlideTemplate(rendered);
setPreviewHtml(sanitized);
} catch (error) {
console.error('Preview rendering failed:', error);
setPreviewHtml('<div class="preview-error">Preview unavailable</div>');
} finally {
setIsLoading(false);
}
};
updatePreview();
}, [selectedLayout, slideContent]);
return (
<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">
{isLoading && (
<div className="preview-loading">
<div className="loading-spinner"></div>
<span>Rendering preview...</span>
</div>
)}
<div
className={`slide-container ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
dangerouslySetInnerHTML={{ __html: previewHtml }}
style={{ opacity: isLoading ? 0.5 : 1 }}
/>
</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>
);
};

View File

@ -1,13 +1,14 @@
import type { SlideLayout } from '../../types/theme.ts';
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
import { highlightCode } from '../../utils/codeHighlighter.ts';
import { renderDiagram } from '../../utils/diagramProcessor.ts';
// Helper function to render template with actual content
export const renderTemplateWithContent = (layout: SlideLayout, content: Record<string, string>): string => {
export const renderTemplateWithContent = async (layout: SlideLayout, content: Record<string, string>): Promise<string> => {
let rendered = layout.htmlTemplate;
// Replace content placeholders
Object.entries(content).forEach(([slotId, value]) => {
for (const [slotId, value] of Object.entries(content)) {
const placeholder = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
// Find the corresponding slot to determine processing type
@ -20,13 +21,16 @@ export const renderTemplateWithContent = (layout: SlideLayout, content: Record<s
// Handle code highlighting
const language = slot.attributes?.['data-language'] || 'javascript';
processedValue = highlightCode(value || '', language);
} else if (slot?.type === 'diagram') {
// Handle diagram rendering
processedValue = await renderDiagram(value || '');
} else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(value || ''))) {
// Handle markdown processing
processedValue = renderSlideMarkdown(value || '', slot?.type);
}
rendered = rendered.replace(placeholder, processedValue);
});
}
// Clean up any remaining placeholders
rendered = rendered.replace(/\{\{[^}]+\}\}/g, '');

View File

@ -0,0 +1,170 @@
import mermaid from 'mermaid';
/**
* Mermaid diagram processor for slide content
* Uses Mermaid library for diagram rendering
*/
// Configure Mermaid for slide-safe diagram rendering
mermaid.initialize({
startOnLoad: false, // We'll manually trigger rendering
theme: 'dark', // Match our dark theme
themeVariables: {
primaryColor: '#9563eb',
primaryTextColor: '#ffffff',
primaryBorderColor: '#94748b',
lineColor: '#eea5e9',
sectionBkColor: '#001112',
altSectionBkColor: '#94748b',
gridColor: '#94a4ab',
secondaryColor: '#94748b',
tertiaryColor: '#eea5e9'
},
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: 16,
flowchart: {
htmlLabels: true,
curve: 'basis'
},
sequence: {
diagramMarginX: 50,
diagramMarginY: 10,
actorMargin: 50,
width: 150,
height: 65,
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
messageMargin: 35
},
gantt: {
titleTopMargin: 25,
barHeight: 20,
gridLineStartPadding: 35,
fontSize: 11
}
});
/**
* Renders Mermaid diagram from text syntax
* @param diagramText - The Mermaid diagram syntax
* @returns Promise resolving to HTML string with rendered diagram
*/
export const renderDiagram = async (diagramText: string): Promise<string> => {
if (!diagramText || typeof diagramText !== 'string') {
return '<div class="diagram-error">No diagram content provided</div>';
}
try {
// Clean and validate the diagram text
const cleanedText = diagramText.trim();
if (!cleanedText) {
return '<div class="diagram-error">Empty diagram content</div>';
}
// Generate unique ID for this diagram
const diagramId = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
console.log('Rendering Mermaid diagram:', cleanedText);
// Render the diagram directly without parse validation (parse might be causing issues)
const { svg } = await mermaid.render(diagramId, cleanedText);
console.log('Mermaid SVG generated:', svg.substring(0, 100) + '...');
// Wrap in container with proper styling
const result = `<div class="mermaid-container">
<div class="mermaid-diagram" id="${diagramId}">${svg}</div>
</div>`;
console.log('Final HTML result:', result.substring(0, 200) + '...');
return result;
} catch (error) {
console.error('Mermaid diagram rendering failed:', error);
// Return error message with the original text for debugging
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return `<div class="diagram-error">
<strong>Diagram Error:</strong> ${errorMessage}
<pre class="error-content">${diagramText.replace(/[<>&"']/g, (char) => {
const escapeMap: Record<string, string> = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;',
"'": '&#x27;'
};
return escapeMap[char];
})}</pre>
</div>`;
}
};
/**
* Checks if content appears to be Mermaid diagram syntax
* Used to determine whether to process content as a diagram
*/
export const isDiagramContent = (content: string): boolean => {
if (!content || typeof content !== 'string') {
return false;
}
const trimmed = content.trim();
// Common Mermaid diagram types
const mermaidPatterns = [
/^graph\s+(TD|TB|BT|RL|LR)/i, // Flowchart
/^flowchart\s+(TD|TB|BT|RL|LR)/i, // Flowchart (newer syntax)
/^sequenceDiagram/i, // Sequence diagram
/^classDiagram/i, // Class diagram
/^stateDiagram/i, // State diagram
/^erDiagram/i, // Entity relationship diagram
/^gantt/i, // Gantt chart
/^pie/i, // Pie chart
/^journey/i, // User journey
/^gitgraph/i, // Git graph
/^mindmap/i, // Mind map
/^timeline/i, // Timeline
];
return mermaidPatterns.some(pattern => pattern.test(trimmed));
};
/**
* Sample diagram content for testing and previews
*/
export const SAMPLE_DIAGRAM_CONTENT = {
flowchart: `graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug]
D --> B
C --> E[End]`,
sequence: `sequenceDiagram
participant A as Alice
participant B as Bob
participant C as Charlie
A->>B: Hello Bob!
B->>C: Hello Charlie!
C->>A: Hello Alice!`,
pie: `pie title Project Status
"Completed" : 65
"In Progress" : 25
"Planning" : 10`,
gantt: `gantt
title Project Timeline
dateFormat YYYY-MM-DD
section Planning
Research :done, des1, 2024-01-01, 2024-01-15
Design :done, des2, 2024-01-10, 2024-01-25
section Development
Frontend :active, dev1, 2024-01-20, 2024-02-15
Backend :dev2, 2024-02-01, 2024-02-28
Testing :test1, 2024-02-20, 2024-03-10`
};

View File

@ -49,10 +49,27 @@ const DEFAULT_SLIDE_CONFIG: Required<SanitizeConfig> = {
// Quotes
'blockquote', 'cite',
// Code elements
'pre', 'code'
'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', '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',
// 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'
]
};
@ -78,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'));
};
});
};