import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import type { Presentation, SlideContent } from '../../types/presentation.ts'; import type { Theme } from '../../types/theme.ts'; import { getPresentationById } from '../../utils/presentationStorage.ts'; import { getTheme } from '../../themes/index.ts'; import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; import { loggers } from '../../utils/logger.ts'; import './PresentationMode.css'; export const PresentationMode: React.FC = () => { const { presentationId } = useParams<{ presentationId: string }>(); const navigate = useNavigate(); const [presentation, setPresentation] = useState(null); const [theme, setTheme] = useState(null); const [currentSlideIndex, setCurrentSlideIndex] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Keyboard navigation handler const handleKeyPress = useCallback((event: KeyboardEvent) => { if (!presentation || presentation.slides.length === 0) return; switch (event.key) { case 'ArrowRight': case 'Space': case 'Enter': event.preventDefault(); setCurrentSlideIndex(prev => Math.min(prev + 1, presentation.slides.length - 1) ); break; case 'ArrowLeft': event.preventDefault(); setCurrentSlideIndex(prev => Math.max(prev - 1, 0)); break; case 'Home': event.preventDefault(); setCurrentSlideIndex(0); break; case 'End': event.preventDefault(); setCurrentSlideIndex(presentation.slides.length - 1); break; case 'Escape': event.preventDefault(); exitPresentationMode(); break; default: // Handle number keys for direct slide navigation const slideNumber = parseInt(event.key); if (slideNumber >= 1 && slideNumber <= presentation.slides.length) { event.preventDefault(); setCurrentSlideIndex(slideNumber - 1); } break; } }, [presentation, navigate, presentationId]); // Set up keyboard listeners useEffect(() => { document.addEventListener('keydown', handleKeyPress); return () => { document.removeEventListener('keydown', handleKeyPress); }; }, [handleKeyPress]); // Load presentation and theme 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); // Load theme CSS const cssLink = document.getElementById('presentation-theme-css') as HTMLLinkElement; const cssPath = `${themeData.basePath}/${themeData.cssFile}`; if (cssLink) { cssLink.href = cssPath; } else { const newCssLink = document.createElement('link'); newCssLink.id = 'presentation-theme-css'; newCssLink.rel = 'stylesheet'; newCssLink.href = cssPath; document.head.appendChild(newCssLink); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load presentation'); } finally { setLoading(false); } }; loadPresentationAndTheme(); // Cleanup theme CSS on unmount return () => { const cssLink = document.getElementById('presentation-theme-css'); if (cssLink) { cssLink.remove(); } }; }, [presentationId]); const exitPresentationMode = () => { navigate(`/presentations/${presentationId}/view/slides/${currentSlideIndex + 1}`); }; const renderSlideContent = (slide: SlideContent): string => { if (!theme) return ''; const layout = theme.layouts.find(l => l.id === slide.layoutId); if (!layout) { loggers.ui.warn(`Layout ${slide.layoutId} not found in theme ${theme.name}`); return '
Layout not found
'; } let renderedTemplate = layout.htmlTemplate; // Replace template variables with slide content Object.entries(slide.content).forEach(([slotId, content]) => { const regex = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g'); renderedTemplate = renderedTemplate.replace(regex, content); }); // Handle conditional blocks and clean up remaining variables renderedTemplate = renderTemplateWithSampleData(renderedTemplate, layout); return sanitizeSlideTemplate(renderedTemplate); }; if (loading) { return (
Loading presentation...
); } if (error) { return (

Error Loading Presentation

{error}

); } if (!presentation || !theme) { return (

Presentation Not Found

); } if (presentation.slides.length === 0) { return (

No Slides Available

This presentation is empty.

); } const currentSlide = presentation.slides[currentSlideIndex]; const totalSlides = presentation.slides.length; const renderedSlideContent = renderSlideContent(currentSlide); return (
{/* Navigation indicator */}
{currentSlideIndex + 1} / {totalSlides}
← → Space: Navigate Esc: Exit
); };