Browse Source

Merge pull request #9 from Astri4-4/manage-video

Manage video
features/create-channel
Sacha GUERIN 5 months ago
committed by GitHub
parent
commit
f1cd373ee2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      backend/app/controllers/channel.controller.js
  2. 6
      backend/app/controllers/comment.controller.js
  3. 18
      backend/app/controllers/playlist.controller.js
  4. 2
      backend/app/controllers/recommendation.controller.js
  5. 2
      backend/app/controllers/search.controller.js
  6. 19
      backend/app/controllers/user.controller.js
  7. 76
      backend/app/controllers/video.controller.js
  8. 5
      backend/app/routes/video.route.js
  9. BIN
      backend/app/uploads/videos/946FFC1D2D8C189D.mp4
  10. 3
      backend/app/utils/database.js
  11. 1773
      backend/logs/access.log
  12. 6
      backend/requests/video.http
  13. 6
      backend/server.js
  14. 37
      frontend/package-lock.json
  15. 2
      frontend/package.json
  16. 7
      frontend/src/components/Comment.jsx
  17. 52
      frontend/src/components/LinearGraph.jsx
  18. 22
      frontend/src/components/Tag.jsx
  19. 417
      frontend/src/pages/ManageVideo.jsx
  20. 8
      frontend/src/pages/Video.jsx
  21. 9
      frontend/src/routes/routes.jsx
  22. 6
      nginx/default.conf

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

