Browse Source

feat: Implement comprehensive video player with interactive controls

- Add custom video player with play/pause functionality
- Implement interactive progress bar with click-to-seek capability
- Add time display showing current time and total duration
- Create hover-based control visibility with smooth transitions
- Add glassmorphism styling for modern control overlay
- Implement video metadata loading and time updates
- Add creator information display with profile picture
- Implement subscribe functionality with authentication checks
- Add like button with API integration
- Include proper error handling for video fetching
- Add responsive design with aspect ratio maintenance
- Integrate with authentication context for user actions

Technical details:
- Uses React hooks (useState, useRef, useEffect) for state management
- Implements proper video event handlers (onPlay, onTimeUpdate, onLoadedMetadata)
- Includes JWT token authentication for API calls
- Features smooth CSS transitions and hover effects
- Properly formats time display (MM:SS format)
- Handles authentication redirects for protected actions
fix/clean
Astri4-4 5 months ago
parent
commit
3a78d73f3c
  1. 18
      backend/app/controllers/media.controller.js
  2. 27
      backend/app/controllers/recommendation.controller.js
  3. 34
      backend/app/controllers/video.controller.js
  4. 4
      backend/app/routes/media.routes.js
  5. 2
      backend/app/routes/video.route.js
  6. 296
      backend/logs/access.log
  7. 1
      frontend/Dockerfile
  8. 15
      frontend/rebuild.sh
  9. 3
      frontend/src/assets/svg/play.svg
  10. 9
      frontend/src/components/Recommendations.jsx
  11. 18
      frontend/src/components/TopCreators.jsx
  12. 17
      frontend/src/components/TrendingVideos.jsx
  13. 27
      frontend/src/components/VideoCard.jsx
  14. 21
      frontend/src/index.css
  15. 74
      frontend/src/pages/Home.jsx
  16. 263
      frontend/src/pages/Video.jsx
  17. 5
      frontend/src/routes/routes.jsx
  18. 2
      frontend/tailwind.config.js
  19. 23
      frontend/vite.config.js
  20. 8
      nginx/default.conf

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

@ -55,3 +55,21 @@ export async function getThumbnail(req, res) {
res.status(500).json({ error: "Internal server error while fetching thumbnail." });
}
}
export async function getVideo(req, res) {
const file = req.params.file;
const filePath = path.join('/app/app/uploads/videos', file);
try {
res.sendFile(filePath, (err) => {
if (err) {
console.error("Error sending video:", err);
} else {
console.log("Video sent successfully.");
}
});
} catch (error) {
console.error("Error fetching video:", error);
res.status(500).json({error: "Internal server error while fetching video."});
}
}

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

