## 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>
250 lines
7.9 KiB
TypeScript
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>
|
|
);
|
|
}; |