@ -18,6 +18,7 @@ export async function create(req, res) {
logger.action("try to create new channel with owner " + channel.owner + " and name " + channel.name); logger.action("try to create new channel with owner " + channel.owner + " and name " + channel.name);
logger.write("Successfully created new channel with name " + channel.name, 200); logger.write("Successfully created new channel with name " + channel.name, 200);
client.end();
res.status(200).json(channel); res.status(200).json(channel);
} }
@ -50,6 +51,7 @@ export async function getById(req, res) {
result.rows[0].videos = videoResult.rows; result.rows[0].videos = videoResult.rows;
logger.write("Successfully get channel with id " + id, 200); logger.write("Successfully get channel with id " + id, 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }
@ -85,6 +87,7 @@ export async function getAll(req, res) {
}) })
logger.write("Successfully get all channels", 200); logger.write("Successfully get all channels", 200);
client.end();
res.status(200).json(result); res.status(200).json(result);
} }
@ -108,6 +111,7 @@ export async function update(req, res) {
const nameResult = await client.query(nameQuery, [channel.name]); const nameResult = await client.query(nameQuery, [channel.name]);
if (nameResult.rows.length > 0) { if (nameResult.rows.length > 0) {
logger.write("failed to update channel because name already taken", 400); logger.write("failed to update channel because name already taken", 400);
client.end();
res.status(400).json({error: 'Name already used'}); res.status(400).json({error: 'Name already used'});
return return
} }
@ -116,6 +120,7 @@ export async function update(req, res) {
const updateQuery = `UPDATE channels SET name = $1, description = $2 WHERE id = $3`; const updateQuery = `UPDATE channels SET name = $1, description = $2 WHERE id = $3`;
await client.query(updateQuery, [channel.name, channel.description, id]); await client.query(updateQuery, [channel.name, channel.description, id]);
logger.write("Successfully updated channel", 200); logger.write("Successfully updated channel", 200);
client.end();
res.status(200).json(channel); res.status(200).json(channel);
} }
@ -128,6 +133,7 @@ export async function del(req, res) {
const query = `DELETE FROM channels WHERE id = $1`; const query = `DELETE FROM channels WHERE id = $1`;
await client.query(query, [id]); await client.query(query, [id]);
logger.write("Successfully deleted channel", 200); logger.write("Successfully deleted channel", 200);
client.end();
res.status(200).json({message: 'Successfully deleted'}); res.status(200).json({message: 'Successfully deleted'});
} }
@ -152,6 +158,7 @@ export async function toggleSubscription(req, res) {
const remainingSubscriptions = countResult.rows[0].count; const remainingSubscriptions = countResult.rows[0].count;
logger.write("Successfully unsubscribed from channel", 200); logger.write("Successfully unsubscribed from channel", 200);
client.end();
res.status(200).json({message: 'Unsubscribed successfully', subscriptions: remainingSubscriptions}); res.status(200).json({message: 'Unsubscribed successfully', subscriptions: remainingSubscriptions});
} else { } else {
// Subscribe // Subscribe
@ -164,6 +171,7 @@ export async function toggleSubscription(req, res) {
const totalSubscriptions = countResult.rows[0].count; const totalSubscriptions = countResult.rows[0].count;
logger.write("Successfully subscribed to channel", 200); logger.write("Successfully subscribed to channel", 200);
client.end();
res.status(200).json({message: 'Subscribed successfully', subscriptions: totalSubscriptions}); res.status(200).json({message: 'Subscribed successfully', subscriptions: totalSubscriptions});
} }
} }
@ -189,6 +197,7 @@ export async function getStats(req, res) {
const client = await getClient(); const client = await getClient();
const result = await client.query(request, [id]); const result = await client.query(request, [id]);
logger.write("Successfully get stats", 200); logger.write("Successfully get stats", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);

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

@ -39,7 +39,7 @@ export async function upload(req, res) {
createdAt: createdAt createdAt: createdAt
} }
client.end();
res.status(200).json(responseComment); res.status(200).json(responseComment);
} }
@ -52,6 +52,7 @@ export async function getByVideo(req, res) {
const query = `SELECT * FROM comments WHERE video = $1`; const query = `SELECT * FROM comments WHERE video = $1`;
const result = await client.query(query, [videoId]); const result = await client.query(query, [videoId]);
logger.write("successfully get comment", 200); logger.write("successfully get comment", 200);
client.end()
res.status(200).json(result.rows); res.status(200).json(result.rows);
} }
@ -63,6 +64,7 @@ export async function getById(req, res) {
const query = `SELECT * FROM comments WHERE id = $1`; const query = `SELECT * FROM comments WHERE id = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("successfully get comment", 200); logger.write("successfully get comment", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }
@ -74,6 +76,7 @@ export async function update(req, res) {
const query = `UPDATE comments SET content = $1 WHERE id = $2`; const query = `UPDATE comments SET content = $1 WHERE id = $2`;
const result = await client.query(query, [req.body.content, id]); const result = await client.query(query, [req.body.content, id]);
logger.write("successfully update comment", 200); logger.write("successfully update comment", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }
@ -85,5 +88,6 @@ export async function del(req, res) {
const query = `DELETE FROM comments WHERE id = $1`; const query = `DELETE FROM comments WHERE id = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("successfully deleted comment", 200); logger.write("successfully deleted comment", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }

18
backend/app/controllers/playlist.controller.js

@ -14,9 +14,11 @@ export async function create(req, res) {
try { try {
const result = await client.query(query, [name, userId]); const result = await client.query(query, [name, userId]);
logger.write("Playlist created with id " + result.rows[0].id, 200); logger.write("Playlist created with id " + result.rows[0].id, 200);
client.end()
res.status(200).json({ id: result.rows[0].id }); res.status(200).json({ id: result.rows[0].id });
} catch (error) { } catch (error) {
logger.write("Error creating playlist: " + error.message, 500); logger.write("Error creating playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -31,9 +33,11 @@ export async function addVideo(req, res) {
try { try {
const result = await client.query(query, [video, id]); const result = await client.query(query, [video, id]);
logger.write("Video added to playlist with id " + id, 200); logger.write("Video added to playlist with id " + id, 200);
client.end();
res.status(200).json({id: result.rows[0].id}); res.status(200).json({id: result.rows[0].id});
} catch (error) { } catch (error) {
logger.write("Error adding video to playlist: " + error.message, 500); logger.write("Error adding video to playlist: " + error.message, 500);
client.end();
res.status(500).json({error: "Internal server error"}); res.status(500).json({error: "Internal server error"});
} }
} }
@ -60,13 +64,16 @@ export async function getByUser(req, res) {
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No playlists found for user with id " + id, 404); logger.write("No playlists found for user with id " + id, 404);
client.end();
res.status(404).json({ error: "No playlists found" }); res.status(404).json({ error: "No playlists found" });
return; return;
} }
logger.write("Playlists retrieved for user with id " + id, 200); logger.write("Playlists retrieved for user with id " + id, 200);
client.end();
res.status(200).json(result.rows); res.status(200).json(result.rows);
} catch (error) { } catch (error) {
logger.write("Error retrieving playlists: " + error.message, 500); logger.write("Error retrieving playlists: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -82,13 +89,16 @@ export async function getById(req, res) {
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No playlist found with id " + id, 404); logger.write("No playlist found with id " + id, 404);
client.end();
res.status(404).json({ error: "Playlist not found" }); res.status(404).json({ error: "Playlist not found" });
return; return;
} }
logger.write("Playlist retrieved with id " + id, 200); logger.write("Playlist retrieved with id " + id, 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} catch (error) { } catch (error) {
logger.write("Error retrieving playlist: " + error.message, 500); logger.write("Error retrieving playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -105,13 +115,16 @@ export async function update(req, res) {
const result = await client.query(query, [name, id]); const result = await client.query(query, [name, id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No playlist found with id " + id, 404); logger.write("No playlist found with id " + id, 404);
client.end();
res.status(404).json({ error: "Playlist not found", result: result.rows, query: query }); res.status(404).json({ error: "Playlist not found", result: result.rows, query: query });
return; return;
} }
logger.write("Playlist updated with id " + result.rows[0].id, 200); logger.write("Playlist updated with id " + result.rows[0].id, 200);
client.end();
res.status(200).json({ id: result.rows[0].id }); res.status(200).json({ id: result.rows[0].id });
} catch (error) { } catch (error) {
logger.write("Error updating playlist: " + error.message, 500); logger.write("Error updating playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -127,13 +140,16 @@ export async function deleteVideo(req, res) {
const result = await client.query(query, [videoId, id]); const result = await client.query(query, [videoId, id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No video found in playlist with id " + id, 404); logger.write("No video found in playlist with id " + id, 404);
client.end();
res.status(404).json({ error: "Video not found in playlist" }); res.status(404).json({ error: "Video not found in playlist" });
return; return;
} }
logger.write("Video deleted from playlist with id " + id, 200); logger.write("Video deleted from playlist with id " + id, 200);
client.end();
res.status(200).json({ id: result.rows[0].id }); res.status(200).json({ id: result.rows[0].id });
} catch (error) { } catch (error) {
logger.write("Error deleting video from playlist: " + error.message, 500); logger.write("Error deleting video from playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -148,9 +164,11 @@ export async function del(req, res) {
try { try {
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("Playlist deleted", 200); logger.write("Playlist deleted", 200);
client.end()
res.status(200).json({ "message": "playlist deleted" }); res.status(200).json({ "message": "playlist deleted" });
} catch (error) { } catch (error) {
logger.write("Error deleting playlist: " + error.message, 500); logger.write("Error deleting playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }

2
backend/app/controllers/recommendation.controller.js

@ -30,6 +30,7 @@ export async function getRecommendations(req, res) {
// Recuperer 10 videos avec les 3 tags ayant le plus d'interaction avec l'utilisateur // Recuperer 10 videos avec les 3 tags ayant le plus d'interaction avec l'utilisateur
client.end()
res.status(200).json({ res.status(200).json({
message: "Recommendations based on user history and interactions are not yet implemented." message: "Recommendations based on user history and interactions are not yet implemented."
}); });
@ -80,6 +81,7 @@ export async function getTrendingVideos(req, res) {
} }
client.end();
res.status(200).json(trendingVideos); res.status(200).json(trendingVideos);
} catch (error) { } catch (error) {
console.error("Error fetching trending videos:", error); console.error("Error fetching trending videos:", error);

2
backend/app/controllers/search.controller.js

@ -76,7 +76,7 @@ export async function search(req, res) {
} }
client.end()
return res.status(200).json(videos); return res.status(200).json(videos);

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

@ -99,6 +99,7 @@ export async function login(req, res) {
} }
logger.write("Successfully logged in", 200); logger.write("Successfully logged in", 200);
client.end();
res.status(200).json({token: token, user: userData}); res.status(200).json({token: token, user: userData});
} }
@ -108,16 +109,21 @@ export async function getById(req, res) {
const logger = req.body.logger; const logger = req.body.logger;
logger.action("try to retrieve user " + id); logger.action("try to retrieve user " + id);
const client = await getClient(); const client = await getClient();
const query = `SELECT id, email, username, picture FROM users WHERE id = $1`; const query = `SELECT id, email, username, picture
FROM users
WHERE id = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve user " + id + " because it doesn't exist", 404); logger.write("failed to retrieve user " + id + " because it doesn't exist", 404);
client.end()
res.status(404).json({error: "Not Found"}); res.status(404).json({error: "Not Found"});
return return
} }
logger.write("successfully retrieved user " + id, 200); logger.write("successfully retrieved user " + id, 200);
if (result.rows[0].picture) {
return res.status(200).json({user: result.rows[0]}); return res.status(200).json({user: result.rows[0]});
} }
}
export async function getByUsername(req, res) { export async function getByUsername(req, res) {
const username = req.params.username; const username = req.params.username;
@ -128,10 +134,12 @@ export async function getByUsername(req, res) {
const result = await client.query(query, [username]); const result = await client.query(query, [username]);
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve user " + username + " because it doesn't exist", 404); logger.write("failed to retrieve user " + username + " because it doesn't exist", 404);
client.end()
res.status(404).json({error: "Not Found"}); res.status(404).json({error: "Not Found"});
return return
} }
logger.write("successfully retrieved user " + username, 200); logger.write("successfully retrieved user " + username, 200);
client.end();
return res.status(200).json({user: result.rows[0]}); return res.status(200).json({user: result.rows[0]});
} }
@ -159,6 +167,7 @@ export async function update(req, res) {
const emailResult = await client.query(emailQuery, [user.email]); const emailResult = await client.query(emailQuery, [user.email]);
if (emailResult.rows[0]) { if (emailResult.rows[0]) {
logger.write("failed to update because email is already used", 400) logger.write("failed to update because email is already used", 400)
client.end();
res.status(400).json({error: "Email already exists"}); res.status(400).json({error: "Email already exists"});
} }
} }
@ -168,6 +177,7 @@ export async function update(req, res) {
const usernameResult = await client.query(usernameQuery, [user.username]); const usernameResult = await client.query(usernameQuery, [user.username]);
if (usernameResult.rows[0]) { if (usernameResult.rows[0]) {
logger.write("failed to update because username is already used", 400) logger.write("failed to update because username is already used", 400)
client.end();
res.status(400).json({error: "Username already exists"}); res.status(400).json({error: "Username already exists"});
} }
} }
@ -204,9 +214,11 @@ export async function update(req, res) {
const updateQuery = `UPDATE users SET email = $1, username = $2, password = $3, picture = $4 WHERE id = $5 RETURNING id, email, username, picture`; const updateQuery = `UPDATE users SET email = $1, username = $2, password = $3, picture = $4 WHERE id = $5 RETURNING id, email, username, picture`;
const result = await client.query(updateQuery, [user.email, user.username, user.password, profilePicture, id]); const result = await client.query(updateQuery, [user.email, user.username, user.password, profilePicture, id]);
logger.write("successfully updated user " + id, 200); logger.write("successfully updated user " + id, 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
client.end()
res.status(500).json({error: err}); res.status(500).json({error: err});
} }
@ -220,6 +232,7 @@ export async function deleteUser(req, res) {
const query = `DELETE FROM users WHERE id = $1`; const query = `DELETE FROM users WHERE id = $1`;
await client.query(query, [id]); await client.query(query, [id]);
logger.write("successfully deleted user " + id); logger.write("successfully deleted user " + id);
client.end();
res.status(200).json({message: 'User deleted'}); res.status(200).json({message: 'User deleted'});
} }
@ -234,11 +247,13 @@ export async function getChannel(req, res) {
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve channel of user " + id + " because it doesn't exist", 404); logger.write("failed to retrieve channel of user " + id + " because it doesn't exist", 404);
client.end();
res.status(404).json({error: "Channel Not Found"}); res.status(404).json({error: "Channel Not Found"});
return; return;
} }
logger.write("successfully retrieved channel of user " + id, 200); logger.write("successfully retrieved channel of user " + id, 200);
client.end();
res.status(200).json({channel: result.rows[0]}); res.status(200).json({channel: result.rows[0]});
} }
@ -285,9 +300,11 @@ export async function getHistory(req, res) {
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve history of user " + id + " because it doesn't exist", 404); logger.write("failed to retrieve history of user " + id + " because it doesn't exist", 404);
res.status(404).json({error: "History Not Found"}); res.status(404).json({error: "History Not Found"});
client.end();
return; return;
} }
logger.write("successfully retrieved history of user " + id, 200); logger.write("successfully retrieved history of user " + id, 200);
client.end();
res.status(200).json(videos); res.status(200).json(videos);
} }

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

@ -52,6 +52,7 @@ export async function upload(req, res) {
const query = `INSERT INTO videos (title, thumbnail, description, channel, visibility, file, slug, format, release_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`; const query = `INSERT INTO videos (title, thumbnail, description, channel, visibility, file, slug, format, release_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`;
await client.query(query, [video.title, 'null', video.description, video.channel, video.visibility, video.file, video.slug, video.format, releaseDate]); await client.query(query, [video.title, 'null', video.description, video.channel, video.visibility, video.file, video.slug, video.format, releaseDate]);
logger.write("successfully uploaded video", 200); logger.write("successfully uploaded video", 200);
await client.end()
res.status(200).json({"message": "Successfully uploaded video"}); res.status(200).json({"message": "Successfully uploaded video"});
} }
@ -75,6 +76,7 @@ export async function uploadThumbnail(req, res) {
const updateQuery = `UPDATE videos SET thumbnail = $1 WHERE id = $2`; const updateQuery = `UPDATE videos SET thumbnail = $1 WHERE id = $2`;
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();
res.status(200).json({"message": "Successfully uploaded thumbnail"}); res.status(200).json({"message": "Successfully uploaded thumbnail"});
} }
@ -97,7 +99,7 @@ export async function getById(req, res) {
video.likes = likesResult.rows[0].like_count; video.likes = likesResult.rows[0].like_count;
// GET COMMENTS // GET COMMENTS
const commentsQuery = `SELECT c.id, c.content, c.created_at, u.username, u.picture FROM comments c JOIN users u ON c.author = u.id WHERE c.video = $1`; const commentsQuery = `SELECT c.id, c.content, c.created_at, u.username, u.picture FROM comments c JOIN users u ON c.author = u.id WHERE c.video = $1 ORDER BY c.created_at DESC`;
const commentsResult = await client.query(commentsQuery, [id]); const commentsResult = await client.query(commentsQuery, [id]);
video.comments = commentsResult.rows; video.comments = commentsResult.rows;
@ -122,6 +124,7 @@ export async function getById(req, res) {
video.tags = tagsResult.rows.map(tag => tag.name); video.tags = tagsResult.rows.map(tag => tag.name);
logger.write("successfully get video " + id, 200); logger.write("successfully get video " + id, 200);
client.end()
res.status(200).json(video); res.status(200).json(video);
} }
@ -133,6 +136,7 @@ export async function getByChannel(req, res) {
const query = `SELECT * FROM videos WHERE channel = $1`; const query = `SELECT * FROM videos WHERE channel = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("successfully get video from channel " + id, 200); logger.write("successfully get video from channel " + id, 200);
client.end()
res.status(200).json(result.rows); res.status(200).json(result.rows);
} }
@ -144,6 +148,7 @@ export async function update(req, res) {
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]);
logger.write("successfully updated video", 200); logger.write("successfully updated video", 200);
client.end()
res.status(200).json({"message": "Successfully updated video"}); res.status(200).json({"message": "Successfully updated video"});
} }
@ -161,6 +166,7 @@ export async function updateVideo(req, res) {
fs.unlink(pathToDelete, (error) => { fs.unlink(pathToDelete, (error) => {
if (error) { if (error) {
logger.write(error, 500); 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 return
} }
@ -172,6 +178,7 @@ export async function updateVideo(req, res) {
fs.writeFileSync(destinationPath, fileBuffer); fs.writeFileSync(destinationPath, fileBuffer);
logger.write("successfully updated video", 200); logger.write("successfully updated video", 200);
client.end()
res.status(200).json({"message": "Successfully updated video"}); res.status(200).json({"message": "Successfully updated video"});
}) })
@ -193,6 +200,7 @@ export async function del(req, res) {
fs.unlink(pathToDelete, (error) => { fs.unlink(pathToDelete, (error) => {
if (error) { if (error) {
logger.write(error, 500); 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 return
} }
@ -207,6 +215,7 @@ export async function del(req, res) {
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()
res.status(200).json({"message": "Successfully deleted video"}); res.status(200).json({"message": "Successfully deleted video"});
}) })
}) })
@ -237,6 +246,7 @@ export async function toggleLike(req, res) {
const likesCount = likesCountResult.rows[0].like_count; const likesCount = likesCountResult.rows[0].like_count;
logger.write("no likes found adding likes for video " + id, 200); 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 { } else {
const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`; const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`;
@ -248,6 +258,7 @@ export async function toggleLike(req, res) {
const likesCount = likesCountResult.rows[0].like_count; const likesCount = likesCountResult.rows[0].like_count;
logger.write("likes found, removing like for video " + id, 200); 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});
} }
@ -295,9 +306,15 @@ export async function addTags(req, res) {
const insertVideoTagQuery = `INSERT INTO video_tags (tag, video) VALUES ($1, $2)`; const insertVideoTagQuery = `INSERT INTO video_tags (tag, video) VALUES ($1, $2)`;
await client.query(insertVideoTagQuery, [id, videoId]); await client.query(insertVideoTagQuery, [id, videoId]);
} }
// GET UPDATED TAGS FOR VIDEO
const updatedTagsQuery = `SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag WHERE vt.video = $1`;
const updatedTagsResult = await client.query(updatedTagsQuery, [videoId]);
const updatedTags = updatedTagsResult.rows;
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"}); res.status(200).json({"message": "Successfully added tags to video", "tags" : updatedTags.map(tag => tag.name)});
} }
@ -355,10 +372,62 @@ export async function getSimilarVideos(req, res) {
} }
logger.write("successfully retrieved similar videos for video " + id, 200); logger.write("successfully get similar videos for video " + id, 200);
await client.end();
res.status(200).json(result.rows); res.status(200).json(result.rows);
} }
export async function getLikesPerDay(req, res) {
const id = req.params.id;
const logger = req.body.logger;
logger.action("try to get likes per day");
const client = await getClient();
try {
const response = {}
const likeQuery = `
SELECT
DATE(created_at) as date,
COUNT(*) as count
FROM likes
WHERE video = $1
GROUP BY DATE(created_at)
ORDER BY date DESC
LIMIT 30
`;
const viewQuery = `
SELECT
DATE(viewed_at) as date,
COUNT(*) as count
FROM history
WHERE video = $1
GROUP BY DATE(viewed_at)
ORDER BY date DESC
LIMIT 30
`;
const resultViews = await client.query(viewQuery, [id]);
response.views = resultViews.rows;
const resultLikes = await client.query(likeQuery, [id]);
response.likes = resultLikes.rows;
console.log(response);
logger.write("successfully retrieved likes per day", 200);
res.status(200).json(response);
} catch (error) {
logger.write("Error retrieving likes per day: " + error.message, 500);
res.status(500).json({ error: "Internal server error" });
} finally {
await client.end();
}
}
export async function addViews(req, res) { export async function addViews(req, res) {
const id = req.params.id; const id = req.params.id;
const logger = req.body.logger; const logger = req.body.logger;
@ -378,5 +447,6 @@ 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();
res.status(200).json({"message": "Successfully added views"}); res.status(200).json({"message": "Successfully added views"});
} }