@ -53,6 +53,33 @@ export async function getTrendingVideos(req, res) {
`;
let result = await client.query(queryTrendingVideos);
const trendingVideos = result.rows;
for (let video of trendingVideos) {
// Get the number of views for each video
let viewsQuery = `SELECT COUNT(*) AS view_count FROM history WHERE video = $1;`;
let viewsResult = await client.query(viewsQuery, [video.id]);
video.views = viewsResult.rows[0].view_count;
// Get the creator of each video
let creatorQuery = `SELECT c.id, c.name FROM channels c JOIN videos v ON c.id = v.channel WHERE v.id = $1;`;
let creatorResult = await client.query(creatorQuery, [video.id]);
if (creatorResult.rows.length > 0) {
video.creator = creatorResult.rows[0];
} else {
video.creator = {id: null, name: "Unknown"};
}
// GET THE PROFILE PICTURE OF THE CREATOR
let profilePictureQuery = `SELECT u.picture FROM users u JOIN channels c ON u.id = c.owner WHERE c.id = $1;`;
let profilePictureResult = await client.query(profilePictureQuery, [video.creator.id]);
if (profilePictureResult.rows.length > 0) {
video.creator.profilePicture = profilePictureResult.rows[0].picture;
} else {
video.creator.profilePicture = null; // Default or placeholder image can be set here
}
}
res.status(200).json(trendingVideos);
} catch (error) {
console.error("Error fetching trending videos:", error);

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

@ -87,6 +87,40 @@ export async function getById(req, res) {
const query = `SELECT * FROM videos WHERE id = ${id}`;
const result = await client.query(query);
const video = result.rows[0];
// GET VIEWS AND LIKES COUNT
const viewsQuery = `SELECT COUNT(*) AS view_count FROM history WHERE video = ${id}`;
const viewsResult = await client.query(viewsQuery);
const likesQuery = `SELECT COUNT(*) AS like_count FROM likes WHERE video = ${id}`;
const likesResult = await client.query(likesQuery);
video.views = viewsResult.rows[0].view_count;
video.likes = likesResult.rows[0].like_count;
// GET COMMENTS
const commentsQuery = `SELECT c.id, c.content, c.created_at, u.username FROM comments c JOIN users u ON c.author = u.id WHERE c.video = ${id}`;
const commentsResult = await client.query(commentsQuery);
video.comments = commentsResult.rows;
// GET CREATOR
const creatorQuery = `SELECT c.id, c.name, c.owner FROM channels c JOIN videos v ON c.id = v.channel WHERE v.id = ${id}`;
const creatorResult = await client.query(creatorQuery);
video.creator = creatorResult.rows[0];
// GET CREATOR PROFILE PICTURE
const profilePictureQuery = `SELECT picture FROM users WHERE id = ${video.creator.owner}`;
const profilePictureResult = await client.query(profilePictureQuery);
video.creator.profile_picture = profilePictureResult.rows[0].picture;
// GET CREATOR SUBSCRIBERS COUNT
const subscribersQuery = `SELECT COUNT(*) AS subscriber_count FROM subscriptions WHERE channel = ${video.creator.id}`;
const subscribersResult = await client.query(subscribersQuery);
video.creator.subscribers = subscribersResult.rows[0].subscriber_count;
// GET TAGS
const tagsQuery = `SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag WHERE vt.video = ${id}`;
const tagsResult = await client.query(tagsQuery);
video.tags = tagsResult.rows.map(tag => tag.name);
logger.write("successfully get video " + id, 200);
res.status(200).json(video);
}

4
backend/app/routes/media.routes.js

@ -1,11 +1,11 @@
import {Router} from 'express';
import {getProfilePicture, getThumbnail} from "../controllers/media.controller.js";
import {getProfilePicture, getThumbnail, getVideo} from "../controllers/media.controller.js";
const router = Router();
router.get("/profile/:file", getProfilePicture);
//router.get("/video/:file", getVideo);
router.get("/video/:file", getVideo);
router.get("/thumbnail/:file", getThumbnail);

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

@ -33,7 +33,7 @@ router.post("/", [videoUpload.single('file'), addLogger, isTokenValid, VideoCrea
router.post("/thumbnail", [thumbnailUpload.single('file'), addLogger, isTokenValid, VideoThumbnail.video, Video.channel, validator, doChannelExistBody, isOwner, doVideoExists], uploadThumbnail )
// GET BY ID
router.get("/:id", [addLogger, isTokenValid, Video.id, validator, doVideoExistsParam], getById);
router.get("/:id", [addLogger, Video.id, validator, doVideoExistsParam], getById);
// GET BY CHANNEL
router.get("/channel/:id", [addLogger, isTokenValid, Channel.id, validator, doChannelExists], getByChannel);

296
backend/logs/access.log

@ -119,3 +119,299 @@
[2025-07-18 21:10:44.250] [undefined] POST(/login): Successfully logged in with status 200
[2025-07-18 21:24:17.066] [undefined] POST(/login): try to login with username 'sacha'
[2025-07-18 21:24:17.118] [undefined] POST(/login): Successfully logged in with status 200
[2025-07-19 15:14:40.732] [undefined] POST(/): try to upload video with status undefined
[2025-07-19 15:14:40.735] [undefined] POST(/): successfully uploaded video with status 200
[2025-07-19 15:14:54.976] [undefined] POST(/thumbnail): try to add thumbnail to video 3
[2025-07-19 15:14:54.979] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-07-19 15:43:29.882] [undefined] POST(/login): try to login with username 'astria'
[2025-07-19 15:43:29.931] [undefined] POST(/login): Successfully logged in with status 200
[2025-07-19 15:48:36.098] [undefined] GET(/:id): Invalid token with status 401
[2025-07-19 15:48:40.465] [undefined] GET(/:id): Invalid token with status 401
[2025-07-19 15:49:34.548] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:49:34.555] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:52:40.713] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:53:16.543] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:53:16.553] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:53:55.670] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:53:55.677] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:54:19.757] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:54:19.764] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:54:24.800] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:54:24.807] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.070] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.077] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.097] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.104] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.115] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.121] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.137] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.144] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.155] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.161] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.172] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.178] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.189] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.195] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.206] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.213] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.224] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.230] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.242] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.249] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.275] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.282] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.293] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.300] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.310] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.317] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.337] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.344] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.354] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.361] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.373] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.379] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.411] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.417] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.428] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.436] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.457] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.463] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.475] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.482] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.494] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.501] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.518] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.525] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.537] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.544] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.554] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.561] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.578] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.585] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.596] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.603] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.615] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.622] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.639] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.646] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.657] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.663] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.675] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.682] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.705] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.711] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.722] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.729] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.739] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.744] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.764] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.771] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.782] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.788] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.797] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.803] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.820] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.826] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.836] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.843] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.852] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.858] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.867] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.874] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.884] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.890] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.899] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.905] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.914] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.921] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:57:18.931] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:57:18.937] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:58:14.286] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:58:14.295] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 15:58:25.380] [undefined] GET(/:id): try to get video 2
[2025-07-19 15:58:25.387] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:00:23.446] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:00:23.456] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:03:22.242] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:03:22.251] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:04:14.786] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:04:14.796] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:04:23.626] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:04:23.634] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:05:35.520] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:05:35.527] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:06:05.466] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:06:05.472] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:06:56.551] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:06:56.557] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:07:18.833] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:07:18.841] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:08:57.061] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:08:57.068] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:10:51.224] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:10:51.231] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:11:03.522] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:11:03.530] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:11:16.054] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:11:16.060] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:11:49.282] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:11:49.288] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:12:06.493] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:12:06.499] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:12:20.606] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:12:20.613] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:12:49.479] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:12:49.485] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:13:01.725] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:13:01.732] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:14:41.215] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:14:41.222] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:15:19.018] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:15:19.026] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:15:32.287] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:15:32.294] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:15:44.463] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:15:44.471] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:16:38.876] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:16:38.882] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:17:34.301] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:17:34.308] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:17:46.051] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:17:46.058] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:18:47.128] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:18:47.135] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:19:09.271] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:19:09.277] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:19:35.974] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:19:35.981] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:20:18.884] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:20:18.891] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:20:50.151] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:20:50.157] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:21:07.517] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:21:07.527] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:21:14.602] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:21:14.610] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:21:21.513] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:21:21.520] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:21:44.213] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:21:44.219] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:22:48.209] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:22:48.215] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:22:54.655] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:22:54.662] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:24:54.112] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:24:54.119] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:24:58.700] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:24:58.712] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:26:34.718] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:26:34.725] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:26:48.648] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:26:48.656] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:27:34.841] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:27:34.848] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:27:43.336] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:27:43.343] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:31:05.829] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:31:05.841] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:32:29.981] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:32:29.989] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:32:46.293] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:32:46.302] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:33:28.372] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:33:28.381] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:34:43.065] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:34:43.073] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:35:35.922] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:35:35.929] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:36:08.537] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:36:08.545] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:37:31.020] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:37:31.027] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:37:42.940] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:37:42.947] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:38:06.260] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:38:06.268] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:38:31.413] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:38:31.420] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:38:41.363] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:38:41.370] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:39:32.126] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:39:32.133] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:39:51.345] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:39:51.353] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:40:24.085] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:40:24.092] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:43:25.763] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:43:25.769] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:43:48.531] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:43:48.538] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:44:02.412] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:44:02.421] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:44:03.840] [undefined] POST(/:id/subscribe): Invalid token with status 401
[2025-07-19 16:44:51.156] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:44:51.163] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:44:52.817] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 2
[2025-07-19 16:46:18.109] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:46:18.116] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:46:19.593] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 2
[2025-07-19 16:46:19.600] [undefined] POST(/:id/subscribe): Successfully subscribed to channel with status 200
[2025-07-19 16:47:01.900] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:47:01.908] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:47:17.720] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:47:17.727] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:48:15.720] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:48:15.728] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:48:32.578] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:48:32.586] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:50:13.518] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:50:13.526] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 16:50:16.236] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:50:16.244] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:50:28.980] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:50:28.988] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:51:06.208] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:51:06.216] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:51:17.351] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:51:17.359] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:51:24.016] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:51:24.024] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:51:45.734] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:51:45.742] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:52:19.574] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:52:19.582] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:52:36.473] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:52:36.480] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:53:22.564] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:53:22.572] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:53:55.967] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:53:55.975] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:54:24.653] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:54:24.661] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:54:30.471] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:54:30.479] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:54:36.682] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:54:36.690] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:54:50.298] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:54:50.307] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:55:01.575] [undefined] POST(/login): try to login with username 'astria'
[2025-07-19 16:55:01.627] [undefined] POST(/login): Successfully logged in with status 200
[2025-07-19 16:55:04.312] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:55:04.319] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:55:12.078] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:56:21.497] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:56:21.509] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:56:53.274] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:56:53.282] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:56:54.853] [undefined] GET(/:id/like): try to toggle like on video 3
[2025-07-19 16:56:54.861] [undefined] GET(/:id/like): no likes found adding likes for video 3 with status 200
[2025-07-19 16:56:56.605] [undefined] GET(/:id): try to get video 3
[2025-07-19 16:56:56.613] [undefined] GET(/:id): successfully get video 3 with status 200
[2025-07-19 16:57:32.256] [undefined] GET(/:id): try to get video 2
[2025-07-19 16:57:32.263] [undefined] GET(/:id): successfully get video 2 with status 200
[2025-07-19 17:02:09.826] [undefined] POST(/thumbnail): try to add thumbnail to video 3
[2025-07-19 17:02:09.828] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-07-19 17:02:23.623] [undefined] POST(/thumbnail): try to add thumbnail to video 2
[2025-07-19 17:02:23.626] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-07-19 17:02:29.733] [undefined] GET(/:id): try to get video 3
[2025-07-19 17:02:29.739] [undefined] GET(/:id): successfully get video 3 with status 200

1
frontend/Dockerfile

@ -6,3 +6,4 @@ RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev"]

15
frontend/rebuild.sh

@ -0,0 +1,15 @@
#!/bin/bash
echo "🧹 Nettoyage des caches..."
rm -rf dist
rm -rf node_modules/.vite
rm -rf node_modules/.cache
echo "📦 Build de l'application..."
npm run build
echo "🔄 Redémarrage du container frontend..."
cd ../
sudo docker-compose restart frontend
echo "✅ Build terminé et container redémarré !"

3
frontend/src/assets/svg/play.svg

@ -0,0 +1,3 @@
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.5 14.4019C30.5 15.5566 30.5 18.4434 28.5 19.5981L4.5 33.4545C2.5 34.6092 2.14642e-06 33.1658 2.24736e-06 30.8564L3.45873e-06 3.14359C3.55968e-06 0.834193 2.5 -0.609184 4.5 0.545517L28.5 14.4019Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

9
frontend/src/components/Recommendations.jsx

@ -2,9 +2,18 @@
export default function Recommendations({videos}) {
return (
<div>
<h2 className="text-3xl font-bold mb-4 text-white">Recommendations</h2>
<div>
<div className="flex flex-wrap">
{videos && videos.map((video, index) => (
<VideoCard key={video.id || index} video={video} />
))}
</div>
</div>
</div>
)

18
frontend/src/components/TopCreators.jsx

@ -0,0 +1,18 @@
export default function TopCreators({ creators }) {
return (
<div className="mt-10">
<h2 className="text-3xl font-bold mb-4 text-white">Top Creators</h2>
<div className="flex flex-wrap">
{creators && creators.map((creator, index) => (
<div key={creator.id || index} className="flex flex-col items-center w-1/4 p-4">
<img src={creator.avatar} alt={creator.name} className="w-full h-auto rounded-lg" />
<h3 className="text-xl font-bold mt-2">{creator.name}</h3>
<span className="text-sm text-gray-500">{creator.subscribers} subscribers</span>
</div>
))}
</div>
</div>
);
}

17
frontend/src/components/TrendingVideos.jsx

@ -0,0 +1,17 @@
import VideoCard from "./VideoCard.jsx";
export default function TrendingVideos({ videos }) {
return (
<div className="mt-10">
<h2 className="text-3xl font-bold mb-4 text-white">Tendances</h2>
<div className="flex flex-wrap gap-11">
{videos && videos.map((video, index) => (
<VideoCard video={video} key={index} />
))}
</div>
</div>
);
}

27
frontend/src/components/VideoCard.jsx

@ -1,12 +1,27 @@
import { useNavigate } from 'react-router-dom';
export default function VideoCard({ video }) {
const navigation = useNavigate();
const handleClick = () => {
navigation(`/video/${video.id}`, {
state: { video }
})
}
return (
<div className="flex flex-col items-center w-1/4 p-4">
<img src={video.thumbnail} alt={video.title} className="w-full h-auto rounded-lg" />
<h2 className="text-xl font-bold mt-2">{video.title}</h2>
<p className="text-gray-600">{video.description}</p>
<span className="text-sm text-gray-500">{video.views} views</span>
<div className="flex flex-col glassmorphism w-445/1920 p-6 cursor-pointer" onClick={handleClick} >
<div className="aspect-video rounded-sm overflow-hidden">
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover"
/>
</div>
<h2 className="text-2xl font-medium font-inter mt-3 text-white">{video.title}</h2>
<div className="text-sm text-gray-400 mt-1 flex items-center">
<img src={video.creator.profilePicture} alt={video.title} className="w-12 aspect-square rounded-full object-cover" />
<span className="ml-2">{video.creator.name}</span>
<span className="ml-3.5">{video.views} vues</span>
</div>
</div>
);
}

21
frontend/src/index.css

@ -10,6 +10,27 @@
src: url('assets/fonts/Montserrat/Montserrat.ttf') format('truetype');
}
.glassmorphism {
border-radius: 15px;
border: 2px 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-rounded-sm {
border-radius: 5px;
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-rounded-md {
border-radius: 10px;
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);
}
@theme {
/* Fonts */
--font-inter: 'Inter', sans-serif;

74
frontend/src/pages/Home.jsx

@ -3,6 +3,8 @@ import HeroImage from '../assets/img/hero.png';
import Recommendations from "../components/Recommendations.jsx";
import {useState, useEffect} from "react";
import { useAuth } from '../contexts/AuthContext';
import TopCreators from "../components/TopCreators.jsx";
import TrendingVideos from "../components/TrendingVideos.jsx";
export default function Home() {
const { isAuthenticated, user } = useAuth();
@ -23,6 +25,17 @@ export default function Home() {
} finally {
setLoading(false);
}
try {
const trendingResponse = await fetch('/api/recommendations/trending');
const trendingData = await trendingResponse.json();
setTrendingVideos(trendingData);
} catch (error) {
console.error('Error fetching trending videos:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
@ -31,35 +44,46 @@ export default function Home() {
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} />
<div className="flex flex-col items-center w-screen pt-[304px]">
<img src={HeroImage} alt="" className="w-1046/1920" />
<main className="px-36">
{/* Hero section */}
<div className="flex flex-col items-center w-full pt-[304px]">
<img src={HeroImage} alt="" className="w-1046/1920" />
{isAuthenticated ? (
<h1 className="font-montserrat text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Bienvenue {user?.username} !
</h1>
) : (
<>
{isAuthenticated ? (
<h1 className="font-montserrat text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Regarder des vidéos comme jamais auparavant
Bienvenue {user?.username} !
</h1>
<div className="flex justify-center gap-28 -translate-y-[100px] mt-10">
<button className="bg-white text-black font-montserrat p-3 rounded-sm text-2xl font-bold">
<a href="/login">
<p>Se connecter</p>
</a>
</button>
<button className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-bold cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</button>
</div>
</>
)}
</div>
) : (
<>
<h1 className="font-montserrat text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Regarder des vidéos comme jamais auparavant
</h1>
<div className="flex justify-center gap-28 -translate-y-[100px] mt-10">
<button className="bg-white text-black font-montserrat p-3 rounded-sm text-2xl font-bold">
<a href="/login">
<p>Se connecter</p>
</a>
</button>
<button className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-bold cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</button>
</div>
</>
)}
</div>
{/* Recommendations section */}
<Recommendations videos={recommendations} />
{/* Top Creators section */}
<TopCreators/>
<Recommendations/>
{/* Trending Videos section */}
<TrendingVideos videos={trendingVideos} />
</main>
</div>
);

263
frontend/src/pages/Video.jsx

@ -0,0 +1,263 @@
import {useNavigate, useParams} from "react-router-dom";
import {useEffect, useState, useRef} from "react";
import Navbar from "../components/Navbar.jsx";
import { useAuth } from "../contexts/AuthContext.jsx";
export default function Video() {
// This component can be used to display a video player or video-related content.
const {id} = useParams();
const { user, isAuthenticated } = useAuth();
const videoRef = useRef(null);
const controllerRef = useRef(null);
const navigation = useNavigate();
const [video, setVideo] = useState(null);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [progress, setProgress] = useState(0);
const [showControls, setShowControls] = useState(false);
useEffect(() => {
const fetchVideo = async () => {
try {
const response = await fetch(`/api/videos/${id}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const videoData = await response.json();
setVideo(videoData);
} catch (error) {
console.error('Error fetching video:', error);
}
}
fetchVideo();
}, [id]);
const handlePlayPause = () => {
if (videoRef.current) {
if (videoRef.current.paused) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
}
};
const handleTimeUpdate = () => {
if (videoRef.current) {
const current = videoRef.current.currentTime;
const total = videoRef.current.duration;
setCurrentTime(current);
setDuration(total);
setProgress((current / total) * 100);
}
};
const handleLoadedMetadata = () => {
if (videoRef.current) {
setDuration(videoRef.current.duration);
}
};
const handleTimeBarClick = (event) => {
if (videoRef.current) {
const timeBar = event.currentTarget;
const clickX = event.nativeEvent.offsetX;
const totalWidth = timeBar.offsetWidth;
const percentage = clickX / totalWidth;
const newTime = percentage * duration;
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
setProgress(percentage * 100);
}
};
const formatTime = (time) => {
if (isNaN(time)) return "0:00";
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const handlePlaying = () => {
if (videoRef.current) {
console.log(`Video is playing at ${videoRef.current.currentTime} seconds`);
}
}
const handleMouseEnter = () => {
setShowControls(true);
};
const handleMouseLeave = () => {
setShowControls(false);
};
const handleSubscribe = async () => {
if (!isAuthenticated) {
navigation('/login');
return;
}
try {
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!token) {
navigation('/login');
return;
}
const response = await fetch(`/api/channels/${video.creator.id}/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
userId: user.id
})
});
if (!response.ok) {
throw new Error('Failed to subscribe');
}
const data = await response.json();
console.log('Subscription successful:', data);
// You could update the UI here to show subscription status
alert('Successfully subscribed!');
} catch (error) {
console.error('Error subscribing:', error);
alert('Failed to subscribe. Please try again.');
}
};
const handleLike = async () => {
if (!isAuthenticated) {
navigation('/login');
return;
}
try {
// Retrieve the token from localStorage
const token = localStorage.getItem('token');
if (!token) {
navigation('/login');
return;
}
const response = await fetch(`/api/videos/${id}/like`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to like video');
}
} catch (error) {
console.error('Error liking video:', error);
}
}
return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} />
<main className="px-36 w-full">
{video ? (
<div className="w-1280/1920 pt-[118px]">
<div
className="relative w-full aspect-video mx-auto mt-10 rounded-lg overflow-hidden"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<video
ref={videoRef}
onPlay={handlePlaying}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
>
<source src={`${video.file}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Video controls */}
<div
className={`absolute bottom-4 left-4 right-4 glassmorphism-rounded-md p-4 flex items-center transition-opacity duration-300 ${
showControls ? 'opacity-100' : 'opacity-0'
}`}
ref={controllerRef}
>
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg" onClick={handlePlayPause}>
<path d="M28.5 14.4019C30.5 15.5566 30.5 18.4434 28.5 19.5981L4.5 33.4545C2.5 34.6092 2.14642e-06 33.1658 2.24736e-06 30.8564L3.45873e-06 3.14359C3.55968e-06 0.834193 2.5 -0.609184 4.5 0.545517L28.5 14.4019Z" fill="white"/>
</svg>
<div className="flex-1 mx-4">
{/* Time display */}
<div className="flex justify-between items-center text-white text-sm mt-2">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
{/* Time bar */}
<div
className="w-full h-2 bg-gray-300 rounded-full mt-2 cursor-pointer hover:h-3 transition-all duration-200"
onClick={handleTimeBarClick}
>
<div
className="h-full bg-white rounded-full transition-all duration-100"
style={{ width: `${progress}%` }}
></div>
</div>
</div>
</div>
</div>
<h1 className="mt-3 font-montserrat font-bold text-2xl text-white">{video.title}</h1>
<div className="flex items-center mt-4">
<img
src={video.creator?.profile_picture || "https://placehold.co/48"}
alt={video.creator?.name || "Creator"}
className="w-12 h-12 rounded-full object-cover mr-3"
/>
<div>
<p className="text-white font-bold font-montserrat">{video.creator?.name}</p>
<p className="text-gray-300 text-sm">{video.creator?.subscribers || 0} abonnés</p>
</div>
<button className="ml-14 bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md cursor-pointer" onClick={handleSubscribe} >
s'abonner
</button>
<button className="ml-4 cursor-pointer" onClick={handleLike}>
<svg width="32" height="32" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 31.5H7.5V12H6C5.20435 12 4.44129 12.3161 3.87868 12.8787C3.31607 13.4413 3 14.2044 3 15V28.5C3 29.2956 3.31607 30.0587 3.87868 30.6213C4.44129 31.1839 5.20435 31.5 6 31.5ZM30 12H19.5L21.183 6.948C21.3332 6.49712 21.3741 6.01702 21.3024 5.54723C21.2306 5.07745 21.0483 4.63142 20.7705 4.24589C20.4926 3.86036 20.1271 3.54636 19.7041 3.32975C19.2811 3.11314 18.8127 3.00012 18.3375 3H18L10.5 11.157V31.5H27L32.868 18.606L33 18V15C33 14.2044 32.6839 13.4413 32.1213 12.8787C31.5587 12.3161 30.7956 12 30 12Z" fill="white"/>
</svg>
</button>
<p className="font-montserrat text-white ml-2" >{video.likes}</p>
</div>
</div>
): (
<p>Loading</p>
)}
</main>
</div>
);
}

5
frontend/src/routes/routes.jsx

@ -1,6 +1,7 @@
import Home from '../pages/Home.jsx'
import Login from '../pages/Login.jsx'
import Register from '../pages/Register.jsx'
import Video from '../pages/Video.jsx'
import ProtectedRoute from '../components/ProtectedRoute.jsx'
const routes = [
@ -21,6 +22,10 @@ const routes = [
</ProtectedRoute>
)
},
{
path: "/video/:id",
element: <Video />
},
]
export default routes;

2
frontend/tailwind.config.js

@ -0,0 +1,2 @@

23
frontend/vite.config.js

@ -5,7 +5,28 @@ import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
react(),
tailwindcss(),
],
server: {
host: true,
port: 5173,
watch: {
usePolling: true
}
},
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
output: {
manualChunks: undefined
}
}
},
css: {
postcss: {
plugins: [],
},
},
})

8
nginx/default.conf

@ -15,15 +15,17 @@ server {
proxy_buffering off;
}
# Static assets caching (only for frontend assets, not API)
# Static assets - NO CACHING for development
location ~* ^/(?!api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
# Handle React Router - all other routes should serve index.html
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri $uri/ /index.html;
}
}
Loading…
Cancel
Save