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 { highlightCode } from '../../utils/codeHighlighter.ts'; import { renderDiagram } from '../../utils/diagramProcessor.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 ); const [renderedSlideContent, setRenderedSlideContent] = useState(''); const [isRenderingSlide, setIsRenderingSlide] = useState(false); // 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]); // Render current slide content when slide changes useEffect(() => { const renderCurrentSlide = async () => { if (!presentation || !theme || currentSlideIndex < 0 || currentSlideIndex >= presentation.slides.length) { setRenderedSlideContent(''); return; } setIsRenderingSlide(true); try { const slide = presentation.slides[currentSlideIndex]; const rendered = await renderSlideContent(slide); setRenderedSlideContent(rendered); } catch (error) { console.error('Failed to render slide:', error); setRenderedSlideContent('
Failed to render slide
'); } finally { setIsRenderingSlide(false); } }; renderCurrentSlide(); }, [presentation, theme, currentSlideIndex]); const renderSlideContent = async (slide: SlideContent): Promise => { 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 for (const [slotId, content] of Object.entries(slide.content)) { const regex = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g'); // Find the corresponding slot to determine processing type const slot = layout.slots.find(s => s.id === slotId); let processedContent = content; // Process based on slot type if (slot?.type === 'code') { // Handle code highlighting const language = slot.attributes?.['data-language'] || 'javascript'; processedContent = highlightCode(content, language); } else if (slot?.type === 'diagram') { // Handle diagram rendering processedContent = await renderDiagram(content); } else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(content))) { // Handle markdown processing processedContent = renderSlideMarkdown(content, slot?.type); } 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 totalSlides = presentation.slides.length; return (
{isRenderingSlide && (
Rendering slide...
)}
{/* Navigation indicator */}
{currentSlideIndex + 1} / {totalSlides}
← → Space: Navigate Esc: Exit
); };