i-darrshan's picture
initial push of the update
8b105ad
raw
history blame
10.8 kB
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 = () => {
// Using refs to store DOM elements for videos, spans (progress bars), and divs (containers for progress bars).
const videoRef = useRef([]); // Ref for video elements to manage playback.
const videoSpanRef = useRef([]); // Ref for spans that represent the progress bar of each video.
const videoDivRef = useRef([]); // Ref for containers that hold the progress bars.
// Defining the state for video-related properties.
const [video, setVideo] = useState({
isEnd: false, // Whether the current video has ended.
startPlay: false, // Whether the video has started playing.
videoId: 0, // The current video ID or index in the carousel.
isLastVideo: false, // Whether the last video in the carousel is playing.
isPlaying: false, // Whether the video is currently playing or paused.
});
// State to store metadata of loaded videos (used to ensure videos are ready for playback).
const [loadedData, setLoadedData] = useState([]);
// Destructuring the video state for easier access in the component.
const { isEnd, isLastVideo, startPlay, videoId, isPlaying } = video;
// Setting up GSAP animation for the video slider.
useGSAP(() => {
// Animating the video slider to move left or right based on the current video ID.
gsap.to('#slider', {
transform: `translateX(${-100 * videoId}%)`, // Slide the container based on the current video index.
duration: 2, // Duration of the animation in seconds.
ease: 'power2.inOut', // Easing function for smooth sliding effect.
});
// Animating the video element itself.
gsap.to('#video', {
scrollTrigger: {
trigger: '#video', // The animation is triggered when the video element comes into view during scroll.
toggleActions: 'restart none none none', // The video restarts when the user scrolls back.
},
onComplete: () => {
// Once the animation completes, set the video state to playing.
setVideo((prevVideo) => ({
...prevVideo,
startPlay: true, // Start the video playback.
isPlaying: true, // Set the playing state to true.
}));
},
});
}, [isEnd, videoId]); // The animation depends on whether the video ended or the video ID changed.
// Effect to handle the video play/pause logic based on user interaction or video state.
useEffect(() => {
if (loadedData.length > 3) { // Ensure the metadata of all videos is loaded before proceeding.
if (!isPlaying) {
videoRef.current[videoId].pause(); // Pause the video if `isPlaying` is false.
} else {
startPlay && videoRef.current[videoId].play(); // Play the video if `isPlaying` is true and it is marked to start.
}
}
}, [startPlay, videoId, isPlaying, loadedData]); // This effect depends on the play state, current video, and loaded video data.
// Function to store video metadata (such as duration) when a video loads.
const handleLoadedMetadata = (i, e) => setLoadedData((prevVideo) => [...prevVideo, e]);
// Effect to animate and update the progress bar of the current video.
useEffect(() => {
let currentProgress = 0; // Keep track of the current progress percentage.
let anim; // Animation instance.
const animUpdate = () => {
if (videoRef.current[videoId]) {
// Safeguard to ensure videoRef.current[videoId] exists
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
); // Calculate the progress percentage.
if (progress !== currentProgress) {
currentProgress = progress;
// Adjust the width of the progress bar container based on screen size.
gsap.to(videoDivRef.current[videoId], {
width: window.innerWidth < 760
? '10vw'
: window.innerWidth < 1200
? '10vw'
: '4vw',
});
// Update the progress bar width and color dynamically.
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(); // Restart the animation if it's the first video.
}
if (isPlaying) {
gsap.ticker.add(animUpdate); // Add the progress update function to GSAP's ticker (runs at every frame).
}
}
return () => {
// Cleanup function to remove the ticker and any ongoing animations.
gsap.ticker.remove(animUpdate);
anim?.kill(); // Kill the animation to prevent memory leaks.
};
}, [videoId, startPlay, isPlaying]); // This effect depends on the video ID and whether the video started playing.
// Function to handle different events in the video (end, last video, reset, play, pause).
const handleProcess = (type, i) => {
switch (type) {
case 'video-end':
setVideo((prevVideo) => ({ ...prevVideo, isEnd: true, videoId: i + 1 })); // Move to the next video when the current video ends.
break;
case 'video-last':
setVideo((prevVideo) => ({ ...prevVideo, isLastVideo: true })); // Set the flag to indicate the last video has finished.
break;
case 'video-reset':
setVideo((prevVideo) => ({ ...prevVideo, isLastVideo: false, videoId: 0 })); // Reset the video carousel to the first video.
break;
case 'play':
setVideo((prevVideo) => ({ ...prevVideo, isPlaying: !prevVideo.isPlaying })); // Toggle play/pause state.
break;
case 'pause':
setVideo((prevVideo) => ({ ...prevVideo, isPlaying: !prevVideo.isPlaying })); // Toggle pause/play state.
break;
default:
return video; // Default case does nothing, just returns the current state.
}
};
// JSX to render the video carousel and controls.
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; // Export the VideoCarousel component.