import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { X, ChevronLeft, ChevronRight, Download, ExternalLink, FileText, Presentation, SkipBack, SkipForward, Edit, Loader2, } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { constructHtmlPreviewUrl } from '@/lib/utils/url'; interface SlideMetadata { title: string; filename: string; file_path: string; preview_url: string; created_at: string; } interface PresentationMetadata { presentation_name: string; title: string; description: string; slides: Record; created_at: string; updated_at: string; } interface FullScreenPresentationViewerProps { isOpen: boolean; onClose: () => void; presentationName?: string; sandboxUrl?: string; initialSlide?: number; onPDFDownload?: (setIsDownloadingPDF: (isDownloading: boolean) => void) => void; } export function FullScreenPresentationViewer({ isOpen, onClose, presentationName, sandboxUrl, initialSlide = 1, onPDFDownload, }: FullScreenPresentationViewerProps) { const [metadata, setMetadata] = useState(null); const [currentSlide, setCurrentSlide] = useState(initialSlide); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [showControls, setShowControls] = useState(true); const [showEditor, setShowEditor] = useState(false); const [isDownloadingPDF, setIsDownloadingPDF] = useState(false); // Create a stable refresh timestamp when metadata changes (like PresentationViewer) const refreshTimestamp = useMemo(() => Date.now(), [metadata]); const slides = metadata ? Object.entries(metadata.slides) .map(([num, slide]) => ({ number: parseInt(num), ...slide })) .sort((a, b) => a.number - b.number) : []; const totalSlides = slides.length; // Helper function to sanitize filename (matching backend logic) const sanitizeFilename = (name: string): string => { return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase(); }; // Load metadata const loadMetadata = useCallback(async () => { if (!presentationName || !sandboxUrl) return; setIsLoading(true); setError(null); try { // Sanitize the presentation name to match backend directory creation const sanitizedPresentationName = sanitizeFilename(presentationName); const metadataUrl = constructHtmlPreviewUrl( sandboxUrl, `presentations/${sanitizedPresentationName}/metadata.json` ); const response = await fetch(`${metadataUrl}?t=${Date.now()}`, { cache: 'no-cache', headers: { 'Cache-Control': 'no-cache' } }); if (response.ok) { const data = await response.json(); setMetadata(data); } else { setError('Failed to load presentation metadata'); } } catch (err) { console.error('Error loading metadata:', err); setError('Failed to load presentation metadata'); } finally { setIsLoading(false); } }, [presentationName, sandboxUrl]); useEffect(() => { if (isOpen) { loadMetadata(); setCurrentSlide(initialSlide); } }, [isOpen, loadMetadata, initialSlide]); // Reload metadata when exiting editor mode to refresh with latest changes useEffect(() => { let timeoutId: NodeJS.Timeout; if (!showEditor) { // Add a small delay to allow the editor to save changes timeoutId = setTimeout(() => { loadMetadata(); }, 300); } return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [showEditor, loadMetadata]); // Navigation functions const goToNextSlide = useCallback(() => { if (currentSlide < totalSlides) { setCurrentSlide(prev => prev + 1); } }, [currentSlide, totalSlides]); const goToPreviousSlide = useCallback(() => { if (currentSlide > 1) { setCurrentSlide(prev => prev - 1); } }, [currentSlide]); // Keyboard navigation useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { // Prevent default for all our handled keys const handledKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' ', 'Home', 'End', 'Escape']; if (handledKeys.includes(e.key)) { e.preventDefault(); e.stopPropagation(); } switch (e.key) { case 'ArrowLeft': case 'ArrowUp': goToPreviousSlide(); break; case 'ArrowRight': case 'ArrowDown': case ' ': goToNextSlide(); break; case 'Home': setCurrentSlide(1); break; case 'End': setCurrentSlide(totalSlides); break; case 'Escape': if (showEditor) { setShowEditor(false); } else { onClose(); } break; } }; // Add event listener to document with capture to ensure we get the events first document.addEventListener('keydown', handleKeyDown, true); return () => document.removeEventListener('keydown', handleKeyDown, true); }, [isOpen, goToNextSlide, goToPreviousSlide, totalSlides, onClose, showEditor]); // Always show controls useEffect(() => { if (isOpen) { setShowControls(true); } }, [isOpen]); const currentSlideData = slides.find(slide => slide.number === currentSlide); // Memoized slide iframe component with proper scaling (matching PresentationViewer) const SlideIframe = useMemo(() => { const SlideIframeComponent = React.memo(({ slide }: { slide: SlideMetadata & { number: number } }) => { const [containerRef, setContainerRef] = useState(null); const [scale, setScale] = useState(1); useEffect(() => { if (containerRef) { const updateScale = () => { const containerWidth = containerRef.offsetWidth; const containerHeight = containerRef.offsetHeight; // Calculate scale to fit 1920x1080 into container while maintaining aspect ratio const scaleX = containerWidth / 1920; const scaleY = containerHeight / 1080; const newScale = Math.min(scaleX, scaleY); // Only update if scale actually changed to prevent unnecessary re-renders if (Math.abs(newScale - scale) > 0.001) { setScale(newScale); } }; // Use a debounced version for resize events to prevent excessive updates let resizeTimeout: NodeJS.Timeout; const debouncedUpdateScale = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(updateScale, 100); }; updateScale(); window.addEventListener('resize', debouncedUpdateScale); return () => { window.removeEventListener('resize', debouncedUpdateScale); clearTimeout(resizeTimeout); }; } }, [containerRef, scale]); if (!sandboxUrl) { return (

No slide content to preview

); } const slideUrl = constructHtmlPreviewUrl(sandboxUrl, slide.file_path); // Add cache-busting to iframe src to ensure fresh content const slideUrlWithCacheBust = `${slideUrl}?t=${refreshTimestamp}`; return (