import React, { useState, useEffect} from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts'; import { loggers } from '../../utils/logger.ts'; import './PresentationMode.css'; import {usePresentationLoader} from "./hooks/usePresentationLoader.ts"; import type { SlideContent } from '../../types/presentation.ts'; import {useKeyboardNavigation} from "./hooks/useKeyboardNavigation.ts"; export const PresentationMode: React.FC = () => { const { presentationId, slideNumber } = useParams<{ presentationId: string; slideNumber: string; }>(); const navigate = useNavigate(); const [currentSlideIndex, setCurrentSlideIndex] = useState( slideNumber ? parseInt(slideNumber, 10) - 1 : 0 ); // Load presentation and theme const { presentation, theme, loading, error } = usePresentationLoader(presentationId); // Navigate to specific slide and update URL const goToSlide = (slideIndex: number) => { if (!presentation) return; const clampedIndex = Math.max(0, Math.min(slideIndex, presentation.slides.length - 1)); setCurrentSlideIndex(clampedIndex); navigate(`/presentations/${presentationId}/present/${clampedIndex + 1}`, { replace: true }); }; // Keyboard navigation handler useKeyboardNavigation({ totalSlides: presentation?.slides.length || 0, currentSlideIndex, onNavigate: goToSlide }); // Sync current slide index with URL parameter useEffect(() => { if (slideNumber) { const newIndex = parseInt(slideNumber, 10) - 1; if (newIndex >= 0 && newIndex !== currentSlideIndex) { setCurrentSlideIndex(newIndex); } } }, [slideNumber]); 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'); // Find the corresponding slot to determine if it should be processed as markdown const slot = layout.slots.find(s => s.id === slotId); const shouldProcessAsMarkdown = slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(content)); const processedContent = shouldProcessAsMarkdown ? renderSlideMarkdown(content, slot?.type) : content; renderedTemplate = renderedTemplate.replace(regex, processedContent); }); // 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
); };