Browse Source

Finish fixing services

fix/services
Astri4-4 4 months ago
parent
commit
e253665544
  1. 6
      backend/app/controllers/channel.controller.js
  2. 73
      backend/app/controllers/video.controller.js
  3. 1239
      backend/logs/access.log
  4. 2
      frontend/src/components/Alert.jsx
  5. 2
      frontend/src/components/AlertList.jsx
  6. 62
      frontend/src/components/Comment.jsx
  7. 6
      frontend/src/components/Navbar.jsx
  8. 4
      frontend/src/components/ProtectedRoute.jsx
  9. 27
      frontend/src/modals/CreateChannelModal.jsx
  10. 94
      frontend/src/pages/Account.jsx
  11. 84
      frontend/src/pages/AddVideo.jsx
  12. 15
      frontend/src/pages/Home.jsx
  13. 75
      frontend/src/pages/ManageChannel.jsx
  14. 121
      frontend/src/pages/ManageVideo.jsx
  15. 211
      frontend/src/pages/Video.jsx
  16. 65
      frontend/src/services/channel.service.js
  17. 67
      frontend/src/services/comment.service.js
  18. 23
      frontend/src/services/recommendation.service.js
  19. 87
      frontend/src/services/user.service.js
  20. 212
      frontend/src/services/video.service.js

6
backend/app/controllers/channel.controller.js

@ -55,11 +55,15 @@ export async function getById(req, res) {
channels.name AS name,
channels.description AS description,
users.picture AS profilePicture,
COUNT(h.id) AS views
COUNT(h.id) AS views,
COUNT(likes.id) AS likes,
COUNT(c.id) AS comments
FROM public.videos
LEFT JOIN public.channels ON videos.channel = channels.id
LEFT JOIN public.users ON channels.OWNER = users.id
LEFT JOIN public.history h ON h.video = videos.id
LEFT JOIN public.likes ON likes.video = videos.id
LEFT JOIN public.comments c ON c.video = videos.id
WHERE videos.channel = $1
GROUP BY videos.id, channels.name, channels.description, users.username, users.picture
`;

73
backend/app/controllers/video.controller.js

@ -1,10 +1,10 @@
import {getClient} from "../utils/database.js";
import { getClient } from "../utils/database.js";
import * as path from "node:path";
import * as fs from "node:fs";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import jwt from "jsonwebtoken";
import {query} from "express-validator";
import { query } from "express-validator";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@ -54,7 +54,7 @@ export async function upload(req, res) {
const id = idResult.rows[0].id;
logger.write("successfully uploaded video", 200);
await client.end()
res.status(200).json({"message": "Successfully uploaded video", "id":id});
res.status(200).json({ "message": "Successfully uploaded video", "id": id });
}
@ -78,7 +78,7 @@ export async function uploadThumbnail(req, res) {
await client.query(updateQuery, [file, req.body.video]);
logger.write("successfully uploaded thumbnail", 200);
await client.end();
res.status(200).json({"message": "Successfully uploaded thumbnail"});
res.status(200).json({ "message": "Successfully uploaded thumbnail" });
}
export async function getById(req, res) {
@ -148,9 +148,48 @@ export async function update(req, res) {
const client = await getClient();
const query = `UPDATE videos SET title = $1, description = $2, visibility = $3 WHERE id = $4`;
await client.query(query, [req.body.title, req.body.description, req.body.visibility, id]);
const resultQuery = `
SELECT
videos.id,
videos.title,
videos.thumbnail,
videos.channel,
videos.file,
videos.description,
videos.visibility,
videos.release_date,
COUNT(DISTINCT l.id) AS likes_count,
COUNT(DISTINCT h.id) AS history_count,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', c.id,
'content', c.content,
'username', u.username,
'video', c.video,
'created_at', c.created_at,
'picture', u.picture
)
) FILTER (
WHERE
c.id IS NOT NULL
) AS comments,
JSON_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags
FROM public.videos
LEFT JOIN public.likes l ON l.video = videos.id
LEFT JOIN public.history h ON h.video = videos.id
LEFT JOIN public.comments c ON c.video = videos.id
LEFT JOIN public.video_tags vt ON vt.video = videos.id
LEFT JOIN public.tags t ON vt.tag = t.id
LEFT JOIN public.users u ON u.id = c.author
WHERE
videos.id = $1
GROUP BY public.videos.id
`;
const result = await client.query(resultQuery, [id]);
logger.write("successfully updated video", 200);
client.end()
res.status(200).json({"message": "Successfully updated video"});
res.status(200).json(result.rows[0]);
}
export async function updateVideo(req, res) {
@ -163,15 +202,15 @@ export async function updateVideo(req, res) {
const video = videoResult.rows[0];
const slug = video.slug;
const format = video.format;
const pathToDelete = path.join(__dirname, "../uploads/videos/", slug + "." + format );
const pathToDelete = path.join(__dirname, "../uploads/videos/", slug + "." + format);
fs.unlink(pathToDelete, (error) => {
if (error) {
logger.write(error, 500);
client.end()
res.status(500).json({"message": "Failed to delete video"});
res.status(500).json({ "message": "Failed to delete video" });
return
}
logger.action("successfully deleted video " + slug + "." + format );
logger.action("successfully deleted video " + slug + "." + format);
const fileBuffer = req.file.buffer;
const finalName = slug + "." + format;
const destinationPath = path.join(__dirname, "../uploads/videos/" + finalName)
@ -180,7 +219,7 @@ export async function updateVideo(req, res) {
logger.write("successfully updated video", 200);
client.end()
res.status(200).json({"message": "Successfully updated video"});
res.status(200).json({ "message": "Successfully updated video" });
})
@ -202,7 +241,7 @@ export async function del(req, res) {
if (error) {
logger.write(error, 500);
client.end()
res.status(500).json({"message": "Failed to delete video"});
res.status(500).json({ "message": "Failed to delete video" });
return
}
@ -210,14 +249,14 @@ export async function del(req, res) {
fs.unlink(pathToDelete, async (error) => {
if (error) {
logger.write(error, 500);
res.status(500).json({"message": "Failed to delete video"});
res.status(500).json({ "message": "Failed to delete video" });
return
}
const query = `DELETE FROM videos WHERE id = $1`;
await client.query(query, [id]);
logger.write("successfully deleted video", 200);
client.end()
res.status(200).json({"message": "Successfully deleted video"});
res.status(200).json({ "message": "Successfully deleted video" });
})
})
@ -248,7 +287,7 @@ export async function toggleLike(req, res) {
logger.write("no likes found adding likes for video " + id, 200);
client.end();
res.status(200).json({"message": "Successfully added like", "likes": likesCount});
res.status(200).json({ "message": "Successfully added like", "likes": likesCount });
} else {
const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`;
await client.query(query, [userId, id]);
@ -260,7 +299,7 @@ export async function toggleLike(req, res) {
logger.write("likes found, removing like for video " + id, 200);
client.end();
res.status(200).json({"message": "Successfully removed like", "likes": likesCount});
res.status(200).json({ "message": "Successfully removed like", "likes": likesCount });
}
@ -315,7 +354,7 @@ export async function addTags(req, res) {
logger.write("successfully added tags to video " + videoId, 200);
await client.end();
res.status(200).json({"message": "Successfully added tags to video", "tags" : updatedTags.map(tag => tag.name)});
res.status(200).json({ "message": "Successfully added tags to video", "tags": updatedTags.map(tag => tag.name) });
}
@ -333,7 +372,7 @@ export async function getSimilarVideos(req, res) {
if (tags.length === 0) {
logger.write("No tags found for video " + id, 404);
res.status(404).json({"message": "No similar videos found"});
res.status(404).json({ "message": "No similar videos found" });
return;
}
@ -449,5 +488,5 @@ export async function addViews(req, res) {
logger.write("successfully added views for video " + id, 200);
await client.end();
res.status(200).json({"message": "Successfully added views"});
res.status(200).json({ "message": "Successfully added views" });
}

1239
backend/logs/access.log

File diff suppressed because it is too large

2
frontend/src/components/Alert.jsx

@ -6,7 +6,7 @@ export default function Alert({ type, message, onClose }) {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 15000); // 15 seconds
}, 5000); // 5 seconds
return () => clearTimeout(timer);
}, [onClose]);

