fix/services #3

Merged
astria merged 2 commits from fix/services into developpement 4 months ago
  1. 54
      backend/app/controllers/channel.controller.js
  2. 24
      backend/app/controllers/user.controller.js
  3. 73
      backend/app/controllers/video.controller.js
  4. 2
      backend/app/routes/channel.route.js
  5. 7
      backend/app/routes/user.route.js
  6. 1239
      backend/logs/access.log
  7. 2
      frontend/src/components/Alert.jsx
  8. 2
      frontend/src/components/AlertList.jsx
  9. 20
      frontend/src/components/ChannelLastVideos.jsx
  10. 62
      frontend/src/components/Comment.jsx
  11. 6
      frontend/src/components/Navbar.jsx
  12. 4
      frontend/src/components/ProtectedRoute.jsx
  13. 43
      frontend/src/components/TabLayout.jsx
  14. 25
      frontend/src/components/VideoCard.jsx
  15. 16
      frontend/src/index.css
  16. 27
      frontend/src/modals/CreateChannelModal.jsx
  17. 94
      frontend/src/pages/Account.jsx
  18. 84
      frontend/src/pages/AddVideo.jsx
  19. 15
      frontend/src/pages/Home.jsx
  20. 75
      frontend/src/pages/ManageChannel.jsx
  21. 121
      frontend/src/pages/ManageVideo.jsx
  22. 211
      frontend/src/pages/Video.jsx
  23. 19
      frontend/src/routes/routes.jsx
  24. 100
      frontend/src/services/channel.service.js
  25. 67
      frontend/src/services/comment.service.js
  26. 23
      frontend/src/services/recommendation.service.js
  27. 103
      frontend/src/services/user.service.js
  28. 212
      frontend/src/services/video.service.js

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

