11"use client" ;
22
3- import { useRef , VideoHTMLAttributes , useState , useEffect } from "react" ;
3+ import { useRef , VideoHTMLAttributes , useState , useEffect , ReactElement } from "react" ;
44import { useGallery } from "./GalleryViewer" ;
55
66interface GalleryVideoProps extends VideoHTMLAttributes < HTMLVideoElement > {
@@ -13,17 +13,56 @@ export const GalleryVideo = ({ enableGallery = true, children, ...props }: Galle
1313 const expandedVideoRef = useRef < HTMLVideoElement > ( null ) ;
1414 const { openGallery } = useGallery ( ) ;
1515 const [ videoSrc , setVideoSrc ] = useState < string | undefined > ( undefined ) ;
16+ const [ isLoaded , setIsLoaded ] = useState ( false ) ;
1617
18+ // Extract video source from children on mount
1719 useEffect ( ( ) => {
18- // Extract video source from children
19- if ( videoRef . current ) {
20- const sourceElement = videoRef . current . querySelector ( "source" ) ;
21- if ( sourceElement ) {
22- setVideoSrc ( sourceElement . src ) ;
20+ const extractSrc = ( ) => {
21+ const childArray = Array . isArray ( children ) ? children : [ children ] ;
22+ const sourceChild = childArray . find (
23+ ( child : any ) => child ?. type === "source"
24+ ) as ReactElement < { src : string } > ;
25+
26+ if ( sourceChild ?. props ?. src ) {
27+ setVideoSrc ( sourceChild . props . src ) ;
2328 }
24- }
29+ } ;
30+
31+ extractSrc ( ) ;
2532 } , [ children ] ) ;
2633
34+ // Lazy loading with Intersection Observer
35+ useEffect ( ( ) => {
36+ const videoElement = videoRef . current ;
37+ if ( ! videoElement || ! videoSrc ) return ;
38+
39+ const observer = new IntersectionObserver (
40+ ( entries ) => {
41+ entries . forEach ( ( entry ) => {
42+ if ( entry . isIntersecting && ! isLoaded ) {
43+ // Load video when it enters viewport
44+ const sourceElement = videoElement . querySelector ( "source" ) ;
45+ if ( sourceElement && videoSrc ) {
46+ sourceElement . src = videoSrc ;
47+ videoElement . load ( ) ;
48+ setIsLoaded ( true ) ;
49+ }
50+ }
51+ } ) ;
52+ } ,
53+ {
54+ rootMargin : "200px" , // Start loading 200px before entering viewport
55+ threshold : 0.01 ,
56+ }
57+ ) ;
58+
59+ observer . observe ( videoElement ) ;
60+
61+ return ( ) => {
62+ observer . disconnect ( ) ;
63+ } ;
64+ } , [ isLoaded , videoSrc ] ) ;
65+
2766 const handleClick = ( ) => {
2867 if ( enableGallery && videoRef . current ) {
2968 // Pause the thumbnail video
@@ -51,17 +90,30 @@ export const GalleryVideo = ({ enableGallery = true, children, ...props }: Galle
5190 } ;
5291
5392 return (
54- < video
55- { ...props }
56- ref = { videoRef }
57- className = { `${ props . className } ${ enableGallery ? "cursor-pointer" : "" } ` }
58- onClick = { enableGallery ? handleClick : props . onClick }
59- autoPlay
60- loop
61- muted
62- playsInline
63- >
64- { children }
65- </ video >
93+ < div className = "relative" >
94+ { /* Loading placeholder */ }
95+ { ! isLoaded && (
96+ < div
97+ className = "absolute inset-0 bg-muted/50 animate-pulse rounded-lg flex items-center justify-center"
98+ style = { { aspectRatio : "16/9" } }
99+ >
100+ < div className = "text-muted-foreground text-sm" > Loading...</ div >
101+ </ div >
102+ ) }
103+
104+ < video
105+ { ...props }
106+ ref = { videoRef }
107+ className = { `${ props . className } ${ enableGallery ? "cursor-pointer" : "" } ${ ! isLoaded ? "opacity-0" : "opacity-100" } transition-opacity duration-300` }
108+ onClick = { enableGallery ? handleClick : props . onClick }
109+ autoPlay = { isLoaded }
110+ loop
111+ muted
112+ playsInline
113+ >
114+ { /* Render source without src initially - will be set by Intersection Observer */ }
115+ < source type = "video/mp4" />
116+ </ video >
117+ </ div >
66118 ) ;
67119} ;
0 commit comments