2
frontend/src/components/AlertList.jsx

@ -3,7 +3,7 @@ import Alert from "./Alert.jsx";
export default function AlertList({ alerts, onCloseAlert }) {
return (
<div className="absolute top-1/1 right-0 flex flex-col gap-2 mt-2 mr-4 z-40">
<div className="fixed bottom-2.5 right-0 flex flex-col gap-2 mt-2 mr-4 z-40">
{alerts.map((alert, index) => (
<Alert
key={index}

62
frontend/src/components/Comment.jsx

@ -1,8 +1,9 @@
import {useAuth} from "../contexts/AuthContext.jsx";
import {useRef, useState} from "react";
import { updateComment, deleteComment } from "../services/comment.service.js";
export default function Comment({ comment, index, videoId, refetchVideo, doShowCommands=true }) {
export default function Comment({ comment, index, videoId, refetchVideo, doShowCommands=true, addAlert }) {
let {user, isAuthenticated} = useAuth();
let commentRef = useRef();
@ -17,23 +18,11 @@ export default function Comment({ comment, index, videoId, refetchVideo, doShowC
}
const handleDelete = async (id) => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/comments/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
// Refresh the video data to update the comments list
refetchVideo();
}
} catch (error) {
console.error('Error deleting comment:', error);
}
const token = localStorage.getItem('token');
const response = await deleteComment(id, token, addAlert);
if (response) {
refetchVideo();
}
}
const handleEditSubmit = async (id, content) => {
@ -41,36 +30,25 @@ export default function Comment({ comment, index, videoId, refetchVideo, doShowC
alert("Comment cannot be empty");
return;
}
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
try {
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!token) {
navigation('/login');
return;
}
if (!token) {
navigation('/login');
return;
}
const body = {
content: content,
video: videoId
};
const response = await fetch(`/api/comments/${comment.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content: content,
video: videoId
})
});
const response = await updateComment(id, body, token, addAlert);
if (!response.ok) {
throw new Error('Failed to post comment');
}
if (response) {
setEditMode(false);
} catch (error) {
console.error('Error posting comment:', error);
}
}
return (

6
frontend/src/components/Navbar.jsx

@ -39,7 +39,8 @@ export default function Navbar({ isSearchPage = false, alerts = [], onCloseAlert
};
return (
<nav className="flex justify-between items-center p-4 text-white absolute top-0 left-0 w-screen relative">
<>
<nav className="flex justify-between items-center p-4 text-white absolute top-0 left-0 w-screen">
<div>
<h1 className="font-montserrat text-5xl font-black">
<a href="/">FreeTube</a>
@ -101,8 +102,9 @@ export default function Navbar({ isSearchPage = false, alerts = [], onCloseAlert
</ul>
</div>
<AlertList alerts={allAlerts} onCloseAlert={handleCloseAlert} />
</nav>
<AlertList alerts={allAlerts} onCloseAlert={handleCloseAlert} />
</>
)
}

4
frontend/src/components/ProtectedRoute.jsx

@ -17,10 +17,6 @@ const ProtectedRoute = ({ children, requireAuth = true }) => {
return <Navigate to="/login" replace />;
}
if (!requireAuth && isAuthenticated) {
return <Navigate to="/" replace />;
}
return children;

27
frontend/src/modals/CreateChannelModal.jsx

@ -1,7 +1,8 @@
import {useState} from "react";
import { createChannel } from "../services/channel.service.js";
export default function CreateChannelModal({isOpen, onClose}) {
export default function CreateChannelModal({isOpen, onClose, addAlert}) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
@ -13,27 +14,17 @@ export default function CreateChannelModal({isOpen, onClose}) {
const onSubmit = async (e) => {
e.preventDefault();
const request = await fetch(`/api/channels/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
"name": name,
"description": description,
"owner": user.id
})
})
if (!request.ok) {
console.error("Not able to create channel");
return; // Prevent further execution if the request failed
const body = {
"name": name,
"description": description,
"owner": user.id
}
const data = await request.json();
const data = await createChannel(body, token, addAlert);
console.log(data);
onClose();
}
return isOpen && (

94
frontend/src/pages/Account.jsx

@ -4,6 +4,7 @@ import PlaylistCard from "../components/PlaylistCard.jsx";
import VideoCard from "../components/VideoCard.jsx";
import {useNavigate} from "react-router-dom";
import CreateChannelModal from "../modals/CreateChannelModal.jsx";
import { getChannel, getUserHistory, getPlaylists, updateUser } from "../services/user.service.js";
export default function Account() {
@ -25,67 +26,13 @@ export default function Account() {
const navigation = useNavigate();
const fetchUserChannel = async () => {
try {
const response = await fetch(`/api/users/${user.id}/channel`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
}
});
if (!response.ok) {
throw new Error("Failed to fetch user data");
}
const data = await response.json();
setUserChannel(data);
} catch (error) {
console.error("Error fetching user channel:", error);
addAlert('error', "Erreur lors de la récupération des données de l'utilisateur.");
return null;
}
setUserChannel(await getChannel(user.id, token, addAlert)); // Reset before fetching
}
const fetchUserHistory = async () => {
if (!user.id || !token) {
console.warn("User ID or token missing, skipping history fetch");
return;
}
try {
const response = await fetch(`/api/users/${user.id}/history`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
}
});
if (!response.ok) {
throw new Error("Failed to fetch user history");
}
const data = await response.json();
setUserHistory(data);
} catch (error) {
console.error("Error fetching user history:", error);
addAlert('error', "Erreur lors de la récupération de l'historique de l'utilisateur.");
}
setUserHistory(await getUserHistory(user.id, token, addAlert));
}
const fetchUserPlaylists = async () => {
if (!user.id || !token) {
console.warn("User ID or token missing, skipping playlists fetch");
return;
}
try {
const response = await fetch(`/api/playlists/user/${user.id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
}
});
if (!response.ok) {
throw new Error("Failed to fetch user playlists");
}
const data = await response.json();
setUserPlaylists(data);
} catch (error) {
console.error("Error fetching user playlists:", error);
addAlert('error', "Erreur lors de la récupération des playlists de l'utilisateur.");
}
setUserPlaylists(await getPlaylists(user.id, token, addAlert));
}
useEffect(() => {
@ -116,27 +63,11 @@ export default function Account() {
password: password || undefined, // Only send password if it's not empty
};
try {
const response = await fetch(`/api/users/${user.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify(updatedUser),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
localStorage.setItem("user", JSON.stringify(data));
const result = await updateUser(user.id, token, updatedUser, addAlert);
if (result) {
localStorage.setItem("user", JSON.stringify(result));
setEditMode(false);
addAlert('success', "Profil mis à jour avec succès.");
} catch (error) {
console.error("Error updating user:", error);
addAlert('error', error.message || "Erreur lors de la mise à jour du profil.");
}
}
@ -149,6 +80,11 @@ export default function Account() {
setAlerts(alerts.filter(alert => alert !== alertToRemove));
};
const closeModal = () => {
setIsModalOpen(false);
fetchUserChannel();
}
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} />
@ -290,7 +226,7 @@ export default function Account() {
<h2 className="font-montserrat font-bold text-3xl text-white mt-10" >Playlists</h2>
<div className="w-full mt-5 flex flex-wrap" >
{
userPlaylists.map((playlist, index) => (
userPlaylists && userPlaylists.map((playlist, index) => (
<PlaylistCard playlist={playlist} key={index} onClick={handlePlaylistClick} />
))
}
@ -299,7 +235,7 @@ export default function Account() {
<h2 className="font-montserrat font-bold text-3xl text-white mt-10" >Historique</h2>
<div className="w-full mt-5 flex flex-wrap gap-2" >
{
userHistory.map((video, index) => (
userHistory && userHistory.map((video, index) => (
<div className="w-1/3" key={index}>
<VideoCard video={video}/>
</div>
@ -309,7 +245,7 @@ export default function Account() {
</div>
</main>
<CreateChannelModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
<CreateChannelModal isOpen={isModalOpen} onClose={() => closeModal()} addAlert={addAlert} />
</div>
)

84
frontend/src/pages/AddVideo.jsx

@ -1,6 +1,8 @@
import Navbar from "../components/Navbar.jsx";
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
import Tag from "../components/Tag.jsx";
import { getChannel } from "../services/user.service.js";
import { uploadVideo, uploadThumbnail, uploadTags } from "../services/video.service.js";
export default function AddVideo() {
@ -23,21 +25,9 @@ export default function AddVideo() {
}, [])
const fetchChannel = async () => {
try {
const response = await fetch(`/api/users/${user.id}/channel`, {
headers: {
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication
},
});
if (!response.ok) {
throw new Error("Erreur lors de la récupération de la chaîne");
}
const data = await response.json();
setChannel(data.channel);
} catch (error) {
console.error("Erreur lors de la récupération de la chaîne :", error);
addAlert('error', 'Erreur lors de la récupération de la chaîne');
}
const fetchedChannel = await getChannel(user.id, token, addAlert);
setChannel(fetchedChannel.channel);
console.log(fetchedChannel.channel);
}
const handleTagKeyDown = (e) => {
@ -58,6 +48,8 @@ export default function AddVideo() {
const handleSubmit = async (e) => {
e.preventDefault();
console.log(channel)
if (!videoTitle || !videoDescription || !videoThumbnail || !videoFile) {
addAlert('error', 'Veuillez remplir tous les champs requis.');
return;
@ -74,32 +66,11 @@ export default function AddVideo() {
const formData = new FormData();
formData.append("title", videoTitle);
formData.append("description", videoDescription);
formData.append("channel", channel.id.toString()); // Ensure it's a string
formData.append("channel", channel.id.toString());
formData.append("visibility", visibility);
formData.append("file", videoFile);
const request = await fetch("/api/videos", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication
},
body: formData
});
if (!request.ok) {
const errorData = await request.json();
console.error("Backend validation errors:", errorData);
// Display specific validation errors if available
if (errorData.errors && errorData.errors.length > 0) {
const errorMessages = errorData.errors.map(error =>
`${error.path}: ${error.msg}`
).join('\n');
addAlert('error', `Erreurs de validation:\n${errorMessages}`);
} else {
addAlert('error', 'Erreurs inconnues');
}
return;
}
const request = await uploadVideo(formData, token, addAlert);
// If the video was successfully created, we can now upload the thumbnail
const response = await request.json();
@ -108,35 +79,14 @@ export default function AddVideo() {
thumbnailFormData.append("video", videoId);
thumbnailFormData.append("file", videoThumbnail);
thumbnailFormData.append("channel", channel.id.toString());
const thumbnailRequest = await fetch("/api/videos/thumbnail", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication
},
body: thumbnailFormData
});
if (!thumbnailRequest.ok) {
const errorData = await thumbnailRequest.json();
console.error("Backend validation errors:", errorData);
addAlert('error', 'Erreur lors de l\'envoie d la miniature');
return;
}
await uploadThumbnail(thumbnailFormData, token, addAlert);
// if the thumbnail was successfully uploaded, we can send the tags
const tagsRequest = await fetch(`/api/videos/${videoId}/tags`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication
},
body: JSON.stringify({ tags: videoTags, channel: channel.id.toString() }) // Ensure channel ID is a string
});
if (!tagsRequest.ok) {
const errorData = await tagsRequest.json();
console.error("Backend validation errors:", errorData);
addAlert('error', 'Erreur lors de l\'ajout des tags');
return;
}
const body = {
tags: videoTags,
channel: channel.id.toString()
};
await uploadTags(body, videoId, token, addAlert);
// If everything is successful, redirect to the video management page
addAlert('success', 'Vidéo ajoutée avec succès !');
@ -196,7 +146,7 @@ export default function AddVideo() {
className="w-full p-2 mb-4 glassmorphism focus:outline-none font-inter text-xl text-white"
placeholder="Entrez les tags de la vidéo (entrée pour valider) 10 maximum"
onKeyDown={handleTagKeyDown}
/>
<div className="flex flex-wrap gap-2 mb-2">
{videoTags.map((tag, index) => (

15
frontend/src/pages/Home.jsx

@ -5,6 +5,7 @@ import {useState, useEffect} from "react";
import { useAuth } from '../contexts/AuthContext';
import TopCreators from "../components/TopCreators.jsx";
import TrendingVideos from "../components/TrendingVideos.jsx";
import { getRecommendations, getTrendingVideos } from '../services/recommendation.service.js';
export default function Home() {
const { isAuthenticated, user } = useAuth();
@ -18,21 +19,13 @@ export default function Home() {
// Fetch recommendations, top creators, and trending videos
const fetchData = async () => {
try {
const response = await fetch('/api/recommendations');
const data = await response.json();
setRecommendations(data.recommendations);
} catch (error) {
console.error('Error fetching data:', error);
setRecommendations(await getRecommendations(addAlert));
} finally {
setLoading(false);
}
try {
const trendingResponse = await fetch('/api/recommendations/trending');
const trendingData = await trendingResponse.json();
setTrendingVideos(trendingData);
} catch (error) {
addAlert('error', 'Erreur lors du chargement des vidéos tendance');
setTrendingVideos(await getTrendingVideos(addAlert));
} finally {
setLoading(false);
}
@ -42,7 +35,7 @@ export default function Home() {
}, []);
const addAlert = (type, message) => {
const newAlert = { type, message, id: Date.now() }; // Add unique ID
const newAlert = { type, message, id: Date.now() };
setAlerts([...alerts, newAlert]);
};

75
frontend/src/pages/ManageChannel.jsx

@ -3,6 +3,7 @@ import {useEffect, useState} from "react";
import {useNavigate, useParams} from "react-router-dom";
import {useAuth} from "../contexts/AuthContext.jsx";
import VideoStatListElement from "../components/VideoStatListElement.jsx";
import {fetchChannelDetails, fetchChannelStats, updateChannel} from "../services/channel.service.js";
export default function ManageChannel() {
@ -27,69 +28,31 @@ export default function ManageChannel() {
useEffect(() => {
fetchChannelData()
fetchChannelStats()
fetchStats()
}, []);
const fetchChannelData = async () => {
try {
const request = await fetch(`/api/channels/${id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
})
const result = await request.json();
setChannel(result);
} catch (error) {
console.error("Error fetching channel data:", error);
}
setChannel(await fetchChannelDetails(id, addAlert));
}
const fetchChannelStats = async () => {
try {
const request = await fetch(`/api/channels/${id}/stats`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
})
const result = await request.json();
setChannelStats(result);
} catch (error) {
console.error("Error fetching channel stats", error);
}
const fetchStats = async () => {
setChannelStats(await fetchChannelStats(id, token, addAlert));
}
const handleUpdateChannel = async () => {
if (!editMode) return;
try {
const response = await fetch(`/api/channels/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
name: channelName || channel.name,
description: description || channel.description,
})
});
if (response.ok) {
setEditMode(false);
addAlert('success', 'Chaîne mise à jour avec succès');
fetchChannelData(); // Refresh channel data after update
} else {
console.error("Failed to update channel");
const errorData = await response.json();
addAlert('error', errorData.message || 'Erreur lors de la mise à jour de la chaîne');
}
} catch (error) {
console.error("Error updating channel:", error);
addAlert('error', 'Erreur lors de la mise à jour de la chaîne');
const data = {
name: channelName || channel.name,
description: description || channel.description,
};
const response = await updateChannel(id, data, token, addAlert);
if (response) {
setEditMode(false);
}
}
const onCloseAlert = (alertToRemove) => {
@ -102,13 +65,13 @@ export default function ManageChannel() {
};
return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<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="pt-[118px] px-36 flex">
<main className="pt-[118px] px-36 flex pb-10">
{/* LEFT SIDE */}
<form className="glassmorphism w-1/3 h-screen py-10 px-4">
<form className="glassmorphism w-1/3 py-10 px-4 h-max">
<img src={user.picture} className="w-1/3 aspect-square object-cover rounded-full mx-auto" alt=""/>
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat ${editMode ? "block" : "hidden"} `}>
Nom de chaine
@ -168,7 +131,7 @@ export default function ManageChannel() {
</form>
{/* RIGHT SIDE */}
<div className="w-2/3 h-screen pl-10" >
<div className="w-2/3 pl-10" >
{/* VIEW / SUBSCRIBERS STATS */}
<div className="flex gap-4" >

121
frontend/src/pages/ManageVideo.jsx

@ -5,6 +5,7 @@ import {useEffect, useState} from "react";
import LinearGraph from "../components/LinearGraph.jsx";
import Comment from "../components/Comment.jsx";
import Tag from "../components/Tag.jsx";
import {getVideoById, getLikesPerDay, updateVideo, updateVideoFile, uploadThumbnail, uploadTags} from "../services/video.service.js";
export default function ManageVideo() {
@ -31,34 +32,16 @@ export default function ManageVideo() {
const editModeClassesTextArea = nonEditModeClassesTextArea + " glassmorphism h-48";
const fetchVideo = async () => {
const request = await fetch(`/api/videos/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!request.ok) {
addAlert('error', 'Erreur lors de la récupération de la vidéo');
}
const data = await request.json();
const data = await getVideoById(id, addAlert);
if (!data) return;
setVideo(data);
setVisibility(data.visibility)
setVideoTitle(data.title);
setDescription(data.description);
}
const fetchLikesPerDay = async () => {
const request = await fetch(`/api/videos/${id}/likes/day`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!request.ok) {
addAlert('error', 'Erreur lors de la récupération des likes par jour');
}
const data = await request.json();
const data = await getLikesPerDay(id, token, addAlert);
if (!data) return;
setLikesPerDay(data.likes);
setViewsPerDay(data.views);
}
@ -74,24 +57,14 @@ export default function ManageVideo() {
e.preventDefault();
if (!editMode) return;
const request = await fetch(`/api/videos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
title: videoTitle,
description: description,
visibility: visibility,
channel: video.channel
})
});
const body = {
title: videoTitle,
description: description,
visibility: visibility,
channel: video.channel
};
if (!request.ok) {
addAlert('error', 'Erreur lors de la mise à jour des détails de la vidéo');
return;
}
const request = await updateVideo(id, body, token, addAlert);
const form = new FormData();
if (videoFile) {
@ -99,18 +72,7 @@ export default function ManageVideo() {
form.append('video', id);
form.append('channel', video.channel);
const videoRequest = await fetch(`/api/videos/${id}/video`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: form
});
if (!videoRequest.ok) {
addAlert('error', 'Erreur lors de la mise à jour de la vidéo');
return;
}
await updateVideoFile(id, form, token, addAlert);
}
const data = await request.json();
@ -134,21 +96,7 @@ export default function ManageVideo() {
formData.append('video', id);
formData.append('channel', video.channel);
const request = await fetch(`/api/videos/thumbnail`, {
"method": 'POST',
"headers": {
"Authorization": `Bearer ${token}`
},
body: formData
})
if (!request.ok) {
addAlert('error', 'Erreur lors de l\'envoi de la miniature');
return;
}
const data = await request.json();
addAlert('success', 'Miniature mise à jour avec succès');
const request = await uploadThumbnail(formData, token, addAlert);
};
const onAddTag = async (e) => {
@ -156,23 +104,12 @@ export default function ManageVideo() {
const newTag = e.target.value.trim();
e.target.value = "";
const request = await fetch(`/api/videos/${id}/tags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
tags: [...video.tags, newTag],
channel: video.channel
})
});
if (!request.ok) {
addAlert('error', 'Erreur lors de l\'ajout du tag');
return;
const body = {
tags: [...video.tags, newTag],
channel: video.channel
}
const data = await request.json();
const request = await uploadTags(body, id, token, addAlert);
setVideo({
...video,
tags: [...video.tags, newTag]
@ -181,22 +118,12 @@ export default function ManageVideo() {
const onSuppressTag = async (tag) => {
const request = await fetch(`/api/videos/${id}/tags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
tags: video.tags.filter(t => t !== tag),
channel: video.channel
})
});
if (!request.ok) {
addAlert('error', 'Erreur lors de la suppression du tag');
return;
const body = {
tags: video.tags.filter(t => t !== tag),
channel: video.channel
}
const data = await request.json();
const request = await uploadTags(body, id, token, addAlert);
const newTags = video.tags.filter(t => t !== tag);
setVideo({
...video,

211
frontend/src/pages/Video.jsx

@ -5,10 +5,12 @@ 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";
export default function Video() {
// This component can be used to display a video player or video-related content.
const {id} = useParams();
const { user, isAuthenticated } = useAuth();
const videoRef = useRef(null);
@ -26,62 +28,25 @@ export default function Video() {
const fetchVideo = useCallback(async () => {
// Fetch video data and similar videos based on the video ID from the URL
try {
const response = await fetch(`/api/videos/${id}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const videoData = await response.json();
setVideo(videoData);
} catch (error) {
addAlert('error', 'Erreur lors de la récupération de la vidéo');
if (!id) {
addAlert('error', 'Vidéo introuvable');
navigation('/');
return;
}
try {
const response = await fetch(`/api/videos/${id}/similar`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const similarVideosData = await response.json();
setSimilarVideos(similarVideosData);
} catch (error) {
addAlert('error', 'Erreur lors de la récupération des vidéos similaires');
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
try {
const token = localStorage.getItem('token');
if (token) {
await fetch(`/api/videos/${id}/views`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
}
} catch (error) {
addAlert('error', 'Erreur lors de l\'ajout des vues à la vidéo');
}
await addView(id, addAlert);
}, [id, navigation]);
const fetchComments = useCallback(async () => {
// Fetch comments for the video
try {
const response = await fetch(`/api/comments/video/${id}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const commentsData = await response.json();
setVideo((prevVideo) => ({
...prevVideo,
comments: commentsData
}));
} catch (error) {
addAlert('error', 'Erreur lors de la récupération des commentaires');
}
}, [id]);
useEffect(() => {
fetchVideo();
}, [fetchVideo]);
@ -171,47 +136,20 @@ export default function Video() {
return;
}
try {
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
const response = await subscribe(video.creator.id, addAlert);
if (!token) {
navigation('/login');
return;
}
const response = await fetch(`/api/channels/${video.creator.id}/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
userId: user.id
})
});
if (!response.ok) {
throw new Error('Failed to subscribe');
}
console.log('Subscription successful:', response);
const data = await response.json();
console.log('Subscription successful:', data);
const subscriptionCount = data.subscriptions || 0;
setVideo((prevVideo) => {
return {
...prevVideo,
creator: {
...prevVideo.creator,
subscribers: subscriptionCount
}
};
})
} catch (error) {
addAlert('error', 'Erreur lors de l\'abonnement');
}
const subscriptionCount = response.subscriptions || 0;
setVideo((prevVideo) => {
return {
...prevVideo,
creator: {
...prevVideo.creator,
subscribers: subscriptionCount
}
};
})
};
const handleLike = async () => {
@ -220,39 +158,22 @@ export default function Video() {
return;
}
try {
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!token) {
navigation('/login');
return;
}
const response = await fetch(`/api/videos/${id}/like`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!response.ok) {
throw new Error('Failed to like video');
}
const data = await response.json();
if (!token) {
navigation('/login');
return;
}
setVideo((prevVideo) => {
return {
...prevVideo,
likes: data.likes || prevVideo.likes + 1 // Update likes count
};
})
const data = await toggleLike(id, token, addAlert);
} catch (error) {
addAlert('error', 'Erreur lors de l\'ajout du like');
}
setVideo((prevVideo) => {
return {
...prevVideo,
likes: data.likes || prevVideo.likes + 1 // Update likes count
};
})
};
const handleComment = async () => {
@ -265,45 +186,21 @@ export default function Video() {
alert("Comment cannot be empty");
return;
}
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
try {
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!token) {
navigation('/login');
return;
}
const response = await fetch(`/api/comments/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content: comment,
video: id
})
});
if (!response.ok) {
throw new Error('Failed to post comment');
}
const data = await response.json();
console.log('Comment posted successfully:', data);
setComment(""); // Clear the comment input
setVideo((prevVideo) => ({
...prevVideo,
comments: [...(prevVideo.comments || []), data] // Add the new comment to the existing comments
}));
if (!token) {
navigation('/login');
return;
}
const data = await addComment(video.id, comment, token, addAlert);
setComment(""); // Clear the comment input
} catch (error) {
addAlert('error', 'Erreur lors de la publication du commentaire');
}
setVideo((prevVideo) => ({
...prevVideo,
comments: [...(prevVideo.comments || []), data]
}));
}
const addAlert = (type, message) => {
@ -432,7 +329,7 @@ export default function Video() {
<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} />
<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>

65
frontend/src/services/channel.service.js

@ -7,11 +7,52 @@ export function fetchChannelDetails(channelId, addAlert) {
return response.json();
})
.catch(error => {
addAlert('error', "Erreur lors de la récupération des détails de la chaîne");
addAlert('error', "Erreur lors de la récupération des détails de la chaîne");
throw error;
});
}
export async function fetchChannelStats(channelId, token, addAlert) {
try {
const request = await fetch(`/api/channels/${channelId}/stats`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
})
const result = await request.json();
return result;
} catch (error) {
console.error("Error fetching channel stats", error);
addAlert('error', "Erreur lors de la récupération des statistiques de la chaîne");
}
}
export async function updateChannel(channelId, data, token, addAlert) {
try {
const response = await fetch(`/api/channels/${channelId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify(data)
});
if (response.ok) {
addAlert('success', 'Chaîne mise à jour avec succès');
return response.json();
} else {
console.error("Failed to update channel");
const errorData = await response.json();
addAlert('error', errorData.message || 'Erreur lors de la mise à jour de la chaîne');
}
} catch (error) {
console.error("Error updating channel:", error);
addAlert('error', 'Erreur lors de la mise à jour de la chaîne');
}
}
export async function subscribe(channelId, addAlert) {
const token = localStorage.getItem('token');
const user = JSON.parse(localStorage.getItem('user'));
@ -33,7 +74,27 @@ export async function subscribe(channelId, addAlert) {
return response.json();
})
.catch(error => {
addAlert('error', "Erreur lors de l'abonnement à la chaîne");
addAlert('error', "Erreur lors de l'abonnement à la chaîne");
throw error;
});
}
export async function createChannel(body, token, addAlert) {
const request = await fetch(`/api/channels/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(body)
})
if (!request.ok) {
console.error("Not able to create channel");
return; // Prevent further execution if the request failed
}
addAlert('success', 'Chaîne créée avec succès');
const data = await request.json();
return data;
}

67
frontend/src/services/comment.service.js

@ -0,0 +1,67 @@
export async function getCommentByVideo(id, addAlert) {
try {
const response = await fetch(`/api/comments/video/${id}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const commentsData = await response.json();
return commentsData;
} catch (error) {
addAlert('error', 'Erreur lors de la récupération des commentaires');
}
}
export async function addComment(videoId, commentData, token, addAlert) {
try {
const response = await fetch(`/api/comments/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content: commentData,
video: videoId
})
});
if (!response.ok) {
throw new Error('Failed to post comment');
}
const data = await response.json();
return data;
} catch (error) {
addAlert('error', 'Erreur lors de l\'ajout du commentaire');
}
}
export async function updateComment(id, body, token, addAlert) {
const response = await fetch(`/api/comments/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
if (!response.ok) {
addAlert('error', 'Erreur lors de la mise à jour du commentaire');
}
}
export async function deleteComment(id, token, addAlert) {
const response = await fetch(`/api/comments/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
addAlert('error', 'Erreur lors de la suppression du commentaire');
}
return response;
}

23
frontend/src/services/recommendation.service.js

@ -0,0 +1,23 @@
export async function getRecommendations(addAlert) {
try {
const response = await fetch('/api/recommendations');
const data = await response.json();
return data.recommendations;
} catch (error) {
console.error('Error fetching data:', error);
addAlert('error', 'Erreur lors du chargement des recommandations');
}
}
export async function getTrendingVideos(addAlert) {
try {
const response = await fetch('/api/recommendations/trending');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
addAlert('error', 'Erreur lors du chargement des vidéos tendance');
}
}

87
frontend/src/services/user.service.js

@ -13,4 +13,91 @@ export async function isSubscribed(channelId, addAlert) {
console.log("Subscription status for channel ID", channelId, ":", result);
return result.subscribed;
}
export async function getChannel(userId, token, addAlert) {
try {
const response = await fetch(`/api/users/${userId}/channel`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
}
});
if (!response.ok) {
throw new Error("Failed to fetch user data");
}
const data = await response.json();
return data
} catch (error) {
console.error("Error fetching user channel:", error);
addAlert('error', "Erreur lors de la récupération des données de l'utilisateur.");
return null;
}
}
export async function getUserHistory(userId, token, addAlert) {
if (!userId || !token) {
console.warn("User ID or token missing, skipping history fetch");
return;
}
try {
const response = await fetch(`/api/users/${userId}/history`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
}
});
if (!response.ok) {
throw new Error("Failed to fetch user history");
}
const data = await response.json();
return data
} catch (error) {
console.error("Error fetching user history:", error);
addAlert('error', "Erreur lors de la récupération de l'historique de l'utilisateur.");
}
}
export async function getPlaylists(userId, token, addAlert) {
if (!userId || !token) {
console.warn("User ID or token missing, skipping playlists fetch");
return;
}
try {
const response = await fetch(`/api/playlists/user/${userId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
}
});
if (!response.ok) {
throw new Error("Failed to fetch user playlists");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching user playlists:", error);
addAlert('error', "Erreur lors de la récupération des playlists de l'utilisateur.");
}
}
export async function updateUser(userId, token, userData, addAlert) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error("Failed to update user");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error updating user:", error);
addAlert('error', "Erreur lors de la mise à jour des données de l'utilisateur.");
}
}

212
frontend/src/services/video.service.js

@ -0,0 +1,212 @@
export async function uploadVideo(formData, token, addAlert) {
const request = await fetch("/api/videos", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication
},
body: formData
});
if (!request.ok) {
const errorData = await request.json();
console.error("Backend validation errors:", errorData);
// Display specific validation errors if available
if (errorData.errors && errorData.errors.length > 0) {
const errorMessages = errorData.errors.map(error =>
`${error.path}: ${error.msg}`
).join('\n');
addAlert('error', `Erreurs de validation:\n${errorMessages}`);
} else {
addAlert('error', 'Erreurs inconnues');
}
return;
}
return request;
}
export async function uploadThumbnail(formData, token, addAlert) {
try {
const thumbnailRequest = await fetch("/api/videos/thumbnail", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`
},
body: formData
});
if (!thumbnailRequest.ok) {
const errorData = await thumbnailRequest.json();
console.error("Backend validation errors:", errorData);
addAlert('error', 'Erreur lors de l\'envoie d la miniature');
return;
}
addAlert('success', 'Miniature envoyée avec succès');
return thumbnailRequest;
} catch (error) {
console.error("Error uploading thumbnail:", error);
addAlert('error', 'Erreur lors de l\'envoie de la miniature');
}
}
export async function uploadTags(body, videoId, token, addAlert) {
const tagsRequest = await fetch(`/api/videos/${videoId}/tags`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication
},
body: JSON.stringify(body) // Ensure channel ID is a string
});
addAlert('success', 'Tags mis à jour avec succès');
if (!tagsRequest.ok) {
const errorData = await tagsRequest.json();
console.error("Backend validation errors:", errorData);
addAlert('error', 'Erreur lors de l\'ajout des tags');
return;
}
}
export async function getVideoById(id, addAlert) {
try {
const request = await fetch(`/api/videos/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (!request.ok) {
addAlert('error', 'Erreur lors de la récupération de la vidéo');
}
const data = await request.json();
return data;
} catch (error) {
console.error("Error fetching video:", error);
addAlert('error', 'Erreur lors de la récupération de la vidéo');
}
}
export async function getLikesPerDay(id, token, addAlert) {
try {
const request = await fetch(`/api/videos/${id}/likes/day`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!request.ok) {
addAlert('error', 'Erreur lors de la récupération des likes par jour');
}
const data = await request.json();
return data;
} catch (error) {
console.error("Error fetching likes per day:", error);
addAlert('error', 'Erreur lors de la récupération des likes par jour');
}
}
export async function updateVideo(id, data, token, addAlert) {
try {
const request = await fetch(`/api/videos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
if (!request.ok) {
const errorData = await request.json();
console.error("Backend validation errors:", errorData);
addAlert('error', 'Erreur lors de la mise à jour de la vidéo');
return;
}
return request;
} catch (error) {
console.error("Error updating video:", error);
addAlert('error', 'Erreur lors de la mise à jour de la vidéo');
}
}
export async function updateVideoFile(id, formData, token, addAlert) {
try {
const videoRequest = await fetch(`/api/videos/${id}/video`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (!videoRequest.ok) {
addAlert('error', 'Erreur lors de la mise à jour de la vidéo');
return;
}
return videoRequest.json();
} catch (error) {
console.error("Error updating video file:", error);
addAlert('error', 'Erreur lors de la mise à jour de la vidéo');
}
}
export async function getSimilarVideos(id, addAlert) {
try {
const request = await fetch(`/api/videos/${id}/similar`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (!request.ok) {
addAlert('error', 'Erreur lors de la récupération des vidéos similaires');
return [];
}
const data = await request.json();
return data;
} catch (error) {
console.error("Error fetching similar videos:", error);
addAlert('error', 'Erreur lors de la récupération des vidéos similaires');
}
}
export async function addView(id, addAlert) {
try {
const token = localStorage.getItem('token');
if (token) {
await fetch(`/api/videos/${id}/views`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
}
} catch (error) {
addAlert('error', 'Erreur lors de l\'ajout des vues à la vidéo');
}
}
export async function toggleLike(id, token, addAlert) {
try {
const response = await fetch(`/api/videos/${id}/like`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to like video');
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error toggling like:", error);
addAlert('error', 'Erreur lors du changement de like');
}
}
Loading…
Cancel
Save