@ -30,25 +30,61 @@ export async function getById(req, res) {
const client = await getClient(); const client = await getClient();
const query = ` const query = `
SELECT * SELECT channels.*, u.username, u.picture, COUNT(s.id) as subscriptions
FROM channels FROM channels
JOIN public.users u ON channels.owner = u.id
LEFT JOIN public.subscriptions s ON channels.id = s.channel
WHERE channels.id = $1 WHERE channels.id = $1
GROUP BY channels.id, name, description, channels.owner, u.username, u.picture
`; `;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
const videoQuery = ` const videoQuery = `
SELECT v.*, COUNT(h.video) as views, COUNT(l.id) as likes, COUNT(c.id) as comments SELECT
FROM videos v videos.id,
LEFT JOIN history h ON v.id = h.video videos.title,
LEFT JOIN likes l ON v.id = l.video videos.description AS video_description,
LEFT JOIN comments c ON v.id = c.video videos.thumbnail,
WHERE v.channel = $1 videos.channel,
GROUP BY v.id, title, thumbnail, description, channel, visibility, file, slug, format, release_date videos.visibility,
videos.file,
videos.slug,
videos.format,
videos.release_date,
channels.name AS name,
channels.description AS description,
users.picture AS profilePicture,
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
`; `;
const videoResult = await client.query(videoQuery, [id]); const videoResult = await client.query(videoQuery, [id]);
result.rows[0].videos = videoResult.rows; const videoReturn = [];
for (const video of videoResult.rows) {
video.creator = {
name: video.name,
profilePicture: video.profilepicture,
description: video.video_description
};
delete video.name;
delete video.profilepicture;
delete video.video_description;
videoReturn.push(video);
}
result.rows[0].videos = videoReturn;
logger.write("Successfully get channel with id " + id, 200); logger.write("Successfully get channel with id " + id, 200);
client.end(); client.end();

24
backend/app/controllers/user.controller.js

@ -307,4 +307,28 @@ export async function getHistory(req, res) {
logger.write("successfully retrieved history of user " + id, 200); logger.write("successfully retrieved history of user " + id, 200);
client.end(); client.end();
res.status(200).json(videos); res.status(200).json(videos);
}
export async function isSubscribed(req, res) {
const token = req.headers.authorization.split(" ")[1];
const tokenPayload = jwt.decode(token);
const userId = tokenPayload.id;
const channelId = req.params.id;
const client = await getClient();
const logger = req.body.logger;
logger.action(`check if user ${userId} is subscribed to channel ${channelId}`);
const query = `SELECT * FROM subscriptions WHERE owner = $1 AND channel = $2`;
const result = await client.query(query, [userId, channelId]);
if (result.rows[0]) {
logger.write(`user ${userId} is subscribed to channel ${channelId}`, 200);
client.end();
return res.status(200).json({subscribed: true});
} else {
logger.write(`user ${userId} is not subscribed to channel ${channelId}`, 200);
client.end();
return res.status(200).json({subscribed: false});
}
} }

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 path from "node:path";
import * as fs from "node:fs"; import * as fs from "node:fs";
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import {query} from "express-validator"; import { query } from "express-validator";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@ -54,7 +54,7 @@ export async function upload(req, res) {
const id = idResult.rows[0].id; const id = idResult.rows[0].id;
logger.write("successfully uploaded video", 200); logger.write("successfully uploaded video", 200);
await client.end() 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]); await client.query(updateQuery, [file, req.body.video]);
logger.write("successfully uploaded thumbnail", 200); logger.write("successfully uploaded thumbnail", 200);
await client.end(); 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) { export async function getById(req, res) {
@ -148,9 +148,48 @@ export async function update(req, res) {
const client = await getClient(); const client = await getClient();
const query = `UPDATE videos SET title = $1, description = $2, visibility = $3 WHERE id = $4`; 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]); 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); logger.write("successfully updated video", 200);
client.end() client.end()
res.status(200).json({"message": "Successfully updated video"}); res.status(200).json(result.rows[0]);
} }
export async function updateVideo(req, res) { export async function updateVideo(req, res) {
@ -163,15 +202,15 @@ export async function updateVideo(req, res) {
const video = videoResult.rows[0]; const video = videoResult.rows[0];
const slug = video.slug; const slug = video.slug;
const format = video.format; 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) => { fs.unlink(pathToDelete, (error) => {
if (error) { if (error) {
logger.write(error, 500); logger.write(error, 500);
client.end() client.end()
res.status(500).json({"message": "Failed to delete video"}); res.status(500).json({ "message": "Failed to delete video" });
return return
} }
logger.action("successfully deleted video " + slug + "." + format ); logger.action("successfully deleted video " + slug + "." + format);
const fileBuffer = req.file.buffer; const fileBuffer = req.file.buffer;
const finalName = slug + "." + format; const finalName = slug + "." + format;
const destinationPath = path.join(__dirname, "../uploads/videos/" + finalName) const destinationPath = path.join(__dirname, "../uploads/videos/" + finalName)
@ -180,7 +219,7 @@ export async function updateVideo(req, res) {
logger.write("successfully updated video", 200); logger.write("successfully updated video", 200);
client.end() 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) { if (error) {
logger.write(error, 500); logger.write(error, 500);
client.end() client.end()
res.status(500).json({"message": "Failed to delete video"}); res.status(500).json({ "message": "Failed to delete video" });
return return
} }
@ -210,14 +249,14 @@ export async function del(req, res) {
fs.unlink(pathToDelete, async (error) => { fs.unlink(pathToDelete, async (error) => {
if (error) { if (error) {
logger.write(error, 500); logger.write(error, 500);
res.status(500).json({"message": "Failed to delete video"}); res.status(500).json({ "message": "Failed to delete video" });
return return
} }
const query = `DELETE FROM videos WHERE id = $1`; const query = `DELETE FROM videos WHERE id = $1`;
await client.query(query, [id]); await client.query(query, [id]);
logger.write("successfully deleted video", 200); logger.write("successfully deleted video", 200);
client.end() 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); logger.write("no likes found adding likes for video " + id, 200);
client.end(); client.end();
res.status(200).json({"message": "Successfully added like", "likes": likesCount}); res.status(200).json({ "message": "Successfully added like", "likes": likesCount });
} else { } else {
const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`; const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`;
await client.query(query, [userId, id]); 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); logger.write("likes found, removing like for video " + id, 200);
client.end(); 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); logger.write("successfully added tags to video " + videoId, 200);
await client.end(); 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) { if (tags.length === 0) {
logger.write("No tags found for video " + id, 404); 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; return;
} }
@ -449,5 +488,5 @@ export async function addViews(req, res) {
logger.write("successfully added views for video " + id, 200); logger.write("successfully added views for video " + id, 200);
await client.end(); await client.end();
res.status(200).json({"message": "Successfully added views"}); res.status(200).json({ "message": "Successfully added views" });
} }

2
backend/app/routes/channel.route.js

@ -18,7 +18,7 @@ const router = Router();
router.post("/", [addLogger, isTokenValid, ChannelCreate.name, ChannelCreate.description, ChannelCreate.owner, validator, doUserExistsBody, doUserHaveChannel, isOwnerBody, doChannelNameExists], create); router.post("/", [addLogger, isTokenValid, ChannelCreate.name, ChannelCreate.description, ChannelCreate.owner, validator, doUserExistsBody, doUserHaveChannel, isOwnerBody, doChannelNameExists], create);
// GET CHANNEL BY ID // GET CHANNEL BY ID
router.get("/:id", [addLogger, isTokenValid, Channel.id, validator, doChannelExists], getById); router.get("/:id", [addLogger, Channel.id, validator, doChannelExists], getById);
// GET ALL CHANNEL // GET ALL CHANNEL
router.get("/", [addLogger, isTokenValid], getAll); router.get("/", [addLogger, isTokenValid], getAll);

7
backend/app/routes/user.route.js

@ -6,7 +6,8 @@ import {
getByUsername, getByUsername,
update, update,
deleteUser, deleteUser,
getChannel, getHistory getChannel, getHistory,
isSubscribed
} from "../controllers/user.controller.js"; } from "../controllers/user.controller.js";
import { import {
UserRegister, UserRegister,
@ -22,6 +23,7 @@ import validator from "../middlewares/error.middleware.js";
import {isTokenValid} from "../middlewares/jwt.middleware.js"; import {isTokenValid} from "../middlewares/jwt.middleware.js";
import {addLogger} from "../middlewares/logger.middleware.js"; import {addLogger} from "../middlewares/logger.middleware.js";
import {profileUpload} from "../middlewares/file.middleware.js"; import {profileUpload} from "../middlewares/file.middleware.js";
import {Channel} from "../middlewares/channel.middleware.js";
const router = Router(); const router = Router();
@ -49,4 +51,7 @@ router.get("/:id/channel", [addLogger, isTokenValid, User.id, validator], getCha
// GET USER HISTORY // GET USER HISTORY
router.get("/:id/history", [addLogger, isTokenValid, User.id, validator], getHistory); router.get("/:id/history", [addLogger, isTokenValid, User.id, validator], getHistory);
// CHECK IF SUBSCRIBED TO CHANNEL
router.get("/:id/channel/subscribed", [addLogger, isTokenValid, User.id, Channel.id, validator], isSubscribed)
export default router; export default router;

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(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
onClose(); onClose();
}, 15000); // 15 seconds }, 5000); // 5 seconds
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [onClose]); }, [onClose]);

2
frontend/src/components/AlertList.jsx

@ -3,7 +3,7 @@ import Alert from "./Alert.jsx";
export default function AlertList({ alerts, onCloseAlert }) { export default function AlertList({ alerts, onCloseAlert }) {
return ( 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) => ( {alerts.map((alert, index) => (
<Alert <Alert
key={index} key={index}

20
frontend/src/components/ChannelLastVideos.jsx

@ -0,0 +1,20 @@
import VideoCard from "./VideoCard.jsx";
export default function ChannelLastVideos({ videos }) {
return (
<div className="grid grid-cols-4 gap-8">
{
videos && videos.length > 0 ? (
videos.map((video) => (
<VideoCard video={video} />
)
)
) : (
<p>Aucune vidéo trouvée</p>
)
}
</div>
)
}

62
frontend/src/components/Comment.jsx

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

6
frontend/src/components/Navbar.jsx

@ -39,7 +39,8 @@ export default function Navbar({ isSearchPage = false, alerts = [], onCloseAlert
}; };
return ( 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> <div>
<h1 className="font-montserrat text-5xl font-black"> <h1 className="font-montserrat text-5xl font-black">
<a href="/">FreeTube</a> <a href="/">FreeTube</a>
@ -101,8 +102,9 @@ export default function Navbar({ isSearchPage = false, alerts = [], onCloseAlert
</ul> </ul>
</div> </div>
<AlertList alerts={allAlerts} onCloseAlert={handleCloseAlert} />
</nav> </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 />; return <Navigate to="/login" replace />;
} }
if (!requireAuth && isAuthenticated) {
return <Navigate to="/" replace />;
}
return children; return children;

43
frontend/src/components/TabLayout.jsx

@ -0,0 +1,43 @@
import React, { useState } from 'react';
export default function TabLayout({ tabs }) {
const [activeTab, setActiveTab] = useState(tabs[0].id);
const onTabChange = (tabId) => {
setActiveTab(tabId);
};
return (
<div>
{/* TABS */}
<div className='flex items-center mt-8 gap-3 mb-4' >
{
tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={` px-4 py-2 font-montserrat font-medium text-lg cursor-pointer ${tab.id === activeTab ? 'bg-white text-black rounded-2xl border-2 border-white' : 'glassmorphism text-white'}`}
>
{tab.label}
</button>
))
}
</div>
{/* ELEMENT */}
<div className="glassmorphism w-full p-4">
{tabs.map((tab) => (
<div key={tab.id} className={`tab-content ${tab.id === activeTab ? 'block' : 'hidden'}`}>
{tab.element()}
</div>
))}
</div>
</div>
)
}

25
frontend/src/components/VideoCard.jsx

@ -1,5 +1,30 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
// SUPPORTED JSON FORMAT
// [
// {
// "id": 1,
// "title": "Video minecraft",
// "thumbnail": "/api/media/thumbnail/78438E11ABA5D0C8.webp",
// "video_description": "Cest une super video minecraft",
// "channel": 1,
// "visibility": "public",
// "file": "/api/media/video/78438E11ABA5D0C8.mp4",
// "slug": "78438E11ABA5D0C8",
// "format": "mp4",
// "release_date": "2025-08-11T11:14:01.357Z",
// "channel_id": 1,
// "owner": 2,
// "views": "2",
// "creator": {
// "name": "astria",
// "profilePicture": "/api/media/profile/sacha.jpg",
// "description": "salut tout le monde"
// },
// "type": "video"
// }
// ]
export default function VideoCard({ video }) { export default function VideoCard({ video }) {
const navigation = useNavigate(); const navigation = useNavigate();
const handleClick = () => { const handleClick = () => {

16
frontend/src/index.css

@ -38,6 +38,22 @@
backdrop-filter: blur(27.5px); backdrop-filter: blur(27.5px);
} }
.glassmorphism-top-round {
border-top-left-radius: 15px;
border-top-right-radius: 15px;
border: 1px solid rgba(239, 239, 239, 0.60);
background: linear-gradient(93deg, rgba(239, 239, 239, 0.06) 0%, rgba(239, 239, 239, 0.01) 100%);
backdrop-filter: blur(27.5px);
}
.glassmorphism-bottom-round {
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
border: 1px solid rgba(239, 239, 239, 0.60);
background: linear-gradient(93deg, rgba(239, 239, 239, 0.06) 0%, rgba(239, 239, 239, 0.01) 100%);
backdrop-filter: blur(27.5px);
}
.resizable-none { .resizable-none {
resize: none; resize: none;
} }

27
frontend/src/modals/CreateChannelModal.jsx

@ -1,7 +1,8 @@
import {useState} from "react"; 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 [name, setName] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
@ -13,27 +14,17 @@ export default function CreateChannelModal({isOpen, onClose}) {
const onSubmit = async (e) => { const onSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
const request = await fetch(`/api/channels/`, { const body = {
method: 'POST', "name": name,
headers: { "description": description,
'Content-Type': 'application/json', "owner": user.id
'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 data = await request.json(); const data = await createChannel(body, token, addAlert);
console.log(data); console.log(data);
onClose();
} }
return isOpen && ( 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 VideoCard from "../components/VideoCard.jsx";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import CreateChannelModal from "../modals/CreateChannelModal.jsx"; import CreateChannelModal from "../modals/CreateChannelModal.jsx";
import { getChannel, getUserHistory, getPlaylists, updateUser } from "../services/user.service.js";
export default function Account() { export default function Account() {
@ -25,67 +26,13 @@ export default function Account() {
const navigation = useNavigate(); const navigation = useNavigate();
const fetchUserChannel = async () => { const fetchUserChannel = async () => {
try { setUserChannel(await getChannel(user.id, token, addAlert)); // Reset before fetching
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;
}
} }
const fetchUserHistory = async () => { const fetchUserHistory = async () => {
if (!user.id || !token) { setUserHistory(await getUserHistory(user.id, token, addAlert));
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.");
}
} }
const fetchUserPlaylists = async () => { const fetchUserPlaylists = async () => {
if (!user.id || !token) { setUserPlaylists(await getPlaylists(user.id, token, addAlert));
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.");
}
} }
useEffect(() => { useEffect(() => {
@ -116,27 +63,11 @@ export default function Account() {
password: password || undefined, // Only send password if it's not empty password: password || undefined, // Only send password if it's not empty
}; };
try { const result = await updateUser(user.id, token, updatedUser, addAlert);
const response = await fetch(`/api/users/${user.id}`, { if (result) {
method: "PUT", localStorage.setItem("user", JSON.stringify(result));
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));
setEditMode(false); setEditMode(false);
addAlert('success', "Profil mis à jour avec succès."); 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)); setAlerts(alerts.filter(alert => alert !== alertToRemove));
}; };
const closeModal = () => {
setIsModalOpen(false);
fetchUserChannel();
}
return ( 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} /> <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> <h2 className="font-montserrat font-bold text-3xl text-white mt-10" >Playlists</h2>
<div className="w-full mt-5 flex flex-wrap" > <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} /> <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> <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" > <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}> <div className="w-1/3" key={index}>
<VideoCard video={video}/> <VideoCard video={video}/>
</div> </div>
@ -309,7 +245,7 @@ export default function Account() {
</div> </div>
</main> </main>
<CreateChannelModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} /> <CreateChannelModal isOpen={isModalOpen} onClose={() => closeModal()} addAlert={addAlert} />
</div> </div>
) )

84
frontend/src/pages/AddVideo.jsx

@ -1,6 +1,8 @@
import Navbar from "../components/Navbar.jsx"; import Navbar from "../components/Navbar.jsx";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import Tag from "../components/Tag.jsx"; 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() { export default function AddVideo() {
@ -23,21 +25,9 @@ export default function AddVideo() {
}, []) }, [])
const fetchChannel = async () => { const fetchChannel = async () => {
try { const fetchedChannel = await getChannel(user.id, token, addAlert);
const response = await fetch(`/api/users/${user.id}/channel`, { setChannel(fetchedChannel.channel);
headers: { console.log(fetchedChannel.channel);
"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 handleTagKeyDown = (e) => { const handleTagKeyDown = (e) => {
@ -58,6 +48,8 @@ export default function AddVideo() {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
console.log(channel)
if (!videoTitle || !videoDescription || !videoThumbnail || !videoFile) { if (!videoTitle || !videoDescription || !videoThumbnail || !videoFile) {
addAlert('error', 'Veuillez remplir tous les champs requis.'); addAlert('error', 'Veuillez remplir tous les champs requis.');
return; return;
@ -74,32 +66,11 @@ export default function AddVideo() {
const formData = new FormData(); const formData = new FormData();
formData.append("title", videoTitle); formData.append("title", videoTitle);
formData.append("description", videoDescription); 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("visibility", visibility);
formData.append("file", videoFile); formData.append("file", videoFile);
const request = await fetch("/api/videos", { const request = await uploadVideo(formData, token, addAlert);
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;
}
// If the video was successfully created, we can now upload the thumbnail // If the video was successfully created, we can now upload the thumbnail
const response = await request.json(); const response = await request.json();
@ -108,35 +79,14 @@ export default function AddVideo() {
thumbnailFormData.append("video", videoId); thumbnailFormData.append("video", videoId);
thumbnailFormData.append("file", videoThumbnail); thumbnailFormData.append("file", videoThumbnail);
thumbnailFormData.append("channel", channel.id.toString()); thumbnailFormData.append("channel", channel.id.toString());
const thumbnailRequest = await fetch("/api/videos/thumbnail", { await uploadThumbnail(thumbnailFormData, token, addAlert);
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;
}
// if the thumbnail was successfully uploaded, we can send the tags // if the thumbnail was successfully uploaded, we can send the tags
const tagsRequest = await fetch(`/api/videos/${videoId}/tags`, { const body = {
method: "PUT", tags: videoTags,
headers: { channel: channel.id.toString()
"Content-Type": "application/json", };
"Authorization": `Bearer ${token}` // Assuming you have a token for authentication await uploadTags(body, videoId, token, addAlert);
},
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;
}
// If everything is successful, redirect to the video management page // If everything is successful, redirect to the video management page
addAlert('success', 'Vidéo ajoutée avec succès !'); 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" 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" placeholder="Entrez les tags de la vidéo (entrée pour valider) 10 maximum"
onKeyDown={handleTagKeyDown} onKeyDown={handleTagKeyDown}
/> />
<div className="flex flex-wrap gap-2 mb-2"> <div className="flex flex-wrap gap-2 mb-2">
{videoTags.map((tag, index) => ( {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 { useAuth } from '../contexts/AuthContext';
import TopCreators from "../components/TopCreators.jsx"; import TopCreators from "../components/TopCreators.jsx";
import TrendingVideos from "../components/TrendingVideos.jsx"; import TrendingVideos from "../components/TrendingVideos.jsx";
import { getRecommendations, getTrendingVideos } from '../services/recommendation.service.js';
export default function Home() { export default function Home() {
const { isAuthenticated, user } = useAuth(); const { isAuthenticated, user } = useAuth();
@ -18,21 +19,13 @@ export default function Home() {
// Fetch recommendations, top creators, and trending videos // Fetch recommendations, top creators, and trending videos
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await fetch('/api/recommendations'); setRecommendations(await getRecommendations(addAlert));
const data = await response.json();
setRecommendations(data.recommendations);
} catch (error) {
console.error('Error fetching data:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
try { try {
const trendingResponse = await fetch('/api/recommendations/trending'); setTrendingVideos(await getTrendingVideos(addAlert));
const trendingData = await trendingResponse.json();
setTrendingVideos(trendingData);
} catch (error) {
addAlert('error', 'Erreur lors du chargement des vidéos tendance');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -42,7 +35,7 @@ export default function Home() {
}, []); }, []);
const addAlert = (type, message) => { const addAlert = (type, message) => {
const newAlert = { type, message, id: Date.now() }; // Add unique ID const newAlert = { type, message, id: Date.now() };
setAlerts([...alerts, newAlert]); 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 {useNavigate, useParams} from "react-router-dom";
import {useAuth} from "../contexts/AuthContext.jsx"; import {useAuth} from "../contexts/AuthContext.jsx";
import VideoStatListElement from "../components/VideoStatListElement.jsx"; import VideoStatListElement from "../components/VideoStatListElement.jsx";
import {fetchChannelDetails, fetchChannelStats, updateChannel} from "../services/channel.service.js";
export default function ManageChannel() { export default function ManageChannel() {
@ -27,69 +28,31 @@ export default function ManageChannel() {
useEffect(() => { useEffect(() => {
fetchChannelData() fetchChannelData()
fetchChannelStats() fetchStats()
}, []); }, []);
const fetchChannelData = async () => { const fetchChannelData = async () => {
try { setChannel(await fetchChannelDetails(id, addAlert));
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);
}
} }
const fetchChannelStats = async () => { const fetchStats = async () => {
try { setChannelStats(await fetchChannelStats(id, token, addAlert));
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 handleUpdateChannel = async () => { const handleUpdateChannel = async () => {
if (!editMode) return; if (!editMode) return;
try { const data = {
const response = await fetch(`/api/channels/${id}`, { name: channelName || channel.name,
method: "PUT", description: description || channel.description,
headers: { };
"Content-Type": "application/json",
"Authorization": `Bearer ${token}` const response = await updateChannel(id, data, token, addAlert);
},
body: JSON.stringify({ if (response) {
name: channelName || channel.name, setEditMode(false);
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 onCloseAlert = (alertToRemove) => { const onCloseAlert = (alertToRemove) => {
@ -102,13 +65,13 @@ export default function ManageChannel() {
}; };
return ( 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} /> <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 */} {/* 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=""/> <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"} `}> <label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat ${editMode ? "block" : "hidden"} `}>
Nom de chaine Nom de chaine
@ -168,7 +131,7 @@ export default function ManageChannel() {
</form> </form>
{/* RIGHT SIDE */} {/* RIGHT SIDE */}
<div className="w-2/3 h-screen pl-10" > <div className="w-2/3 pl-10" >
{/* VIEW / SUBSCRIBERS STATS */} {/* VIEW / SUBSCRIBERS STATS */}
<div className="flex gap-4" > <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 LinearGraph from "../components/LinearGraph.jsx";
import Comment from "../components/Comment.jsx"; import Comment from "../components/Comment.jsx";
import Tag from "../components/Tag.jsx"; import Tag from "../components/Tag.jsx";
import {getVideoById, getLikesPerDay, updateVideo, updateVideoFile, uploadThumbnail, uploadTags} from "../services/video.service.js";
export default function ManageVideo() { export default function ManageVideo() {
@ -31,34 +32,16 @@ export default function ManageVideo() {
const editModeClassesTextArea = nonEditModeClassesTextArea + " glassmorphism h-48"; const editModeClassesTextArea = nonEditModeClassesTextArea + " glassmorphism h-48";
const fetchVideo = async () => { const fetchVideo = async () => {
const request = await fetch(`/api/videos/${id}`, { const data = await getVideoById(id, addAlert);
method: 'GET', if (!data) return;
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();
setVideo(data); setVideo(data);
setVisibility(data.visibility) setVisibility(data.visibility)
setVideoTitle(data.title); setVideoTitle(data.title);
setDescription(data.description); setDescription(data.description);
} }
const fetchLikesPerDay = async () => { const fetchLikesPerDay = async () => {
const request = await fetch(`/api/videos/${id}/likes/day`, { const data = await getLikesPerDay(id, token, addAlert);
method: 'GET', if (!data) return;
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();
setLikesPerDay(data.likes); setLikesPerDay(data.likes);
setViewsPerDay(data.views); setViewsPerDay(data.views);
} }
@ -74,24 +57,14 @@ export default function ManageVideo() {
e.preventDefault(); e.preventDefault();
if (!editMode) return; if (!editMode) return;
const request = await fetch(`/api/videos/${id}`, { const body = {
method: 'PUT', title: videoTitle,
headers: { description: description,
'Content-Type': 'application/json', visibility: visibility,
'Authorization': `Bearer ${token}` channel: video.channel
}, };
body: JSON.stringify({
title: videoTitle,
description: description,
visibility: visibility,
channel: video.channel
})
});
if (!request.ok) { const request = await updateVideo(id, body, token, addAlert);
addAlert('error', 'Erreur lors de la mise à jour des détails de la vidéo');
return;
}
const form = new FormData(); const form = new FormData();
if (videoFile) { if (videoFile) {
@ -99,18 +72,7 @@ export default function ManageVideo() {
form.append('video', id); form.append('video', id);
form.append('channel', video.channel); form.append('channel', video.channel);
const videoRequest = await fetch(`/api/videos/${id}/video`, { await updateVideoFile(id, form, token, addAlert);
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: form
});
if (!videoRequest.ok) {
addAlert('error', 'Erreur lors de la mise à jour de la vidéo');
return;
}
} }
const data = await request.json(); const data = await request.json();
@ -134,21 +96,7 @@ export default function ManageVideo() {
formData.append('video', id); formData.append('video', id);
formData.append('channel', video.channel); formData.append('channel', video.channel);
const request = await uploadThumbnail(formData, token, addAlert);
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 onAddTag = async (e) => { const onAddTag = async (e) => {
@ -156,23 +104,12 @@ export default function ManageVideo() {
const newTag = e.target.value.trim(); const newTag = e.target.value.trim();
e.target.value = ""; e.target.value = "";
const body = {
const request = await fetch(`/api/videos/${id}/tags`, { tags: [...video.tags, newTag],
method: 'PUT', channel: video.channel
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 data = await request.json();
const request = await uploadTags(body, id, token, addAlert);
setVideo({ setVideo({
...video, ...video,
tags: [...video.tags, newTag] tags: [...video.tags, newTag]
@ -181,22 +118,12 @@ export default function ManageVideo() {
const onSuppressTag = async (tag) => { const onSuppressTag = async (tag) => {
const request = await fetch(`/api/videos/${id}/tags`, { const body = {
method: 'PUT', tags: video.tags.filter(t => t !== tag),
headers: { channel: video.channel
'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 data = await request.json();
const request = await uploadTags(body, id, token, addAlert);
const newTags = video.tags.filter(t => t !== tag); const newTags = video.tags.filter(t => t !== tag);
setVideo({ setVideo({
...video, ...video,

211
frontend/src/pages/Video.jsx

@ -5,10 +5,12 @@ import { useAuth } from "../contexts/AuthContext.jsx";
import Comment from "../components/Comment.jsx"; import Comment from "../components/Comment.jsx";
import VideoCard from "../components/VideoCard.jsx"; import VideoCard from "../components/VideoCard.jsx";
import Tag from "../components/Tag.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() { export default function Video() {
// This component can be used to display a video player or video-related content.
const {id} = useParams(); const {id} = useParams();
const { user, isAuthenticated } = useAuth(); const { user, isAuthenticated } = useAuth();
const videoRef = useRef(null); const videoRef = useRef(null);
@ -26,62 +28,25 @@ export default function Video() {
const fetchVideo = useCallback(async () => { const fetchVideo = useCallback(async () => {
// Fetch video data and similar videos based on the video ID from the URL // Fetch video data and similar videos based on the video ID from the URL
try { if (!id) {
const response = await fetch(`/api/videos/${id}`); addAlert('error', 'Vidéo introuvable');
if (!response.ok) { navigation('/');
throw new Error('Network response was not ok'); return;
}
const videoData = await response.json();
setVideo(videoData);
} catch (error) {
addAlert('error', 'Erreur lors de la récupération de la vidéo');
} }
try {
const response = await fetch(`/api/videos/${id}/similar`); const data = await getVideoById(id, addAlert);
if (!response.ok) { if (!data) return;
throw new Error('Network response was not ok'); setVideo(data);
}
const similarVideosData = await response.json(); const similarVideosResponse = await getSimilarVideos(id, addAlert);
setSimilarVideos(similarVideosData); if (similarVideosResponse) {
} catch (error) { setSimilarVideos(similarVideosResponse);
addAlert('error', 'Erreur lors de la récupération des vidéos similaires');
} }
// Add views to the video // Add views to the video
try { await addView(id, addAlert);
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');
}
}, [id, navigation]); }, [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(() => { useEffect(() => {
fetchVideo(); fetchVideo();
}, [fetchVideo]); }, [fetchVideo]);
@ -171,47 +136,20 @@ export default function Video() {
return; return;
} }
try { const response = await subscribe(video.creator.id, addAlert);
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!token) { console.log('Subscription successful:', response);
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');
}
const data = await response.json(); const subscriptionCount = response.subscriptions || 0;
console.log('Subscription successful:', data); setVideo((prevVideo) => {
return {
const subscriptionCount = data.subscriptions || 0; ...prevVideo,
setVideo((prevVideo) => { creator: {
return { ...prevVideo.creator,
...prevVideo, subscribers: subscriptionCount
creator: { }
...prevVideo.creator, };
subscribers: subscriptionCount })
}
};
})
} catch (error) {
addAlert('error', 'Erreur lors de l\'abonnement');
}
}; };
const handleLike = async () => { const handleLike = async () => {
@ -220,39 +158,22 @@ export default function Video() {
return; return;
} }
try { // Retrieve the token from localStorage
// Retrieve the token from localStorage const token = localStorage.getItem('token');
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}`
}
});
if (!response.ok) { if (!token) {
throw new Error('Failed to like video'); navigation('/login');
} return;
}
const data = await response.json();
setVideo((prevVideo) => { const data = await toggleLike(id, token, addAlert);
return {
...prevVideo,
likes: data.likes || prevVideo.likes + 1 // Update likes count
};
})
} catch (error) { setVideo((prevVideo) => {
addAlert('error', 'Erreur lors de l\'ajout du like'); return {
} ...prevVideo,
likes: data.likes || prevVideo.likes + 1 // Update likes count
};
})
}; };
const handleComment = async () => { const handleComment = async () => {
@ -265,45 +186,21 @@ export default function Video() {
alert("Comment cannot be empty"); alert("Comment cannot be empty");
return; return;
} }
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
try { if (!token) {
// Retrieve the token from localStorage navigation('/login');
const token = localStorage.getItem('token'); return;
}
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
}));
const data = await addComment(video.id, comment, token, addAlert);
setComment(""); // Clear the comment input
} catch (error) { setVideo((prevVideo) => ({
addAlert('error', 'Erreur lors de la publication du commentaire'); ...prevVideo,
} comments: [...(prevVideo.comments || []), data]
}));
} }
const addAlert = (type, message) => { const addAlert = (type, message) => {
@ -432,7 +329,7 @@ export default function Video() {
<div className="mt-4"> <div className="mt-4">
{video.comments && video.comments.length > 0 ? ( {video.comments && video.comments.length > 0 ? (
video.comments.map((comment, index) => ( 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> <p className="text-gray-400">Aucun commentaire pour le moment. Soyez le premier à en publier !</p>

19
frontend/src/routes/routes.jsx

@ -8,6 +8,7 @@ import ManageChannel from "../pages/ManageChannel.jsx";
import ManageVideo from "../pages/ManageVideo.jsx"; import ManageVideo from "../pages/ManageVideo.jsx";
import AddVideo from "../pages/AddVideo.jsx"; import AddVideo from "../pages/AddVideo.jsx";
import Search from "../pages/Search.jsx"; import Search from "../pages/Search.jsx";
import Channel from "../pages/Channel.jsx";
const routes = [ const routes = [
{ path: "/", element: <Home/> }, { path: "/", element: <Home/> },
@ -29,7 +30,11 @@ const routes = [
}, },
{ {
path: "/video/:id", path: "/video/:id",
element: <Video /> element: (
<ProtectedRoute requireAuth={false}>
<Video />
</ProtectedRoute>
)
}, },
{ {
path: "/profile", path: "/profile",
@ -64,8 +69,16 @@ const routes = [
) )
}, },
{ {
path: "/search", path: "search",
element: <Search /> element: (
<Search />
)
},
{
path: "channel/:id",
element: (
<Channel/>
)
} }
] ]

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

@ -0,0 +1,100 @@
export function fetchChannelDetails(channelId, addAlert) {
return fetch(`/api/channels/${channelId}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
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'));
console.log("Subscribing to channel with ID:", channelId, "for user:", user.id);
return fetch(`/api/channels/${channelId}/subscribe`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': "application/json"
},
body: JSON.stringify({
userId: user.id
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
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');
}
}

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

@ -0,0 +1,103 @@
export async function isSubscribed(channelId, addAlert) {
const token = localStorage.getItem('token');
if (!token) {
return;
}
const headers = {
Authorization: `Bearer ${token}`
};
const request = await fetch(`/api/users/${channelId}/channel/subscribed`, { headers })
const result = await request.json();
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