You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
509 lines
22 KiB
509 lines
22 KiB
import {useNavigate, useParams, useSearchParams} from "react-router-dom";
|
|
import {useEffect, useState, useRef, useCallback} from "react";
|
|
import Navbar from "../components/Navbar.jsx";
|
|
import { useAuth } from "../contexts/AuthContext.jsx";
|
|
import Comment from "../components/Comment.jsx";
|
|
import VideoCard from "../components/VideoCard.jsx";
|
|
import Tag from "../components/Tag.jsx";
|
|
import {addView, getSimilarVideos, getVideoById, toggleLike} from "../services/video.service.js";
|
|
import {subscribe} from "../services/channel.service.js";
|
|
import {addComment} from "../services/comment.service.js";
|
|
import { getPlaylists } from "../services/user.service.js";
|
|
import { addToPlaylist, getPlaylistById } from "../services/playlist.service.js";
|
|
import PlaylistVideoCard from "../components/PlaylistVideoCard.jsx";
|
|
|
|
|
|
export default function Video() {
|
|
const {id} = useParams();
|
|
const { user, isAuthenticated } = useAuth();
|
|
const videoRef = useRef(null);
|
|
const controllerRef = useRef(null);
|
|
const navigation = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const playlistId = searchParams.get("playlistId");
|
|
const isPlaylist = playlistId !== null;
|
|
|
|
const [video, setVideo] = useState(null);
|
|
const [nextVideo, setNextVideo] = useState(null);
|
|
const [similarVideos, setSimilarVideos] = useState([]);
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const [duration, setDuration] = useState(0);
|
|
const [progress, setProgress] = useState(0);
|
|
const [showControls, setShowControls] = useState(false);
|
|
const [comment, setComment] = useState("");
|
|
const [alerts, setAlerts] = useState([]);
|
|
const [playlists, setPlaylists] = useState([]);
|
|
const [isAddToPlaylistOpen, setIsAddToPlaylistOpen] = useState(false);
|
|
const [currentPlaylist, setCurrentPlaylist] = useState(null);
|
|
|
|
const fetchVideo = useCallback(async () => {
|
|
// Fetch video data and similar videos based on the video ID from the URL
|
|
if (!id) {
|
|
addAlert('error', 'Vidéo introuvable');
|
|
navigation('/');
|
|
return;
|
|
}
|
|
|
|
const data = await getVideoById(id, addAlert);
|
|
if (!data) return;
|
|
setVideo(data);
|
|
|
|
const similarVideosResponse = await getSimilarVideos(id, addAlert);
|
|
if (similarVideosResponse) {
|
|
setSimilarVideos(similarVideosResponse);
|
|
}
|
|
|
|
// Add views to the video
|
|
await addView(id, addAlert);
|
|
}, [id, navigation]);
|
|
|
|
const fetchPlaylists = async () => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
const user = JSON.parse(localStorage.getItem('user'));
|
|
|
|
const data = await getPlaylists(user.id, token, addAlert);
|
|
if (data) {
|
|
setPlaylists(data);
|
|
}
|
|
}
|
|
|
|
const fetchCurrentPlaylist = async () => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
if (!playlistId) return;
|
|
|
|
setCurrentPlaylist(await getPlaylistById(playlistId, token, addAlert));
|
|
}
|
|
|
|
const fetchNextVideo = async () => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
console.log("Fetching next video");
|
|
console.log("currentPlaylist", currentPlaylist);
|
|
console.log("current video id from params:", id, "type:", typeof id);
|
|
console.log("playlist videos:", currentPlaylist?.videos?.map(v => ({ id: v.id, type: typeof v.id })));
|
|
|
|
//Find position of current video id in currentPlaylist.videos
|
|
const currentIndex = currentPlaylist?.videos.findIndex(video => {
|
|
console.log(`Comparing video.id: ${video.id} (${typeof video.id}) with id: ${id} (${typeof id})`);
|
|
return video.id.toString() === id.toString();
|
|
});
|
|
console.log("currentIndex", currentIndex);
|
|
|
|
if (currentIndex !== -1) {
|
|
if (currentPlaylist?.videos[currentIndex + 1]) {
|
|
setNextVideo(currentPlaylist.videos[currentIndex + 1]);
|
|
console.log("nextVideo", currentPlaylist.videos[currentIndex + 1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
const passToNextVideo = () => {
|
|
if (!nextVideo) {
|
|
console.log("No next video available");
|
|
return;
|
|
}
|
|
console.log("Passing to next video:", nextVideo);
|
|
|
|
// Navigate to the next video with playlist context
|
|
if (playlistId) {
|
|
navigation(`/video/${nextVideo.id}?playlistId=${playlistId}`);
|
|
} else {
|
|
navigation(`/video/${nextVideo.id}`);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchVideo();
|
|
fetchPlaylists();
|
|
fetchCurrentPlaylist();
|
|
}, [fetchVideo]);
|
|
|
|
useEffect(() => {
|
|
fetchNextVideo();
|
|
}, [currentPlaylist]);
|
|
|
|
const handlePlayPause = () => {
|
|
if (videoRef.current) {
|
|
if (videoRef.current.paused) {
|
|
videoRef.current.play();
|
|
} else {
|
|
videoRef.current.pause();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleTimeUpdate = () => {
|
|
if (videoRef.current) {
|
|
const current = videoRef.current.currentTime;
|
|
const total = videoRef.current.duration;
|
|
|
|
setCurrentTime(current);
|
|
setDuration(total);
|
|
setProgress((current / total) * 100);
|
|
}
|
|
};
|
|
|
|
const handleLoadedMetadata = () => {
|
|
if (videoRef.current) {
|
|
setDuration(videoRef.current.duration);
|
|
}
|
|
};
|
|
|
|
const handleTimeBarClick = (event) => {
|
|
if (videoRef.current) {
|
|
const timeBar = event.currentTarget;
|
|
const clickX = event.nativeEvent.offsetX;
|
|
const totalWidth = timeBar.offsetWidth;
|
|
const percentage = clickX / totalWidth;
|
|
const newTime = percentage * duration;
|
|
|
|
videoRef.current.currentTime = newTime;
|
|
setCurrentTime(newTime);
|
|
setProgress(percentage * 100);
|
|
}
|
|
};
|
|
|
|
const formatTime = (time) => {
|
|
if (isNaN(time)) return "0:00";
|
|
|
|
const minutes = Math.floor(time / 60);
|
|
const seconds = Math.floor(time % 60);
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return "";
|
|
|
|
const months = [
|
|
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
|
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
|
|
];
|
|
|
|
const date = new Date(dateString);
|
|
const day = date.getDate();
|
|
const month = months[date.getMonth()];
|
|
const year = date.getFullYear();
|
|
|
|
return `${day} ${month} ${year}`;
|
|
};
|
|
|
|
const handlePlaying = () => {
|
|
if (videoRef.current) {
|
|
console.log(`Video is playing at ${videoRef.current.currentTime} seconds`);
|
|
}
|
|
}
|
|
|
|
const handleMouseEnter = () => {
|
|
setShowControls(true);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setShowControls(false);
|
|
};
|
|
|
|
const handleSubscribe = async () => {
|
|
if (!isAuthenticated) {
|
|
navigation('/login');
|
|
return;
|
|
}
|
|
|
|
const response = await subscribe(video.creator.id, addAlert);
|
|
|
|
console.log('Subscription successful:', response);
|
|
|
|
const subscriptionCount = response.subscriptions || 0;
|
|
setVideo((prevVideo) => {
|
|
return {
|
|
...prevVideo,
|
|
creator: {
|
|
...prevVideo.creator,
|
|
subscribers: subscriptionCount
|
|
}
|
|
};
|
|
})
|
|
};
|
|
|
|
const handleLike = async () => {
|
|
if (!isAuthenticated) {
|
|
navigation('/login');
|
|
return;
|
|
}
|
|
|
|
// Retrieve the token from localStorage
|
|
const token = localStorage.getItem('token');
|
|
|
|
if (!token) {
|
|
navigation('/login');
|
|
return;
|
|
}
|
|
|
|
const data = await toggleLike(id, token, addAlert);
|
|
|
|
setVideo((prevVideo) => {
|
|
return {
|
|
...prevVideo,
|
|
likes: data.likes || prevVideo.likes + 1 // Update likes count
|
|
};
|
|
})
|
|
};
|
|
|
|
const handleComment = async () => {
|
|
if (!isAuthenticated) {
|
|
navigation('/login');
|
|
return;
|
|
}
|
|
|
|
if (!comment.trim()) {
|
|
alert("Comment cannot be empty");
|
|
return;
|
|
}
|
|
// Retrieve the token from localStorage
|
|
const token = localStorage.getItem('token');
|
|
|
|
if (!token) {
|
|
navigation('/login');
|
|
return;
|
|
}
|
|
|
|
const data = await addComment(video.id, comment, token, addAlert);
|
|
setComment(""); // Clear the comment input
|
|
|
|
setVideo((prevVideo) => ({
|
|
...prevVideo,
|
|
comments: [...(prevVideo.comments || []), data]
|
|
}));
|
|
}
|
|
|
|
const addAlert = (type, message) => {
|
|
const newAlert = { type, message, id: Date.now() }; // Add unique ID
|
|
setAlerts([...alerts, newAlert]);
|
|
};
|
|
|
|
const onCloseAlert = (alertToRemove) => {
|
|
setAlerts(alerts.filter(alert => alert !== alertToRemove));
|
|
};
|
|
|
|
const handleAddToPlaylist = async (id) => {
|
|
if (!isAuthenticated) {
|
|
navigation('/login');
|
|
return;
|
|
}
|
|
|
|
const body = {
|
|
video: video.id
|
|
}
|
|
const token = localStorage.getItem('token');
|
|
|
|
await addToPlaylist(id, body, token, addAlert);
|
|
|
|
setIsAddToPlaylistOpen(!isAddToPlaylistOpen);
|
|
}
|
|
|
|
return (
|
|
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
|
|
<Navbar isSearchPage={false} alerts={alerts} onCloseAlert={onCloseAlert} />
|
|
|
|
<main className="px-36 w-full flex justify-between pt-[118px]">
|
|
{video ? (
|
|
<>
|
|
{/* Video player section */}
|
|
<div className="w-1280/1920">
|
|
|
|
<div
|
|
className="relative w-full aspect-video mx-auto rounded-lg overflow-hidden"
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<video
|
|
key={video.id}
|
|
id={`video-${video.id}`}
|
|
ref={videoRef}
|
|
onPlay={handlePlaying}
|
|
onTimeUpdate={handleTimeUpdate}
|
|
onLoadedMetadata={handleLoadedMetadata}
|
|
onEnded={passToNextVideo}
|
|
>
|
|
<source src={`${video.file}`} type="video/mp4" />
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
|
|
{/* Video controls */}
|
|
<div
|
|
className={`absolute bottom-4 left-4 right-4 glassmorphism-rounded-md p-4 flex items-center transition-opacity duration-300 ${
|
|
showControls ? 'opacity-100' : 'opacity-0'
|
|
}`}
|
|
ref={controllerRef}
|
|
>
|
|
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg" onClick={handlePlayPause}>
|
|
<path d="M28.5 14.4019C30.5 15.5566 30.5 18.4434 28.5 19.5981L4.5 33.4545C2.5 34.6092 2.14642e-06 33.1658 2.24736e-06 30.8564L3.45873e-06 3.14359C3.55968e-06 0.834193 2.5 -0.609184 4.5 0.545517L28.5 14.4019Z" fill="white"/>
|
|
</svg>
|
|
|
|
<div className="flex-1 mx-4">
|
|
{/* Time display */}
|
|
<div className="flex justify-between items-center text-white text-sm mt-2">
|
|
<span>{formatTime(currentTime)}</span>
|
|
<span>{formatTime(duration)}</span>
|
|
</div>
|
|
|
|
{/* Time bar */}
|
|
<div
|
|
className="w-full h-2 bg-gray-300 rounded-full mt-2 cursor-pointer hover:h-3 transition-all duration-200"
|
|
onClick={handleTimeBarClick}
|
|
>
|
|
<div
|
|
className="h-full bg-white rounded-full transition-all duration-100"
|
|
style={{ width: `${progress}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<h1 className="mt-3 font-montserrat font-bold text-2xl text-white">{video.title}</h1>
|
|
|
|
{/* Channel and like */}
|
|
<div className="flex items-center mt-4">
|
|
<img
|
|
src={video.creator?.profile_picture || "https://placehold.co/48"}
|
|
alt={video.creator?.name || "Creator"}
|
|
className="w-12 h-12 rounded-full object-cover mr-3"
|
|
/>
|
|
<div>
|
|
<p className="text-white font-bold font-montserrat">{video.creator?.name}</p>
|
|
<p className="text-gray-300 text-sm">{video.creator?.subscribers || 0} abonnés</p>
|
|
</div>
|
|
<button className="ml-14 bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md cursor-pointer" onClick={handleSubscribe} >
|
|
s'abonner
|
|
</button>
|
|
<button className="ml-4 cursor-pointer" onClick={handleLike}>
|
|
<svg width="32" height="32" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M6 31.5H7.5V12H6C5.20435 12 4.44129 12.3161 3.87868 12.8787C3.31607 13.4413 3 14.2044 3 15V28.5C3 29.2956 3.31607 30.0587 3.87868 30.6213C4.44129 31.1839 5.20435 31.5 6 31.5ZM30 12H19.5L21.183 6.948C21.3332 6.49712 21.3741 6.01702 21.3024 5.54723C21.2306 5.07745 21.0483 4.63142 20.7705 4.24589C20.4926 3.86036 20.1271 3.54636 19.7041 3.32975C19.2811 3.11314 18.8127 3.00012 18.3375 3H18L10.5 11.157V31.5H27L32.868 18.606L33 18V15C33 14.2044 32.6839 13.4413 32.1213 12.8787C31.5587 12.3161 30.7956 12 30 12Z" fill="white"/>
|
|
</svg>
|
|
</button>
|
|
<p className="font-montserrat text-white ml-2" >{video.likes}</p>
|
|
|
|
<button className="relative ml-14">
|
|
<div className="bg-primary cursor-pointer px-4 py-2 rounded-md flex items-center gap-4" onClick={() => setIsAddToPlaylistOpen(!isAddToPlaylistOpen)} >
|
|
<p className="text-white font-montserrat font-bold" >playlist</p>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"></path></svg>
|
|
</div>
|
|
|
|
|
|
{
|
|
playlists.length > 0 && isAddToPlaylistOpen && (
|
|
<div className="absolute inset-0 w-max h-max z-40 glassmorphism top-1/1 mt-2 left-0 rounded-2xl px-4 py-2 cursor-default">
|
|
<ul className="flex flex-col gap-2">
|
|
{playlists.map((playlist) => (
|
|
<li
|
|
key={playlist.id}
|
|
className="text-white font-montserrat font-medium text-sm cursor-pointer hover:underline flex items-center justify-between gap-4"
|
|
onClick={() => handleAddToPlaylist(playlist.id)}
|
|
>
|
|
<p className="text-start">{playlist.name}</p>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"></path></svg>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
</div>
|
|
)
|
|
}
|
|
|
|
</button>
|
|
</div>
|
|
|
|
{/* Video details */}
|
|
<div className="glassmorphism rounded-md py-4 px-6 mt-7">
|
|
{/* Tags */}
|
|
{video.tags && video.tags.length > 0 && (
|
|
<div className="mb-3">
|
|
<div className="flex flex-wrap gap-2">
|
|
{video.tags.map((tag, index) => (
|
|
<Tag tag={tag} key={index} doShowControls={false} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<p className="font-montserrat text-white">{video.views} vues - {formatDate(video.release_date)}</p>
|
|
<p className="font-montserrat text-white mt-2">{video.description}</p>
|
|
|
|
</div>
|
|
|
|
{/* Comments section */}
|
|
<div>
|
|
<h2 className="font-montserrat text-white text-2xl mt-8">Commentaires</h2>
|
|
<textarea
|
|
className="glassmorphism h-[100px] w-full font-inter text-white placeholder:text-[#9f9f9f] focus:outline-none py-4 px-6"
|
|
placeholder="Ajouter un commentaire..."
|
|
value={comment}
|
|
onChange={(e) => setComment(e.target.value)}
|
|
></textarea>
|
|
|
|
<button className="bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md mt-2 cursor-pointer" onClick={handleComment}>
|
|
Publier
|
|
</button>
|
|
|
|
{/* Comments list */}
|
|
<div className="mt-4">
|
|
{video.comments && video.comments.length > 0 ? (
|
|
video.comments.map((comment, index) => (
|
|
<Comment comment={comment} index={index} videoId={id} key={index} refetchVideo={fetchVideo} addAlert={addAlert} />
|
|
))
|
|
) : (
|
|
<p className="text-gray-400">Aucun commentaire pour le moment. Soyez le premier à en publier !</p>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
{/* Similar videos section */}
|
|
<div className="flex-1">
|
|
{
|
|
!isPlaylist ? (
|
|
<div className="flex flex-col items-center gap-2">
|
|
{similarVideos.map((video, index) => (
|
|
<div className="w-9/10" key={index}>
|
|
<VideoCard video={video} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="glassmorphism w-9/10 py-4 px-2" >
|
|
<h2 className="font-montserrat text-white text-2xl">{currentPlaylist?.name}</h2>
|
|
{
|
|
currentPlaylist?.videos && currentPlaylist.videos.length > 0 ? (
|
|
<div className="flex flex-col items-center gap-2">
|
|
{currentPlaylist.videos.map((video, index) => (
|
|
<div className="w-full" key={index}>
|
|
<PlaylistVideoCard video={video} playlistId={currentPlaylist.id} navigation={navigation} currentVideo={id} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="font-montserrat text-white mt-2">Aucune vidéo trouvée dans cette playlist.</p>
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
</>
|
|
): (
|
|
<p>Loading</p>
|
|
)}
|
|
|
|
</main>
|
|
|
|
</div>
|
|
);
|
|
}
|