Remove PresentationViewer component and view functionality
- Remove view buttons from presentations list (both icon and card actions) - Delete PresentationViewer component and CSS files - Remove view route from App.tsx routing - Streamline presentation actions to Edit, Present, and Delete only 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e7f8650ff4
commit
a84546b30b
@ -12,5 +12,5 @@
|
||||
"hasMasterSlide": true
|
||||
}
|
||||
},
|
||||
"generated": "2025-08-21T18:19:19.993Z"
|
||||
"generated": "2025-08-21T18:25:50.144Z"
|
||||
}
|
@ -0,0 +1,360 @@
|
||||
# Theme Creation Guidelines
|
||||
|
||||
## Theme Structure
|
||||
|
||||
Each theme must follow this directory structure:
|
||||
```
|
||||
themes/
|
||||
theme-name/
|
||||
style.css # Theme styles and metadata
|
||||
master-slide.html # Master slide template (optional)
|
||||
layouts/ # Layout templates directory
|
||||
layout1.html
|
||||
layout2.html
|
||||
...
|
||||
```
|
||||
|
||||
## CSS Theme Metadata
|
||||
|
||||
Theme metadata MUST be included as comments at the top of `style.css`:
|
||||
|
||||
```css
|
||||
/*
|
||||
* Theme: [theme-id]
|
||||
* Name: [Display Name]
|
||||
* Description: [Theme description]
|
||||
* Author: [Author Name] ([email])
|
||||
* Version: [version]
|
||||
*/
|
||||
```
|
||||
|
||||
## CSS Variables System
|
||||
|
||||
### Required Theme Variables
|
||||
Define these CSS custom properties in `:root` for consistent theming:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Colors */
|
||||
--theme-primary: #color; /* Primary brand color */
|
||||
--theme-secondary: #color; /* Secondary accent color */
|
||||
--theme-accent: #color; /* Interactive accent color */
|
||||
--theme-background: #color; /* Slide background */
|
||||
--theme-text: #color; /* Primary text color */
|
||||
--theme-text-secondary: #color; /* Secondary text color */
|
||||
|
||||
/* Typography */
|
||||
--theme-font-heading: 'Font', fallback;
|
||||
--theme-font-body: 'Font', fallback;
|
||||
--theme-font-code: 'Font', fallback;
|
||||
|
||||
/* Layout */
|
||||
--slide-padding: 5%; /* Slide edge padding */
|
||||
--content-max-width: 90%; /* Max content width */
|
||||
}
|
||||
```
|
||||
|
||||
### Required Base Slide Styling
|
||||
Always include this base styling that works with the global `.slide-container` classes:
|
||||
|
||||
```css
|
||||
.slide-container .slide-content,
|
||||
.slide {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--theme-background);
|
||||
color: var(--theme-text);
|
||||
font-family: var(--theme-font-body);
|
||||
padding: var(--slide-padding);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
## Layout HTML Templates
|
||||
|
||||
### Required Layout Structure
|
||||
Each layout HTML file must:
|
||||
|
||||
1. **Use semantic class naming**: `.layout-[layout-name]`
|
||||
2. **Include slot elements** for editable content
|
||||
3. **Use Handlebars syntax** for template variables
|
||||
|
||||
### Slot System
|
||||
Slots are editable areas defined with specific data attributes:
|
||||
|
||||
```html
|
||||
<div class="slot [slot-type]"
|
||||
data-slot="[slot-id]"
|
||||
data-placeholder="[placeholder-text]"
|
||||
data-required
|
||||
data-multiline="true"
|
||||
data-accept="image/*"
|
||||
data-hidden="true">
|
||||
{{slot-id}}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Slot Data Attributes:
|
||||
- `data-slot="[id]"`: Unique identifier for the slot
|
||||
- `data-placeholder="[text]"`: Placeholder text when empty
|
||||
- `data-required`: Mark slot as required (optional)
|
||||
- `data-multiline="true"`: Allow multiline text input (optional)
|
||||
- `data-accept="image/*"`: For image slots (optional)
|
||||
- `data-hidden="true"`: Hide from direct editing (optional)
|
||||
|
||||
#### Common Slot Types:
|
||||
- **Title slots**: `data-slot="title"`
|
||||
- **Text content**: `data-slot="content"`
|
||||
- **Images**: `data-slot="image"` with `data-accept="image/*"`
|
||||
- **Subtitles**: `data-slot="subtitle"`
|
||||
|
||||
### Layout CSS Naming Convention
|
||||
Style layouts using the pattern: `.layout-[layout-name]`
|
||||
|
||||
```css
|
||||
.layout-my-layout,
|
||||
.slide-container .layout-my-layout {
|
||||
/* Layout-specific styles */
|
||||
}
|
||||
|
||||
.layout-my-layout .slot[data-slot="title"] {
|
||||
/* Slot-specific styles */
|
||||
}
|
||||
```
|
||||
|
||||
## Creating New Layouts
|
||||
|
||||
### 1. Create Layout HTML Template
|
||||
Create `themes/[theme]/layouts/my-layout.html`:
|
||||
|
||||
```html
|
||||
<div class="slide layout-my-layout">
|
||||
<h1 class="slot title-slot"
|
||||
data-slot="title"
|
||||
data-placeholder="Slide Title"
|
||||
data-required>
|
||||
{{title}}
|
||||
</h1>
|
||||
|
||||
<div class="slot content-area"
|
||||
data-slot="content"
|
||||
data-placeholder="Your content here..."
|
||||
data-multiline="true">
|
||||
{{content}}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. Add Layout Styles to theme CSS
|
||||
Add to the theme's `style.css`:
|
||||
|
||||
```css
|
||||
/* My Layout */
|
||||
.layout-my-layout,
|
||||
.slide-container .layout-my-layout {
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layout-my-layout .slot[data-slot="title"] {
|
||||
font-size: clamp(1.5rem, 6vw, 2.5rem);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layout-my-layout .slot[data-slot="content"] {
|
||||
flex: 1;
|
||||
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||
text-align: left;
|
||||
}
|
||||
```
|
||||
|
||||
## Standard Slot Styles
|
||||
|
||||
### Required Slot Base Styles
|
||||
```css
|
||||
.slot {
|
||||
position: relative;
|
||||
border: 2px dashed transparent;
|
||||
min-height: 2rem;
|
||||
transition: border-color 0.2s ease;
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.slot:hover,
|
||||
.slot.editing {
|
||||
border-color: var(--theme-accent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.slot.empty {
|
||||
border-color: var(--theme-secondary);
|
||||
opacity: 0.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slot.empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--theme-text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
```
|
||||
|
||||
### Slot Type Styles
|
||||
```css
|
||||
/* Text slots */
|
||||
.slot[data-type="title"] {
|
||||
font-family: var(--theme-font-heading);
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slot[data-type="subtitle"] {
|
||||
font-family: var(--theme-font-heading);
|
||||
line-height: 1.4;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slot[data-type="text"] {
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Image slots */
|
||||
.slot[data-type="image"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.slot[data-type="image"] img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
## Master Slide Templates
|
||||
|
||||
Master slides provide unchangeable content that appears on all slides. Create `master-slide.html`:
|
||||
|
||||
```html
|
||||
<div class="master-slide footer">
|
||||
{{footerText}}
|
||||
</div>
|
||||
```
|
||||
|
||||
Style master slide elements:
|
||||
```css
|
||||
.master-slide {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.master-slide.footer {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### Aspect Ratio Support
|
||||
Include aspect ratio specific adjustments:
|
||||
|
||||
```css
|
||||
.slide-container.aspect-16-9 .slide-content,
|
||||
.slide-container.aspect-16-9 .slide {
|
||||
--slide-padding: 4%;
|
||||
--content-max-width: 85%;
|
||||
}
|
||||
|
||||
.slide-container.aspect-4-3 .slide-content,
|
||||
.slide-container.aspect-4-3 .slide {
|
||||
--slide-padding: 6%;
|
||||
--content-max-width: 80%;
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile Responsive
|
||||
Add mobile breakpoints:
|
||||
|
||||
```css
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--slide-padding: 3%;
|
||||
--content-max-width: 95%;
|
||||
}
|
||||
|
||||
.layout-my-layout .slot[data-slot="title"] {
|
||||
font-size: clamp(1.2rem, 5vw, 2rem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Print Support
|
||||
Include print styles for presentation export:
|
||||
|
||||
```css
|
||||
@media print {
|
||||
.slide {
|
||||
page-break-after: always;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.slot {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.slot.empty::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use clamp() for responsive typography**: `font-size: clamp(min, preferred, max)`
|
||||
2. **Leverage CSS custom properties**: Makes themes easily customizable
|
||||
3. **Follow existing naming conventions**: `.layout-[name]`, `.slot[data-slot="name"]`
|
||||
4. **Test across aspect ratios**: Ensure layouts work in 16:9, 4:3, and 16:10
|
||||
5. **Consider accessibility**: Use sufficient color contrast and readable fonts
|
||||
6. **Use semantic HTML**: Proper heading hierarchy and meaningful class names
|
||||
7. **Keep layouts flexible**: Use flexbox/grid for responsive behavior
|
||||
|
||||
## Template Variables (Handlebars)
|
||||
|
||||
Use Handlebars syntax for dynamic content:
|
||||
- `{{variableName}}` - Simple variable
|
||||
- `{{#if condition}}...{{/if}}` - Conditional rendering
|
||||
- `{{#image}}...{{/image}}` - Check if image exists
|
||||
|
||||
Common variables:
|
||||
- `{{title}}` - Slide title
|
||||
- `{{content}}` - Main content
|
||||
- `{{image}}` - Image source
|
||||
- `{{imageAlt}}` - Image alt text
|
||||
- `{{footerText}}` - Footer text
|
@ -4,7 +4,6 @@ import { ThemeDetailPage } from './components/themes/ThemeDetailPage.tsx';
|
||||
import { LayoutDetailPage } from './components/themes/LayoutDetailPage.tsx';
|
||||
import { LayoutPreviewPage } from './components/themes/LayoutPreviewPage.tsx';
|
||||
import { NewPresentationPage } from './components/presentations/NewPresentationPage.tsx';
|
||||
import { PresentationViewer } from './components/presentations/PresentationViewer.tsx';
|
||||
import { PresentationMode } from './components/presentations/PresentationMode.tsx';
|
||||
import { PresentationEditor } from './components/presentations/PresentationEditor.tsx';
|
||||
import { SlideEditor } from './components/slide-editor/SlideEditor.tsx';
|
||||
@ -25,7 +24,6 @@ function App() {
|
||||
<Route path="/presentations" element={<PresentationsList />} />
|
||||
<Route path="/presentations/new" element={<NewPresentationPage />} />
|
||||
<Route path="/presentations/:presentationId/edit/slides/:slideNumber" element={<PresentationEditor />} />
|
||||
<Route path="/presentations/:presentationId/view/slides/:slideNumber" element={<PresentationViewer />} />
|
||||
<Route path="/presentations/:presentationId/present/:slideNumber" element={<PresentationMode />} />
|
||||
<Route path="/presentations/:presentationId/slide/:slideId/edit" element={<SlideEditor />} />
|
||||
<Route path="/themes" element={<ThemeBrowser />} />
|
||||
|
@ -1,390 +0,0 @@
|
||||
.presentation-viewer {
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.viewer-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.presentation-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.presentation-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.presentation-title h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.presentation-description {
|
||||
margin: 0.25rem 0 0 0;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.presentation-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-badge {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.slide-counter {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.viewer-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.action-button.secondary:hover {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.action-button.large {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.viewer-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-presentation {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.empty-content h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #374151;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-content p {
|
||||
margin: 0 0 2rem 0;
|
||||
color: #6b7280;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Slide Area */
|
||||
.slide-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.slide-container {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.slide-content h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.slide-content p {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.slide-preview {
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 0.5rem;
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.slide-placeholder {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.slide-placeholder p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.slide-notes {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.slide-notes h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.slide-notes p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.slide-error {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Slide Navigation */
|
||||
.slide-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
color: #374151;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-button:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.nav-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slide-thumbnails {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
max-width: 300px;
|
||||
overflow-x: auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid #e2e8f0;
|
||||
background: white;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumbnail:hover {
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.thumbnail.active {
|
||||
border-color: #3b82f6;
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* Loading and Error States */
|
||||
.loading-content,
|
||||
.error-content,
|
||||
.not-found-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 50vh;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.error-content h2,
|
||||
.not-found-content h2 {
|
||||
color: #dc2626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-content p,
|
||||
.not-found-content p {
|
||||
color: #64748b;
|
||||
margin: 0.5rem 0 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.viewer-header {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.presentation-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.presentation-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.viewer-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.viewer-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.slide-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.slide-navigation {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.slide-thumbnails {
|
||||
justify-content: center;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
@ -1,249 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import type { Presentation } from '../../types/presentation.ts';
|
||||
import type { Theme } from '../../types/theme.ts';
|
||||
import { getPresentationById } from '../../utils/presentationStorage.ts';
|
||||
import { loadTheme } from '../../utils/themeLoader.ts';
|
||||
import './PresentationViewer.css';
|
||||
|
||||
export const PresentationViewer: React.FC = () => {
|
||||
const { presentationId, slideNumber } = useParams<{
|
||||
presentationId: string;
|
||||
slideNumber: 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 currentSlideIndex = slideNumber ? parseInt(slideNumber, 10) - 1 : 0;
|
||||
|
||||
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 loadTheme(presentationData.metadata.theme, false);
|
||||
if (!themeData) {
|
||||
setError(`Theme not found: ${presentationData.metadata.theme}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setTheme(themeData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load presentation');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPresentationAndTheme();
|
||||
}, [presentationId]);
|
||||
|
||||
const goToSlide = (slideIndex: number) => {
|
||||
if (!presentation) return;
|
||||
|
||||
const slideNum = slideIndex + 1;
|
||||
navigate(`/presentations/${presentationId}/view/slides/${slideNum}`);
|
||||
};
|
||||
|
||||
const goToPreviousSlide = () => {
|
||||
if (currentSlideIndex > 0) {
|
||||
goToSlide(currentSlideIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextSlide = () => {
|
||||
if (presentation && currentSlideIndex < presentation.slides.length - 1) {
|
||||
goToSlide(currentSlideIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const editPresentation = () => {
|
||||
if (!presentation) return;
|
||||
|
||||
const slideNum = Math.max(1, currentSlideIndex + 1);
|
||||
navigate(`/presentations/${presentationId}/edit/slides/${slideNum}`);
|
||||
};
|
||||
|
||||
const enterPresentationMode = () => {
|
||||
if (!presentation) return;
|
||||
navigate(`/presentations/${presentationId}/present/${currentSlideIndex + 1}`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="presentation-viewer">
|
||||
<div className="loading-content">
|
||||
<div className="loading-spinner">Loading presentation...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="presentation-viewer">
|
||||
<div className="error-content">
|
||||
<h2>Error Loading Presentation</h2>
|
||||
<p>{error}</p>
|
||||
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!presentation || !theme) {
|
||||
return (
|
||||
<div className="presentation-viewer">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const currentSlide = presentation.slides[currentSlideIndex];
|
||||
const totalSlides = presentation.slides.length;
|
||||
|
||||
return (
|
||||
<div className="presentation-viewer">
|
||||
<header className="viewer-header">
|
||||
<div className="presentation-info">
|
||||
<Link to="/themes" className="back-link">← Back</Link>
|
||||
<div className="presentation-title">
|
||||
<h1>{presentation.metadata.name}</h1>
|
||||
{presentation.metadata.description && (
|
||||
<p className="presentation-description">{presentation.metadata.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="presentation-meta">
|
||||
<span className="theme-badge">Theme: {theme.name}</span>
|
||||
<span className="slide-counter">
|
||||
{totalSlides === 0 ? 'No slides' : `Slide ${currentSlideIndex + 1} of ${totalSlides}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="viewer-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
onClick={editPresentation}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-button primary"
|
||||
onClick={enterPresentationMode}
|
||||
>
|
||||
Present
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="viewer-content">
|
||||
{totalSlides === 0 ? (
|
||||
<div className="empty-presentation">
|
||||
<div className="empty-content">
|
||||
<h2>This presentation is empty</h2>
|
||||
<p>Switch to edit mode to add slides</p>
|
||||
<button
|
||||
type="button"
|
||||
className="action-button primary large"
|
||||
onClick={editPresentation}
|
||||
>
|
||||
Edit Presentation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="slide-area">
|
||||
<div className="slide-container">
|
||||
{currentSlide ? (
|
||||
<div className="slide-content">
|
||||
<h3>Slide {currentSlideIndex + 1}</h3>
|
||||
<p>Layout: {currentSlide.layoutId}</p>
|
||||
<div className="slide-preview">
|
||||
{/* TODO: Render actual slide content based on layout */}
|
||||
<div className="slide-placeholder">
|
||||
<p>Slide content will be rendered here</p>
|
||||
<p>Layout: {currentSlide.layoutId}</p>
|
||||
<p>Content slots: {Object.keys(currentSlide.content).length}</p>
|
||||
</div>
|
||||
</div>
|
||||
{currentSlide.notes && (
|
||||
<div className="slide-notes">
|
||||
<h4>Notes:</h4>
|
||||
<p>{currentSlide.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="slide-error">
|
||||
<p>Invalid slide number</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="slide-navigation">
|
||||
<button
|
||||
type="button"
|
||||
className="nav-button"
|
||||
onClick={goToPreviousSlide}
|
||||
disabled={currentSlideIndex === 0}
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
|
||||
<div className="slide-thumbnails">
|
||||
{presentation.slides.map((slide, index) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
type="button"
|
||||
className={`thumbnail ${index === currentSlideIndex ? 'active' : ''}`}
|
||||
onClick={() => goToSlide(index)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="nav-button"
|
||||
onClick={goToNextSlide}
|
||||
disabled={currentSlideIndex === totalSlides - 1}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -83,10 +83,6 @@ export const PresentationsList: React.FC = () => {
|
||||
navigate(`/presentations/${id}/edit/slides/${slideNumber}`);
|
||||
};
|
||||
|
||||
const handleViewPresentation = (id: string, slideCount: number) => {
|
||||
const slideNumber = slideCount > 0 ? 1 : 1; // Always go to slide 1, or empty state
|
||||
navigate(`/presentations/${id}/view/slides/${slideNumber}`);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
@ -171,14 +167,6 @@ export const PresentationsList: React.FC = () => {
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon"
|
||||
onClick={() => handleViewPresentation(presentation.metadata.id, presentation.slides.length)}
|
||||
title="View presentation"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon delete"
|
||||
@ -227,13 +215,6 @@ export const PresentationsList: React.FC = () => {
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="card-action secondary"
|
||||
onClick={() => handleViewPresentation(presentation.metadata.id, presentation.slides.length)}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
{presentation.slides.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
Loading…
Reference in New Issue
Block a user