|
import React from "react"; |
|
import gsap from "gsap"; |
|
import { useGSAP } from "@gsap/react"; |
|
import { ScrollTrigger } from "gsap/all"; |
|
import { useEffect, useRef, useState } from "react"; |
|
import { hightlightsSlides } from "../utils"; |
|
import { pauseImg, playImg, replayImg } from "../utils"; |
|
|
|
gsap.registerPlugin(ScrollTrigger); |
|
|
|
const VideoCarousel = () => { |
|
|
|
const videoRef = useRef([]); |
|
const videoSpanRef = useRef([]); |
|
const videoDivRef = useRef([]); |
|
|
|
|
|
const [video, setVideo] = useState({ |
|
isEnd: false, |
|
startPlay: false, |
|
videoId: 0, |
|
isLastVideo: false, |
|
isPlaying: false, |
|
}); |
|
|
|
|
|
const [loadedData, setLoadedData] = useState([]); |
|
|
|
|
|
const { isEnd, isLastVideo, startPlay, videoId, isPlaying } = video; |
|
|
|
|
|
useGSAP(() => { |
|
|
|
gsap.to('#slider', { |
|
transform: `translateX(${-100 * videoId}%)`, |
|
duration: 2, |
|
ease: 'power2.inOut', |
|
}); |
|
|
|
|
|
gsap.to('#video', { |
|
scrollTrigger: { |
|
trigger: '#video', |
|
toggleActions: 'restart none none none', |
|
}, |
|
onComplete: () => { |
|
|
|
setVideo((prevVideo) => ({ |
|
...prevVideo, |
|
startPlay: true, |
|
isPlaying: true, |
|
})); |
|
}, |
|
}); |
|
}, [isEnd, videoId]); |
|
|
|
|
|
useEffect(() => { |
|
if (loadedData.length > 3) { |
|
if (!isPlaying) { |
|
videoRef.current[videoId].pause(); |
|
} else { |
|
startPlay && videoRef.current[videoId].play(); |
|
} |
|
} |
|
}, [startPlay, videoId, isPlaying, loadedData]); |
|
|
|
|
|
const handleLoadedMetadata = (i, e) => setLoadedData((prevVideo) => [...prevVideo, e]); |
|
|
|
|
|
useEffect(() => { |
|
let currentProgress = 0; |
|
let anim; |
|
const animUpdate = () => { |
|
if (videoRef.current[videoId]) { |
|
|
|
anim?.progress( |
|
videoRef.current[videoId].currentTime / hightlightsSlides[videoId].videoDuration |
|
); |
|
} |
|
}; |
|
|
|
if (videoSpanRef.current[videoId]) { |
|
anim = gsap.to(videoSpanRef.current[videoId], { |
|
onUpdate: () => { |
|
if (videoRef.current[videoId]) { |
|
const progress = Math.ceil( |
|
anim.progress() * 100 |
|
); |
|
|
|
if (progress !== currentProgress) { |
|
currentProgress = progress; |
|
|
|
|
|
gsap.to(videoDivRef.current[videoId], { |
|
width: window.innerWidth < 760 |
|
? '10vw' |
|
: window.innerWidth < 1200 |
|
? '10vw' |
|
: '4vw', |
|
}); |
|
|
|
|
|
gsap.to(videoSpanRef.current[videoId], { |
|
width: `${currentProgress}%`, |
|
backgroundColor: 'white', |
|
}); |
|
} |
|
} |
|
}, |
|
onComplete: () => { |
|
if (isPlaying) { |
|
gsap.to(videoDivRef.current[videoId], { |
|
width: '12px', |
|
}); |
|
gsap.to(videoSpanRef.current[videoId], { |
|
background: '#afafaf', |
|
}); |
|
} |
|
}, |
|
}); |
|
|
|
if (videoId === 0) { |
|
anim.restart(); |
|
} |
|
|
|
if (isPlaying) { |
|
gsap.ticker.add(animUpdate); |
|
} |
|
} |
|
|
|
return () => { |
|
|
|
gsap.ticker.remove(animUpdate); |
|
anim?.kill(); |
|
}; |
|
}, [videoId, startPlay, isPlaying]); |
|
|
|
|
|
const handleProcess = (type, i) => { |
|
switch (type) { |
|
case 'video-end': |
|
setVideo((prevVideo) => ({ ...prevVideo, isEnd: true, videoId: i + 1 })); |
|
break; |
|
case 'video-last': |
|
setVideo((prevVideo) => ({ ...prevVideo, isLastVideo: true })); |
|
break; |
|
case 'video-reset': |
|
setVideo((prevVideo) => ({ ...prevVideo, isLastVideo: false, videoId: 0 })); |
|
break; |
|
case 'play': |
|
setVideo((prevVideo) => ({ ...prevVideo, isPlaying: !prevVideo.isPlaying })); |
|
break; |
|
case 'pause': |
|
setVideo((prevVideo) => ({ ...prevVideo, isPlaying: !prevVideo.isPlaying })); |
|
break; |
|
default: |
|
return video; |
|
} |
|
}; |
|
|
|
|
|
return ( |
|
<> |
|
<div className="flex items-center"> |
|
{hightlightsSlides.map((list, i) => ( |
|
<div key={list.id} id="slider" className="sm:pr-20 pr-10"> |
|
<div className="video-carousel_container"> |
|
<div className="w-full h-full flex-center rounded-3xl overflow-hidden bg-black"> |
|
<video |
|
id="video" |
|
playsInline={true} // Enables video to play inline (not in fullscreen) on mobile devices. |
|
data-testid="video-element" |
|
preload="auto" // Preload the video metadata before playing. |
|
muted // Mute the video by default. |
|
className={`${list.id === 2 && 'translate-x-44'} pointer-events-none`} // Add special class for specific video and disable interaction. |
|
ref={(el) => (videoRef.current[i] = el)} // Store the video element in the videoRef array. |
|
onEnded={() => |
|
i !== 3 ? handleProcess('video-end', i) : handleProcess('video-last') // Handle video end or last video event. |
|
} |
|
onPlay={() => { |
|
setVideo((prevVideo) => ({ |
|
...prevVideo, |
|
isPlaying: true, // Set video to playing state on play event. |
|
})); |
|
}} |
|
onLoadedMetadata={(e) => handleLoadedMetadata(i, e)} // Store video metadata when loaded. |
|
> |
|
<source src={list.video} type="video/mp4" /> // Set the video source and type. |
|
</video> |
|
</div> |
|
{/* // Render each text for the current video. */} |
|
<div className="absolute top-12 left-[5%] z-10"> |
|
{list.textLists.map((text) => ( |
|
<p key={text} className="md:text-2xl text-xl font-medium"> |
|
{text} |
|
</p> |
|
))} |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
<div className="relative flex-center mt-10"> |
|
<div className="flex-center py-5 px-7 bg-gray-300 backdrop-blur rounded-full"> |
|
{videoRef.current.map((_, i) => ( |
|
<span |
|
key={i} |
|
className="mx-2 w-3 h-3 bg-gray-200 rounded-full relative cursor-pointer" |
|
ref={(el) => (videoDivRef.current[i] = el)} // Store the container div reference for progress bars. |
|
data-testid={`progressbar-${i}`} // Use data-testid for testing |
|
aria-valuenow="0" // Provide initial progress value |
|
aria-valuemin="0" // Set minimum progress |
|
aria-valuemax="100" // Set maximum progress |
|
> |
|
<span |
|
className="absolute h-full w-full rounded-full" |
|
ref={(el) => (videoSpanRef.current[i] = el)} // Store the span reference for progress updates. |
|
style={{ width: '0%' }} // Initialize progress width to 0 |
|
/> |
|
</span> |
|
))} |
|
</div> |
|
<button className="control-btn" role="button"> |
|
<img data-testid = "button-control-img" |
|
src={isLastVideo ? replayImg : !isPlaying ? playImg : pauseImg} // Set the control button image based on video state. |
|
alt={isLastVideo ? 'replay' : !isPlaying ? 'play' : 'pause'} // Set alt text for the control button. |
|
onClick={ |
|
isLastVideo |
|
? () => handleProcess('video-reset') // Handle replay action if the last video finished. |
|
: !isPlaying |
|
? () => handleProcess('play') // Handle play action if video is paused. |
|
: () => handleProcess('pause') // Handle pause action if video is playing. |
|
} |
|
/> |
|
</button> |
|
</div> |
|
</> |
|
); |
|
}; |
|
|
|
export default VideoCarousel; |