slideshare/src/components/presentations/PresentationViewer.tsx
Michael Mainguy 1008bd4bca Implement slide preview and fix import standards project-wide
## New Feature: Full Screen Slide Preview
- Add SlidePreviewModal component for full screen slide preview in SlideEditor
- ESC key support and temporary hint for user guidance
- Proper aspect ratio handling with theme CSS inheritance
- Modal follows existing UI patterns for consistency

## Import Standards Compliance (31 files updated)
- Fix all imports to use explicit .tsx/.ts extensions per IMPORT_STANDARDS.md
- Eliminate barrel imports in App.tsx for better Vite tree shaking
- Add direct imports with explicit paths across entire codebase
- Preserve CSS imports and external library imports unchanged

## Code Architecture Improvements
- Add comprehensive CSS & Component Architecture Guidelines to CLAUDE.md
- Document modal patterns, aspect ratio handling, and CSS reuse principles
- Reference all coding standards files for consistent development workflow
- Prevent future CSS overcomplication and component duplication

## Performance Optimizations
- Enable Vite tree shaking with proper import structure
- Improve module resolution speed with explicit extensions
- Optimize build performance through direct imports

## Files Changed
- 31 TypeScript/React files with import fixes
- 2 new SlidePreviewModal files (component + CSS)
- Updated project documentation and coding guidelines
- Fixed aspect ratio CSS patterns across components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 06:52:56 -05:00

250 lines
7.9 KiB
TypeScript

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 { getTheme } from '../../themes/index.ts';
import { loggers } from '../../utils/logger.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 getTheme(presentationData.metadata.theme);
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 = () => {
// TODO: Implement full-screen presentation mode
loggers.ui.info('Full-screen presentation mode requested - feature to be implemented');
};
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>
);
};