1- ' use client'
1+ " use client"
22
3- import { useState } from 'react'
3+ import { useEffect , useRef , useState } from 'react'
44import { AppSidebar } from '@/components/app-sidebar'
55import { cn } from '@/lib/utils'
66import {
@@ -38,17 +38,12 @@ export default function AgencyPage() {
3838 onClick = { ( ) => handlePersonaClick ( persona ) }
3939 className = "group relative bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-800 dark:to-neutral-900 aspect-square rounded-xl hover:from-neutral-100 hover:to-neutral-200 dark:hover:from-neutral-700 dark:hover:to-neutral-800 transition-all duration-300 cursor-pointer border border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 hover:shadow-lg overflow-hidden"
4040 >
41- { /* Video Background */ }
42- < video
41+ { /* Video Background (lazy) */ }
42+ < LazyAutoplayVideo
4343 className = "absolute inset-0 w-full h-full object-cover rounded-xl"
44- autoPlay
45- loop
46- muted
47- playsInline
44+ src = { persona . videoSrc }
4845 poster = { persona . imageSrc }
49- >
50- < source src = { persona . videoSrc } type = "video/mp4" />
51- </ video >
46+ />
5247
5348 { /* Mobile overlay */ }
5449 < div className = "absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent rounded-xl md:hidden" />
@@ -113,6 +108,7 @@ export default function AgencyPage() {
113108 loop
114109 muted
115110 playsInline
111+ preload = "metadata"
116112 poster = { selectedPersona . imageSrc }
117113 >
118114 < source src = { selectedPersona . videoSrc } type = "video/mp4" />
@@ -204,4 +200,81 @@ export default function AgencyPage() {
204200 </ Dialog >
205201 </ div >
206202 )
207- }
203+ }
204+
205+ // Lazy-load and autoplay mp4 only when in viewport
206+ function LazyAutoplayVideo ( { src, poster, className } : { src : string ; poster : string ; className ?: string } ) {
207+ const ref = useRef < HTMLDivElement | null > ( null )
208+ const videoRef = useRef < HTMLVideoElement | null > ( null )
209+ const [ inView , setInView ] = useState ( false )
210+
211+ useEffect ( ( ) => {
212+ const el = ref . current
213+ if ( ! el ) return
214+
215+ const reducedMotion = typeof window !== 'undefined' && window . matchMedia && window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches
216+ if ( reducedMotion ) return // Respect user preference; keep image poster
217+
218+ const observer = new IntersectionObserver (
219+ ( entries ) => {
220+ for ( const e of entries ) {
221+ if ( e . isIntersecting ) {
222+ setInView ( true )
223+ } else {
224+ setInView ( false )
225+ }
226+ }
227+ } ,
228+ { root : null , threshold : 0.35 }
229+ )
230+
231+ observer . observe ( el )
232+ return ( ) => observer . disconnect ( )
233+ } , [ ] )
234+
235+ useEffect ( ( ) => {
236+ const v = videoRef . current
237+ if ( ! v ) return
238+ if ( inView ) {
239+ // Try to play when available
240+ const play = async ( ) => {
241+ try {
242+ await v . play ( )
243+ } catch {
244+ // Autoplay may be blocked; ignore
245+ }
246+ }
247+ play ( )
248+ } else {
249+ v . pause ( )
250+ }
251+ } , [ inView ] )
252+
253+ return (
254+ < div ref = { ref } className = { className } >
255+ { inView ? (
256+ < video
257+ ref = { videoRef }
258+ className = "w-full h-full object-cover"
259+ autoPlay
260+ loop
261+ muted
262+ playsInline
263+ preload = "auto"
264+ poster = { poster }
265+ >
266+ < source src = { src } type = "video/mp4" />
267+ </ video >
268+ ) : (
269+ // Poster-only until in view (no network fetch for video)
270+ < img
271+ src = { poster }
272+ alt = ""
273+ loading = "lazy"
274+ decoding = "async"
275+ className = "w-full h-full object-cover"
276+ />
277+ ) }
278+ </ div >
279+ )
280+ }
0 commit comments