5
backend/app/routes/video.route.js

@ -10,7 +10,7 @@ import {
uploadThumbnail, uploadThumbnail,
updateVideo, updateVideo,
toggleLike, toggleLike,
addTags, getSimilarVideos, addViews addTags, getSimilarVideos, addViews, getLikesPerDay
} from "../controllers/video.controller.js"; } from "../controllers/video.controller.js";
import { import {
doVideoExists, doVideoExists,
@ -58,5 +58,8 @@ router.get("/:id/similar", [addLogger, Video.id, validator, doVideoExistsParam],
// ADD VIEWS // ADD VIEWS
router.get("/:id/views", [addLogger, isTokenValid, Video.id, validator, doVideoExistsParam], addViews); router.get("/:id/views", [addLogger, isTokenValid, Video.id, validator, doVideoExistsParam], addViews);
// GET LIKE PER DAY
router.get("/:id/likes/day", [addLogger, isTokenValid, Video.id, validator, doVideoExistsParam], getLikesPerDay);
export default router; export default router;

BIN
backend/app/uploads/videos/946FFC1D2D8C189D.mp4

Binary file not shown.

3
backend/app/utils/database.js

@ -62,7 +62,8 @@ export async function initDb() {
query = `CREATE TABLE IF NOT EXISTS likes ( query = `CREATE TABLE IF NOT EXISTS likes (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
owner INTEGER NOT NULL REFERENCES users(id), owner INTEGER NOT NULL REFERENCES users(id),
video INTEGER NOT NULL REFERENCES videos(id) video INTEGER NOT NULL REFERENCES videos(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);`; );`;
await client.query(query); await client.query(query);

1773
backend/logs/access.log

File diff suppressed because it is too large

6
backend/requests/video.http

@ -1,4 +1,4 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTI5NDQxMDF9.dbGCL8qqqLR3e7Ngns-xPfZAvp0WQzAbjaEHjDVg1HI @token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTMzODAyNjB9._rUcieo3acJp6tjQao7V3UQz0_ngHuB2z36_fG_fIX8
### UPDATE VIDEO ### UPDATE VIDEO
PUT http://127.0.0.1:8000/api/videos/3 PUT http://127.0.0.1:8000/api/videos/3
@ -16,7 +16,7 @@ GET http://127.0.0.1:8000/api/videos/14/like
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
### ADD TAGS ### ADD TAGS
PUT http://127.0.0.1:8000/api/videos/2/tags PUT http://127.0.0.1:8000/api/videos/3/tags
Content-Type: application/json Content-Type: application/json
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
@ -26,7 +26,7 @@ Authorization: Bearer {{token}}
"Create Mod", "Create Mod",
"Redstone" "Redstone"
], ],
"channel": 2 "channel": 1
} }
### ###

6
backend/server.js

@ -20,9 +20,9 @@ const app = express();
// INITIALIZE DATABASE // INITIALIZE DATABASE
// Increase body size limits for file uploads
app.use(express.urlencoded({extended: true})); app.use(express.urlencoded({extended: true, limit: '500mb'}));
app.use(express.json()); app.use(express.json({limit: '500mb'}));
app.use(cors()) app.use(cors())
// ROUTES // ROUTES

37
frontend/package-lock.json

@ -9,7 +9,10 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"chart.js": "^4.5.0",
"chartjs": "^0.3.24",
"react": "^19.1.0", "react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.3", "react-router-dom": "^7.6.3",
"tailwindcss": "^4.1.11" "tailwindcss": "^4.1.11"
@ -1004,6 +1007,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19", "version": "1.0.0-beta.19",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
@ -1794,6 +1803,24 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chart.js": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chartjs": {
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/chartjs/-/chartjs-0.3.24.tgz",
"integrity": "sha512-h6G9qcDqmFYnSWqjWCzQMeOLiypS+pM6Fq2Rj7LPty8Kjx5yHonwwJ7oEHImZpQ2u9Pu36XGYfardvvBiQVrhg==",
"license": "MIT"
},
"node_modules/chownr": { "node_modules/chownr": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@ -2999,6 +3026,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",

2
frontend/package.json

@ -11,7 +11,9 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"chart.js": "^4.5.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.3", "react-router-dom": "^7.6.3",
"tailwindcss": "^4.1.11" "tailwindcss": "^4.1.11"

7
frontend/src/components/Comment.jsx

@ -2,7 +2,7 @@ import {useAuth} from "../contexts/AuthContext.jsx";
import {useRef, useState} from "react"; import {useRef, useState} from "react";
export default function Comment({ comment, index, videoId, refetchVideo }) { export default function Comment({ comment, index, videoId, refetchVideo, doShowCommands=true }) {
let {user, isAuthenticated} = useAuth(); let {user, isAuthenticated} = useAuth();
let commentRef = useRef(); let commentRef = useRef();
@ -82,8 +82,11 @@ export default function Comment({ comment, index, videoId, refetchVideo }) {
className="w-8 h-8 rounded-full object-cover mr-3" className="w-8 h-8 rounded-full object-cover mr-3"
/> />
<span className="font-montserrat font-bold text-white">{comment.username}</span> <span className="font-montserrat font-bold text-white">{comment.username}</span>
<span className="text-gray-400 ml-2 text-sm">{new Date(comment.created_at).toLocaleDateString()}</span>
</div> </div>
<p className={(editMode) ? editClass : "text-white focus:outline-none "} ref={commentRef}>{comment.content}</p> <p className={(editMode) ? editClass : "text-white focus:outline-none "} ref={commentRef}>{comment.content}</p>
{
doShowCommands && (
<div className="flex gap-2 items-center mt-2"> <div className="flex gap-2 items-center mt-2">
{ isAuthenticated && user.username === comment.username && editMode === false ? ( { isAuthenticated && user.username === comment.username && editMode === false ? (
<button className="text-blue-500 mt-2 hover:underline" onClick={() => handleEdit(comment.id)}> <button className="text-blue-500 mt-2 hover:underline" onClick={() => handleEdit(comment.id)}>
@ -115,6 +118,8 @@ export default function Comment({ comment, index, videoId, refetchVideo }) {
</button> </button>
) : null } ) : null }
</div> </div>
)
}
</div> </div>
) )

52
frontend/src/components/LinearGraph.jsx

@ -0,0 +1,52 @@
import {Line} from "react-chartjs-2";
import {Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
export default function LinearGraph({ dataToGraph, className, legend, borderColor="rgba(75, 192, 192, 1)" }) {
const prepareData = () => {
if (!dataToGraph || dataToGraph.length === 0) {
return {
labels: [],
datasets: []
};
}
const labels = dataToGraph.map(item => item.date.split("T")[0]);
const data = dataToGraph.map(item => item.count);
return {
labels,
datasets: [{
label: legend,
data,
fill: false,
pointRadius: 3,
borderColor: borderColor,
tension: 0,
stepped: false
}]
};
}
const data = prepareData();
const options = {
}
return (
<div className={className}>
<Line options={options} data={data} className="w-full border-red-500 " />
</div>
)
}

22
frontend/src/components/Tag.jsx

@ -0,0 +1,22 @@
export default function Tag({ tag, onSuppress, doShowControls=true }) {
return (
<div className="glassmorphism px-2 py-1 w-max flex flex-row items-center gap-2">
<span className="font-inter text-white">#{tag}</span>
{doShowControls && (
<span className="tag-controls cursor-pointer" onClick={onSuppress}>
<svg
className="w-6 h-6 fill-white"
stroke="#FFF"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</span>
)}
</div>
)
}

417
frontend/src/pages/ManageVideo.jsx

@ -0,0 +1,417 @@
import Navbar from "../components/Navbar.jsx";
import {useParams} from "react-router-dom";
import {useAuth} from "../contexts/AuthContext.jsx";
import {useEffect, useState} from "react";
import LinearGraph from "../components/LinearGraph.jsx";
import Comment from "../components/Comment.jsx";
import Tag from "../components/Tag.jsx";
export default function ManageVideo() {
const { user } = useAuth();
const token = localStorage.getItem("token");
const {id} = useParams();
const [video, setVideo] = useState(null);
const [likesPerDay, setLikesPerDay] = useState([]);
const [viewsPerDay, setViewsPerDay] = useState([]);
const [videoTitle, setVideoTitle] = useState(null);
const [description, setDescription] = useState(null);
const [visibility, setVisibility] = useState("private");
const [editMode, setEditMode] = useState(false);
const [thumbnailPreview, setThumbnailPreview] = useState(null);
const [videoFile, setVideoFile] = useState(null);
const nonEditModeClasses = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none";
const editModeClasses = nonEditModeClasses + " glassmorphism";
const nonEditModeClassesTextArea = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none w-full"
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) {
throw new Error("Failed to fetch video");
}
const data = await request.json();
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) {
throw new Error("Failed to fetch likes per day");
}
const data = await request.json();
setLikesPerDay(data.likes);
setViewsPerDay(data.views);
}
useEffect(() => {
if (user) {
fetchVideo()
fetchLikesPerDay()
}
}, [user, id, token]);
const onSubmit = async (e) => {
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
})
});
if (!request.ok) {
console.error("Failed to update video");
return;
}
const form = new FormData();
if (videoFile) {
form.append('file', videoFile);
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) {
console.error("Failed to update video file");
return;
}
}
const data = await request.json();
setVideo(data);
setEditMode(false);
}
const handleThumbnailChange = async (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setThumbnailPreview(e.target.result);
};
reader.readAsDataURL(file);
}
const formData = new FormData();
formData.append('file', file);
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) {
console.error("Failed to upload thumbnail");
return;
}
const data = await request.json();
};
const onAddTag = async (e) => {
if (e.key !== 'Enter' || e.target.value.trim() === "") return;
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) {
console.error("Failed to add tag");
return;
}
const data = await request.json();
setVideo({
...video,
tags: [...video.tags, newTag]
});
}
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) {
console.error("Failed to suppress tag");
return;
}
const data = await request.json();
const newTags = video.tags.filter(t => t !== tag);
setVideo({
...video,
tags: newTags
});
}
return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar/>
<main className="px-36 pb-36">
{ /* GRAPHS */ }
<div>
<div className="flex pt-[118px] gap-4" >
<LinearGraph
dataToGraph={viewsPerDay}
legend="Vues"
className="glassmorphism flex-1 h-[300px] p-4"
/>
<LinearGraph
dataToGraph={likesPerDay}
legend="Likes"
className="glassmorphism flex-1 h-[300px] p-4"
borderColor="#FF073A"
/>
</div>
<div className="flex gap-4 mt-4 ">
{ /* LEFT SIDE */ }
<div className="flex-1" >
{ /* THUMBNAIL */ }
<label htmlFor="thumbnail" className="glassmorphism flex justify-center items-center py-5 relative overflow-hidden ">
<img src={thumbnailPreview || (video ? video.thumbnail : "")} alt="" className=" rounded-sm"/>
<div className="absolute top-0 left-0 bg-[#00000080] w-full h-full flex justify-center items-center opacity-0 hover:opacity-100 transition" >
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" className="fill-white" viewBox="0 0 24 24">
<path d="M19.045 7.401c.378-.378.586-.88.586-1.414s-.208-1.036-.586-1.414l-1.586-1.586c-.378-.378-.88-.586-1.414-.586s-1.036.208-1.413.585L4 13.585V18h4.413L19.045 7.401zm-3-3 1.587 1.585-1.59 1.584-1.586-1.585 1.589-1.584zM6 16v-1.585l7.04-7.018 1.586 1.586L7.587 16H6zm-2 4h16v2H4z"></path>
</svg>
</div>
</label>
<input type="file" name="thumbnail" id="thumbnail" className="opacity-0 w-0 h-0 hidden" onChange={handleThumbnailChange} accept="image/*"/>
{ /* VIDEO INFOS */ }
<form className="glassmorphism p-4 mt-4 flex-1" >
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat`}>
Titre de la vidéo
</label>
<input
type="text"
id="name"
value={videoTitle || videoTitle === "" ? videoTitle : video ? video.title : "Chargement"}
onChange={(e) => setVideoTitle(e.target.value)}
className={(editMode ? editModeClasses : nonEditModeClasses) + " text-xl"}
placeholder="Titre de la vidéo"
disabled={!editMode}
/>
<label htmlFor="name" className={`text-2xl text-white mb-1 block font-montserrat`}>
Description
</label>
<textarea
name="description"
id=""
className={(editMode ? editModeClassesTextArea : nonEditModeClassesTextArea)}
value={description || description === "" ? description : video ? video.description : "Chargement"}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description de votre chaine"
disabled={!editMode}
></textarea>
<label htmlFor="visibility" className="text-2xl text-white mb-1 block font-montserrat">
Visibilité
</label>
<select
className={(editMode ? editModeClasses : nonEditModeClasses)}
value={visibility}
name="visibility"
onChange={(e) => setVisibility(e.target.value)}
disabled={!editMode}
>
<option value="public">Publique</option>
<option value="private">Privée</option>
</select>
<label htmlFor="video" className={`flex gap-2 glassmorphism p-2 items-center mt-4 cursor-pointer ${editMode ? "block" : "hidden"}`}>
<video src={video ? video.file : ""} className="w-1/8 aspect-video rounded-sm" ></video>
<p className="text-2xl text-white mb-1 block font-montserrat">Fichier vidéo</p>
</label>
<input
type="file"
name="video"
id="video"
className="hidden"
accept="video/*"
onChange={(e) => setVideoFile(e.target.files[0])}
/>
{
editMode ? (
<div className="mt-4">
<button
type="button"
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer"
onClick={(e) => {onSubmit(e)}}
>
Enregistrer
</button>
<button
type="button"
className="bg-red-500 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer ml-3"
onClick={() => setEditMode(!editMode)}
>
Annuler
</button>
</div>
) : (
<button
type="button"
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer mt-4"
onClick={() => setEditMode(!editMode)}
>
Modifier
</button>
)
}
</form>
</div>
{ /* RIGHT SIDE */ }
<div className="flex-1">
<div className="flex gap-4">
{ /* TOTAL VIEWS */ }
<div className="glassmorphism py-16 flex-1">
<p className="text-2xl font-bold text-white mb-2 text-center">{video ? video.views : 0} vues</p>
</div>
{ /* TOTAL LIKES */ }
<div className="glassmorphism py-16 flex-1">
<p className="text-2xl font-bold text-white mb-2 text-center">{video ? video.likes : 0} likes</p>
</div>
</div>
{ /* COMMENTS */ }
<div className="glassmorphism p-4 mt-4 h-[500px] overflow-y-auto">
<h2 className="text-2xl font-bold text-white mb-4">Commentaires</h2>
{video && video.comments && video.comments.length > 0 ? (
video.comments.map((comment) => (
<Comment comment={comment} doShowCommands={false} />
))
) : (
<p className="text-gray-500">Aucun commentaire</p>
)}
</div>
{ /* TAGS */ }
<div className="glassmorphism p-4 mt-4">
<h2 className="text-2xl font-bold text-white mb-4">Tags</h2>
<div className="flex flex-wrap gap-2">
{ video && video.tags && video.tags.length > 0 ? (
video.tags.map((tag) => (
<Tag tag={tag} key={tag} onSuppress={() => onSuppressTag(tag)} />
))
) : (
<p className="text-gray-500">Aucun tag</p>
)}
</div>
<input
type="text"
className="glassmorphism text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none mt-4"
placeholder="Ajouter un tag"
onKeyPress={(e) => onAddTag(e)}
/>
</div>
{ /* LINK */ }
<div className="glassmorphism p-4 mt-4">
<h2 className="text-2xl font-bold text-white mb-4">Lien de la vidéo</h2>
<p className="text-md font-normal text-white">
<a href={`/video/${id}`} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">
{window.location.origin}/video/{id}
</a>
<button
className="ml-2 bg-primary text-white p-2 rounded-sm cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/video/${id}`);
}}
>
Copier
</button>
</p>
</div>
</div>
</div>
</div>
</main>
</div>
)
}

