@@ -78,8 +78,19 @@ export default function ProductionTabs({
7878 const containerRef = useRef < HTMLDivElement > ( null ) ;
7979 const [ hasBeenVisible , setHasBeenVisible ] = useState ( false ) ;
8080
81+ // Lazy loading state - track which tabs have been visited
82+ const [ visitedTabs , setVisitedTabs ] = useState < Set < string > > ( new Set ( [ tabs [ 0 ] ?. id || 'scripts' ] ) ) ;
83+ // Loading state per video
84+ const [ loadingState , setLoadingState ] = useState < Record < string , { loading : boolean ; progress : number } > > ( { } ) ;
85+
8186 const getCurrentVideo = ( ) => videoRefs . current [ selectedTab ] ;
8287
88+ // Mark tab as visited when selected
89+ const handleTabSelect = ( tabId : string ) => {
90+ setSelectedTab ( tabId ) ;
91+ setVisitedTabs ( prev => new Set ( [ ...prev , tabId ] ) ) ;
92+ } ;
93+
8394 // Detect when component is visible in viewport
8495 useEffect ( ( ) => {
8596 const container = containerRef . current ;
@@ -98,6 +109,78 @@ export default function ProductionTabs({
98109 return ( ) => observer . disconnect ( ) ;
99110 } , [ hasBeenVisible ] ) ;
100111
112+ // Track video loading progress
113+ useEffect ( ( ) => {
114+ const video = videoRefs . current [ selectedTab ] ;
115+ if ( ! video || ! visitedTabs . has ( selectedTab ) ) return ;
116+
117+ const handleLoadStart = ( ) => {
118+ setLoadingState ( prev => ( {
119+ ...prev ,
120+ [ selectedTab ] : { loading : true , progress : 0 }
121+ } ) ) ;
122+ } ;
123+
124+ const handleProgress = ( ) => {
125+ if ( video . buffered . length > 0 && video . duration > 0 ) {
126+ const bufferedEnd = video . buffered . end ( video . buffered . length - 1 ) ;
127+ const progressPercent = ( bufferedEnd / video . duration ) * 100 ;
128+ setLoadingState ( prev => ( {
129+ ...prev ,
130+ [ selectedTab ] : { loading : progressPercent < 100 , progress : progressPercent }
131+ } ) ) ;
132+ }
133+ } ;
134+
135+ const handleCanPlayThrough = ( ) => {
136+ setLoadingState ( prev => ( {
137+ ...prev ,
138+ [ selectedTab ] : { loading : false , progress : 100 }
139+ } ) ) ;
140+ } ;
141+
142+ const handleWaiting = ( ) => {
143+ setLoadingState ( prev => ( {
144+ ...prev ,
145+ [ selectedTab ] : { ...prev [ selectedTab ] , loading : true }
146+ } ) ) ;
147+ } ;
148+
149+ const handlePlaying = ( ) => {
150+ setLoadingState ( prev => ( {
151+ ...prev ,
152+ [ selectedTab ] : { ...prev [ selectedTab ] , loading : false }
153+ } ) ) ;
154+ } ;
155+
156+ video . addEventListener ( 'loadstart' , handleLoadStart ) ;
157+ video . addEventListener ( 'progress' , handleProgress ) ;
158+ video . addEventListener ( 'canplaythrough' , handleCanPlayThrough ) ;
159+ video . addEventListener ( 'waiting' , handleWaiting ) ;
160+ video . addEventListener ( 'playing' , handlePlaying ) ;
161+
162+ // Check initial state
163+ if ( video . readyState >= 4 ) {
164+ setLoadingState ( prev => ( {
165+ ...prev ,
166+ [ selectedTab ] : { loading : false , progress : 100 }
167+ } ) ) ;
168+ } else if ( video . readyState < 3 ) {
169+ setLoadingState ( prev => ( {
170+ ...prev ,
171+ [ selectedTab ] : { loading : true , progress : 0 }
172+ } ) ) ;
173+ }
174+
175+ return ( ) => {
176+ video . removeEventListener ( 'loadstart' , handleLoadStart ) ;
177+ video . removeEventListener ( 'progress' , handleProgress ) ;
178+ video . removeEventListener ( 'canplaythrough' , handleCanPlayThrough ) ;
179+ video . removeEventListener ( 'waiting' , handleWaiting ) ;
180+ video . removeEventListener ( 'playing' , handlePlaying ) ;
181+ } ;
182+ } , [ selectedTab , visitedTabs ] ) ;
183+
101184 // Play/pause videos when tab changes
102185 useEffect ( ( ) => {
103186 // Pause all videos first
@@ -253,7 +336,7 @@ export default function ProductionTabs({
253336 { tabs . map ( ( tab ) => (
254337 < button
255338 key = { tab . id }
256- onClick = { ( ) => setSelectedTab ( tab . id ) }
339+ onClick = { ( ) => handleTabSelect ( tab . id ) }
257340 className = { `flex-1 px-4 py-2 font-medium text-sm transition-colors border-b-2 text-center text-gray-900 dark:text-white ${
258341 selectedTab === tab . id
259342 ? 'border-blue-500'
@@ -276,18 +359,57 @@ export default function ProductionTabs({
276359 { tab . description }
277360 </ p >
278361 { /* Video */ }
279- < div className = "relative group/video rounded-lg overflow-hidden" >
280- < video
281- ref = { ( el ) => { videoRefs . current [ tab . id ] = el ; } }
282- className = "w-full object-cover"
283- loop
284- muted
285- playsInline
286- >
287- < source src = { tab . video } type = "video/webm" />
288- </ video >
362+ < div className = "relative group/video rounded-lg overflow-hidden bg-gray-900" >
363+ { visitedTabs . has ( tab . id ) ? (
364+ < video
365+ ref = { ( el ) => { videoRefs . current [ tab . id ] = el ; } }
366+ className = "w-full object-cover"
367+ loop
368+ muted
369+ playsInline
370+ preload = "auto"
371+ >
372+ < source src = { tab . video } type = "video/webm" />
373+ </ video >
374+ ) : (
375+ < div className = "w-full aspect-video" />
376+ ) }
377+ { /* Loading overlay with progress */ }
378+ { selectedTab === tab . id && loadingState [ tab . id ] ?. loading && (
379+ < div className = "absolute inset-0 flex flex-col items-center justify-center bg-gray-900/90 backdrop-blur-sm" >
380+ < div className = "w-16 h-16 mb-4 relative" >
381+ < svg className = "w-full h-full" viewBox = "0 0 50 50" >
382+ < circle
383+ cx = "25"
384+ cy = "25"
385+ r = "20"
386+ fill = "none"
387+ stroke = "rgba(255, 255, 255, 0.2)"
388+ strokeWidth = "4"
389+ />
390+ < circle
391+ cx = "25"
392+ cy = "25"
393+ r = "20"
394+ fill = "none"
395+ stroke = "rgba(59, 130, 246, 0.9)"
396+ strokeWidth = "4"
397+ strokeDasharray = { 2 * Math . PI * 20 }
398+ strokeDashoffset = { 2 * Math . PI * 20 * ( 1 - ( loadingState [ tab . id ] ?. progress || 0 ) / 100 ) }
399+ strokeLinecap = "round"
400+ className = "transition-all duration-300"
401+ style = { { transform : 'rotate(-90deg)' , transformOrigin : '50% 50%' } }
402+ />
403+ </ svg >
404+ < span className = "absolute inset-0 flex items-center justify-center text-white text-xs font-medium" >
405+ { Math . round ( loadingState [ tab . id ] ?. progress || 0 ) } %
406+ </ span >
407+ </ div >
408+ < p className = "text-white/70 text-sm" > Loading video...</ p >
409+ </ div >
410+ ) }
289411 { /* Subtitle overlay */ }
290- { enableSubtitles && selectedTab === tab . id && currentSubtitle && (
412+ { enableSubtitles && selectedTab === tab . id && currentSubtitle && ! loadingState [ tab . id ] ?. loading && (
291413 < div
292414 className = "absolute inset-0 flex items-center justify-center bg-gray-900/80 backdrop-blur-[2px] transition-opacity duration-300 cursor-pointer"
293415 onClick = { skipSubtitle }
0 commit comments