diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index 665c3a4..6ce8170 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -21,7 +21,7 @@ import BaseButton from './BaseButton'; import CardBox from './CardBox'; import { OfflineToggle } from './Offline/OfflineToggle'; import LayoutGuest from '../layouts/Guest'; -import { getPageTitle } from '../config'; +import { getPageTitle, baseURLApi } from '../config'; import { PRELOAD_CONFIG } from '../config/preload.config'; import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator'; import { useTransitionPlayback } from '../hooks/useTransitionPlayback'; @@ -31,7 +31,6 @@ import { markPresignedUrlFailed, isRelativeStoragePath, } from '../lib/assetUrl'; -import { baseURLApi } from '../config'; import { buildElementStyle } from '../lib/elementStyles'; import type { RuntimeProject, @@ -214,6 +213,9 @@ export default function RuntimePresentation({ const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isFullscreen, setIsFullscreen] = useState(false); + const [isBackgroundReady, setIsBackgroundReady] = useState(true); + const [pendingTransitionComplete, setPendingTransitionComplete] = + useState(false); const transitionVideoRef = useRef(null); @@ -289,7 +291,7 @@ export default function RuntimePresentation({ }); // Integrate useTransitionPlayback hook for smooth transitions (matches Constructor pattern) - const { isBuffering } = useTransitionPlayback({ + const { isBuffering, phase: transitionPhase } = useTransitionPlayback({ videoRef: transitionVideoRef, transition: transitionPreview ? { @@ -300,17 +302,23 @@ export default function RuntimePresentation({ } : null, onComplete: (targetPageId) => { + const video = transitionVideoRef.current; if (targetPageId) { const targetPage = pages.find((p) => p.id === targetPageId); waitForPageImages(targetPage || null).then(() => { + // Mark background as not ready - new image will need to load + setIsBackgroundReady(false); setSelectedPageId(targetPageId); setPageHistory((prev) => [...prev, targetPageId]); - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setTransitionPreview(null); - }); - }); + // Signal that transition is complete and waiting for background + setPendingTransitionComplete(true); }); + } else { + // No target page - clean up and remove overlay + video?.removeAttribute('src'); + video?.load(); + setTransitionPreview(null); + setPendingTransitionComplete(false); } }, features: { @@ -359,6 +367,21 @@ export default function RuntimePresentation({ document.removeEventListener('fullscreenchange', handleFullscreenChange); }, []); + // Remove transition overlay when background is ready + useEffect(() => { + if (pendingTransitionComplete && isBackgroundReady) { + const video = transitionVideoRef.current; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + video?.removeAttribute('src'); + video?.load(); + setTransitionPreview(null); + setPendingTransitionComplete(false); + }); + }); + } + }, [pendingTransitionComplete, isBackgroundReady]); + // Load presentation data useEffect(() => { let isCancelled = false; @@ -471,6 +494,17 @@ export default function RuntimePresentation({ } }, [selectedPage]); + // Handle background ready state for pages without images or with videos + useEffect(() => { + // If no background image, or if there's a video (video takes over), mark as ready + if ( + !selectedPage?.background_image_url || + selectedPage?.background_video_url + ) { + setIsBackgroundReady(true); + } + }, [selectedPage?.background_image_url, selectedPage?.background_video_url]); + const navigateToPage = useCallback( async ( targetPageId: string, @@ -488,8 +522,10 @@ export default function RuntimePresentation({ isReverse: isBack, }); } else { - // Direct navigation - wait for images first + // Direct navigation - wait for images first, then switch await waitForPageImages(targetPage); + // Mark background as loading (Image onLoad will set it back to true) + setIsBackgroundReady(false); setSelectedPageId(targetPageId); setPageHistory((prev) => [...prev, targetPageId]); } @@ -756,6 +792,24 @@ export default function RuntimePresentation({ backgroundPosition: 'center', }} > + {/* Background image element - ensures proper loading for waitForPageImages() */} + {backgroundImageUrl && !backgroundVideoUrl && ( +
+ setIsBackgroundReady(true)} + onError={() => setIsBackgroundReady(true)} + /> +
+ )} + {/* Background video */} {backgroundVideoUrl && (