8
frontend/src/pages/Video.jsx

@ -4,6 +4,7 @@ import Navbar from "../components/Navbar.jsx";
import { useAuth } from "../contexts/AuthContext.jsx"; 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";
export default function Video() { export default function Video() {
@ -396,12 +397,7 @@ export default function Video() {
<div className="mb-3"> <div className="mb-3">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{video.tags.map((tag, index) => ( {video.tags.map((tag, index) => (
<span <Tag tag={tag} key={index} doShowControls={false} />
key={index}
className="bg-gray-700 text-white px-3 py-1 rounded-full text-sm font-montserrat"
>
#{tag}
</span>
))} ))}
</div> </div>
</div> </div>

9
frontend/src/routes/routes.jsx

@ -5,6 +5,7 @@ import Video from '../pages/Video.jsx'
import ProtectedRoute from '../components/ProtectedRoute.jsx' import ProtectedRoute from '../components/ProtectedRoute.jsx'
import Account from "../pages/Account.jsx"; import Account from "../pages/Account.jsx";
import ManageChannel from "../pages/ManageChannel.jsx"; import ManageChannel from "../pages/ManageChannel.jsx";
import ManageVideo from "../pages/ManageVideo.jsx";
const routes = [ const routes = [
{ path: "/", element: <Home/> }, { path: "/", element: <Home/> },
@ -43,6 +44,14 @@ const routes = [
<ManageChannel/> <ManageChannel/>
</ProtectedRoute> </ProtectedRoute>
) )
},
{
path: "/manage-video/:id",
element: (
<ProtectedRoute requireAuth={true}>
<ManageVideo/>
</ProtectedRoute>
)
} }
] ]

6
nginx/default.conf

@ -12,6 +12,9 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;
# Allow large file uploads for videos (up to 500MB)
client_max_body_size 500M;
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key; ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
@ -26,6 +29,9 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; proxy_buffering off;
# Also set timeout for large uploads
proxy_read_timeout 300s;
proxy_send_timeout 300s;
} }
# Static assets - NO CACHING for development # Static assets - NO CACHING for development

Loading…
Cancel
Save