diff --git a/src/app/globals.css b/src/app/globals.css index 04bc7ae..89771b4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -619,7 +619,7 @@ scroll-snap-type: x mandatory; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; - touch-action: pan-x pinch-zoom; + touch-action: pan-x pan-y pinch-zoom; scrollbar-width: none; -ms-overflow-style: none; gap: 1rem; @@ -643,6 +643,7 @@ display: flex; flex-direction: column; justify-content: flex-start; + padding-bottom: calc(env(safe-area-inset-bottom) + 7.25rem); overflow: visible; } } diff --git a/src/components/UnifiedQuitPlanCard.tsx b/src/components/UnifiedQuitPlanCard.tsx index fe3726f..01874a2 100644 --- a/src/components/UnifiedQuitPlanCard.tsx +++ b/src/components/UnifiedQuitPlanCard.tsx @@ -309,14 +309,19 @@ export function UnifiedQuitPlanCard({ if (!showNicotine && !showWeed) return null; return ( - + Quit Journey Plan - + {showNicotine && ( (null); + const videoRetryTimeoutRef = useRef(null); const [isVideoLoaded, setIsVideoLoaded] = useState(false); const [isVideoPlaying, setIsVideoPlaying] = useState(false); @@ -160,6 +161,35 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader checkMobile(); }, []); + const clearVideoRetryTimeout = () => { + if (videoRetryTimeoutRef.current !== null) { + window.clearTimeout(videoRetryTimeoutRef.current); + videoRetryTimeoutRef.current = null; + } + }; + + const scheduleVideoRetry = () => { + clearVideoRetryTimeout(); + videoRetryTimeoutRef.current = window.setTimeout(() => { + const video = videoRef.current; + if (!video) return; + + if (video.readyState < 2) { + video.load(); + } + + video.play().catch(() => { + // Ignore autoplay retries that still fail. + }); + }, 900); + }; + + useEffect(() => { + return () => { + clearVideoRetryTimeout(); + }; + }, []); + // Force play background video useEffect(() => { if (videoRef.current) { @@ -176,12 +206,15 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader video.playsInline = true; video.setAttribute('playsinline', 'true'); // Explicit attribute for some older browsers video.setAttribute('webkit-playsinline', 'true'); + video.preload = 'metadata'; + } else { + video.preload = 'auto'; } video.loop = true; // If already ready, set loaded immediately - if (video.readyState >= 3) { + if (video.readyState >= 2) { setIsVideoLoaded(true); } @@ -190,10 +223,20 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader if (playPromise !== undefined) { playPromise.catch((err) => { console.warn("Autoplay failed, user interaction might be needed:", err); - // If PWA, we might want to show the poster as fallback silently without error spam + if (isMobile || isPWA) { + scheduleVideoRetry(); + } }); } + + if (isMobile || isPWA) { + scheduleVideoRetry(); + } } + + return () => { + clearVideoRetryTimeout(); + }; }, [isMobile, isPWA]); useEffect(() => { @@ -366,18 +409,49 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader "x-webkit-airplay": "deny", "disableRemotePlayback": true } as any)} - preload="auto" + preload={isMobile || isPWA ? 'metadata' : 'auto'} controls={false} disablePictureInPicture onContextMenu={(e) => e.preventDefault()} onLoadedData={() => setIsVideoLoaded(true)} - onPlay={() => setIsVideoPlaying(true)} - onPlaying={() => setIsVideoPlaying(true)} - onTimeUpdate={() => { - if (!isVideoPlaying && videoRef.current && videoRef.current.currentTime > 0) { + onCanPlay={() => { + setIsVideoLoaded(true); + if (videoRef.current?.paused) { + videoRef.current.play().catch(() => { + if (isMobile || isPWA) { + scheduleVideoRetry(); + } + }); + } + }} + onPlaying={() => { + if (videoRef.current && videoRef.current.currentTime > 0.06) { setIsVideoPlaying(true); } }} + onTimeUpdate={() => { + if (!isVideoPlaying && videoRef.current && videoRef.current.currentTime > 0.06) { + setIsVideoPlaying(true); + } + }} + onWaiting={() => { + setIsVideoPlaying(false); + if (isMobile || isPWA) { + scheduleVideoRetry(); + } + }} + onStalled={() => { + setIsVideoPlaying(false); + if (isMobile || isPWA) { + scheduleVideoRetry(); + } + }} + onPause={() => { + setIsVideoPlaying(false); + }} + onError={() => { + setIsVideoPlaying(false); + }} className={cn( "absolute inset-0 w-full h-full object-cover transition-opacity duration-[1500ms] ease-in-out", isMobile ? "scale-105" : "scale-110",