diff --git a/backend/database/schema.sql b/backend/database/schema.sql index ee421b1..8a707af 100644 --- a/backend/database/schema.sql +++ b/backend/database/schema.sql @@ -7,6 +7,8 @@ CREATE TABLE `avatar_filename` VARCHAR(255) ); +-- ! Add a videoFilename column to the Film table to store the video file name in case there is no video URL and the video is stored locally + DROP TABLE IF EXISTS `Film`; CREATE TABLE @@ -17,7 +19,8 @@ CREATE TABLE `cover_url` VARCHAR(255) DEFAULT NULL, `cover_filename` VARCHAR(255) DEFAULT NULL, `title` VARCHAR(255) NOT NULL, - `videoUrl` VARCHAR(255) NOT NULL, + `videoUrl` VARCHAR(255) DEFAULT NULL, + `videoFilename` VARCHAR(255) DEFAULT NULL, `duration` INT NOT NULL, `year` VARCHAR(4) NOT NULL, `description` VARCHAR(700) NOT NULL, diff --git a/backend/public/assets/icons/circle-arrow-left-solid.svg b/backend/public/assets/icons/circle-arrow-left-solid.svg index 4d0bee1..c098c52 100644 --- a/backend/public/assets/icons/circle-arrow-left-solid.svg +++ b/backend/public/assets/icons/circle-arrow-left-solid.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/backend/public/assets/icons/xmark-solid.svg b/backend/public/assets/icons/xmark-solid.svg new file mode 100644 index 0000000..c33a8ac --- /dev/null +++ b/backend/public/assets/icons/xmark-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js index 48b2361..27d9775 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,3 +1,4 @@ +// const path = require("path"); // Load the express module to create a web application const express = require("express"); @@ -145,4 +146,8 @@ app.use(logErrors); /* ************************************************************************* */ +// app.get("*", (req, res) => { +// res.sendFile(path.join(__dirname, "/../public/index.html")); +// }); + module.exports = app; diff --git a/backend/src/controllers/authControllers.js b/backend/src/controllers/authControllers.js index 19a98ff..7f0de1f 100644 --- a/backend/src/controllers/authControllers.js +++ b/backend/src/controllers/authControllers.js @@ -16,7 +16,6 @@ const login = async (req, res, next) => { user.hashed_password, req.body.password ); - console.warn("verified =>", verified); if (verified) { // delete user.hashed_password; diff --git a/backend/src/controllers/categorieControllers.js b/backend/src/controllers/categorieControllers.js index 9875a0d..748eace 100644 --- a/backend/src/controllers/categorieControllers.js +++ b/backend/src/controllers/categorieControllers.js @@ -19,7 +19,6 @@ const count = async (request, response, next) => { try { // Fetch all items from the database const categories = await tables.Categorie.count(); - console.warn(categories); // Respond with the items in JSON format response.json(categories); diff --git a/backend/src/controllers/filmControllers.js b/backend/src/controllers/filmControllers.js index 4b4fd66..499afeb 100644 --- a/backend/src/controllers/filmControllers.js +++ b/backend/src/controllers/filmControllers.js @@ -1,5 +1,7 @@ const tables = require("../tables"); +// % How to retrieve the uploaded videos from the request object in the edit method? + const browse = async (req, res, next) => { try { const films = await tables.Film.readAll(); @@ -25,6 +27,19 @@ const read = async (req, res, next) => { const edit = async (req, res, next) => { const { id } = req.params; req.body.id = id; + + if (req.files.cover && req.files.cover[0]) { + req.body.cover_filename = req.files.cover[0].filename; + } + + if (req.files.miniature && req.files.miniature[0]) { + req.body.miniature_filename = req.files.miniature[0].filename; + } + + if (req.files.videoFile && req.files.videoFile[0]) { + req.body.videoFilename = req.files.videoFile[0].filename; + } + try { const result = await tables.Film.update(req.body); if (result) { @@ -39,13 +54,16 @@ const edit = async (req, res, next) => { }; const add = async (req, res, next) => { - if (req.body.images.length === 2) { - const miniature = req.body.images[0]; - const cover = req.body.images[1]; - req.body.miniature_filename = miniature; - req.body.cover_filename = cover; - } else { - res.status(403).send({ message: "Missing file" }); + if (req.files.cover && req.files.cover[0]) { + req.body.cover_filename = req.files.cover[0].filename; + } + + if (req.files.miniature && req.files.miniature[0]) { + req.body.miniature_filename = req.files.miniature[0].filename; + } + + if (req.files.videoFile && req.files.videoFile[0]) { + req.body.videoFilename = req.files.videoFile[0].filename; } const film = req.body; @@ -67,17 +85,6 @@ const add = async (req, res, next) => { categoriesIds, }); - // Utiliser Promise.all pour paralléliser les opérations asynchrones - // const response = await Promise.all( - // categories.map(async (category) => { - // // Appeler la fonction asynchrone pour la liaison catégorie-film - // await tables.categorie_par_film.create({ - // filmId: insertId, - // categorieId: category.id, - // }); - // }) - // ); - if (response) { res.status(200).json({ insertId }); } else { diff --git a/backend/src/controllers/userControllers.js b/backend/src/controllers/userControllers.js index 681b868..02f9936 100644 --- a/backend/src/controllers/userControllers.js +++ b/backend/src/controllers/userControllers.js @@ -108,15 +108,6 @@ const edit = async (req, res, next) => { res.status(404).json({ error: "User not found" }); } - console.warn("currentUser =>", currentUser); - console.warn("currentUser.hashed_password =>", currentUser.hashed_password); - - console.warn( - "currentPassword && newPassword =>", - current_password, - new_password - ); - // Ensure currentUser has a hashed_password and it's not empty if ( !currentUser.hashed_password || @@ -133,7 +124,7 @@ const edit = async (req, res, next) => { currentUser.hashed_password, current_password ); - console.warn("isPasswordCorrect =>", isPasswordCorrect); + if (!isPasswordCorrect) { return res.status(400).json({ error: "Incorrect current password" }); } diff --git a/backend/src/models/CategorieParFilmManager.js b/backend/src/models/CategorieParFilmManager.js index cfe0f46..5cfd5f0 100644 --- a/backend/src/models/CategorieParFilmManager.js +++ b/backend/src/models/CategorieParFilmManager.js @@ -20,8 +20,6 @@ class CategorieParFilmManager extends AbstractManager { arrDep.push(filmId, catId, unique_key); }); - // console.log("querySQL =>", querySQL); - // console.log("arrDep =>", arrDep); // Execute the SQL INSERT query to add a new categorieParFilm to the "categorieParFilm" table const result = await this.database.query(`${querySQL};`, arrDep); diff --git a/backend/src/models/FilmManager.js b/backend/src/models/FilmManager.js index 8b0bcb0..1a77bac 100644 --- a/backend/src/models/FilmManager.js +++ b/backend/src/models/FilmManager.js @@ -1,5 +1,7 @@ const AbstractManager = require("./AbstractManager"); +// § Add videoFilename field in each method of the FilmManager class. + class FilmManager extends AbstractManager { constructor() { super({ table: "Film" }); @@ -10,18 +12,20 @@ class FilmManager extends AbstractManager { cover_filename, title, videoUrl, + videoFilename, duration, year, description, isAvailable, }) { const [result] = await this.database.query( - `insert into ${this.table} (miniature_filename, cover_filename, title, videoUrl, duration, year, description, isAvailable) values (?, ?, ?, ?, ?, ?, ?, ?)`, + `insert into ${this.table} (miniature_filename, cover_filename, title, videoUrl, videoFilename, duration, year, description, isAvailable) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ miniature_filename, cover_filename, title, videoUrl, + videoFilename, duration, year, description, @@ -47,19 +51,23 @@ class FilmManager extends AbstractManager { async update({ id, miniature_filename, + cover_filename, title, videoUrl, + videoFilename, duration, year, description, IsAvailable, }) { const [result] = await this.database.query( - `update ${this.table} SET miniature_filename=?, title=?, videoUrl=?, duration=?, year=?, description=?, IsAvailable=? where id=?`, + `update ${this.table} SET miniature_filename = COALESCE(?, miniature_filename), cover_filename = COALESCE(?, cover_filename), title = COALESCE(?, title), videoUrl = COALESCE(?, videoUrl), videoFilename = COALESCE(?, videoFilename), duration = COALESCE(?, duration), year = COALESCE(?, year), description = COALESCE(?, description), IsAvailable = COALESCE(?, IsAvailable) where id=?`, [ miniature_filename, + cover_filename, title, videoUrl, + videoFilename, duration, year, description, diff --git a/backend/src/router.js b/backend/src/router.js index 5312acd..b330509 100644 --- a/backend/src/router.js +++ b/backend/src/router.js @@ -8,7 +8,7 @@ const router = express.Router(); // Import itemControllers module for handling item-related operations -const { uploadImages } = require("./services/multer"); +const { uploadImages, uploadImages2 } = require("./services/multer"); const { hashPassword, verifyToken } = require("./services/auth"); const userControllers = require("./controllers/userControllers"); @@ -22,6 +22,8 @@ const commentaireFilmControllers = require("./controllers/commentaireFilmControl const authControllers = require("./controllers/authControllers"); +// § I would to implement the upload of video files for adding or editing a film with the multer package. + // Route to get a list of items router.get("/users", userControllers.browse); router.get("/films", filmControllers.browse); @@ -65,7 +67,15 @@ router.get("/category/:id", categorieControllers.read); // Route to edit a specific item by ID router.put("/user/:id", userControllers.edit); router.put("/comments/:commentId", commentaireFilmControllers.updateComment); -router.put("/films/:id", filmControllers.edit); +router.put( + "/films/:id", + uploadImages2.fields([ + { name: "miniature", maxCount: 1 }, + { name: "cover", maxCount: 1 }, + { name: "videoFile", maxCount: 1 }, + ]), + filmControllers.edit +); router.put("/category/:id", categorieControllers.edit); // Route to add a new item @@ -74,7 +84,15 @@ router.post("/users", hashPassword, userControllers.add); router.post("/favorites/film", favoriFilmControllers.addMovieToFavorite); router.post("/watchlist/film", watchlistControllers.addMovieToWatchlist); router.post("/comments", commentaireFilmControllers.addComment); -router.post("/films", uploadImages.array("images", 2), filmControllers.add); +router.post( + "/films", + uploadImages.fields([ + { name: "miniature", maxCount: 1 }, + { name: "cover", maxCount: 1 }, + { name: "videoFile", maxCount: 1 }, + ]), + filmControllers.add +); router.post( "/film/:filmId/category/:categoryId", categorieParFilmControllers.addFilmToCategory diff --git a/backend/src/services/multer.js b/backend/src/services/multer.js index 02414a7..dd50f08 100644 --- a/backend/src/services/multer.js +++ b/backend/src/services/multer.js @@ -2,6 +2,8 @@ const multer = require("multer"); const { v4 } = require("uuid"); +// ! Implement the upload of video files for adding or editing a film with the multer package. + const imagesStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "./public/assets/images"); @@ -12,20 +14,42 @@ const imagesStorage = multer.diskStorage({ // eslint-disable-next-line prefer-template const name = v4() + "." + extension; - if (req.body.images) { - req.body.images.push(name); - } else { - req.body.images = [name]; + if (req.body.cover) { + req.body.cover = name; + } + + if (req.body.miniature) { + req.body.miniature = name; + } + cb(null, name); + }, +}); + +const imagesStorage2 = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, "./public/assets/images"); + }, + filename: (req, file, cb) => { + const extArray = file.mimetype.split("/"); + const extension = extArray[extArray.length - 1]; + // eslint-disable-next-line prefer-template + const name = v4() + "." + extension; + + if (req.body.cover) { + req.body.cover = name; + } + + if (req.body.miniature) { + req.body.miniature = name; } cb(null, name); }, - // limits: { - // fieldSize: 1024 * 5, - // }, }); const uploadImages = multer({ storage: imagesStorage }); +const uploadImages2 = multer({ storage: imagesStorage2 }); module.exports = { uploadImages, + uploadImages2, }; diff --git a/frontend/src/components/CategoryDisplay.jsx b/frontend/src/components/CategoryDisplay.jsx index 1943ba3..383801e 100644 --- a/frontend/src/components/CategoryDisplay.jsx +++ b/frontend/src/components/CategoryDisplay.jsx @@ -77,11 +77,11 @@ function CategoryDisplay({ categorie, getCategories }) { toast.success("Category deleted"); getCategories(); } else { - toast.error("An error occurred"); + toast.error("Error deleting category"); } } catch (error) { console.error(error); - toast.error("An error occurred"); + toast.error("Error deleting category"); } finally { setIsDeleting(false); } diff --git a/frontend/src/components/FreeMovie.jsx b/frontend/src/components/FreeMovie.jsx index d5fb3be..cc364c8 100644 --- a/frontend/src/components/FreeMovie.jsx +++ b/frontend/src/components/FreeMovie.jsx @@ -288,7 +288,7 @@ FreeMovie.propTypes = { movie: PropTypes.shape({ id: PropTypes.number.isRequired, cover_filename: PropTypes.string, - cover_url: PropTypes.string.isRequired, + cover_url: PropTypes.string, title: PropTypes.string.isRequired, year: PropTypes.string.isRequired, duration: PropTypes.number.isRequired, diff --git a/frontend/src/components/MovieDescription.jsx b/frontend/src/components/MovieDescription.jsx index d22253b..d556a9a 100644 --- a/frontend/src/components/MovieDescription.jsx +++ b/frontend/src/components/MovieDescription.jsx @@ -22,7 +22,7 @@ MovieDescription.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, cover_filename: PropTypes.string, - cover_url: PropTypes.string.isRequired, + cover_url: PropTypes.string, year: PropTypes.string.isRequired, duration: PropTypes.number.isRequired, IsAvailable: PropTypes.number.isRequired, diff --git a/frontend/src/pages/AddVideos.jsx b/frontend/src/pages/AddVideos.jsx index e95753d..2c5beae 100644 --- a/frontend/src/pages/AddVideos.jsx +++ b/frontend/src/pages/AddVideos.jsx @@ -4,6 +4,8 @@ import { useNavigate, useLocation } from "react-router-dom"; import toast from "react-hot-toast"; import { useMovies } from "../contexts/MovieContext"; +// § The user should be able to decide if he wanna upload a video or use a youtube link to add a video. + function AddVideos() { const location = useLocation(); const [file, setFile] = useState(undefined); @@ -13,6 +15,7 @@ function AddVideos() { const [description, setDescription] = useState(""); const [title, setTitle] = useState(""); const [videoUrl, setVideoUrl] = useState(""); + const [videoFilename, setVideoFilename] = useState(""); const [year, setYear] = useState(""); const [duration, setDuration] = useState(""); const [isAvailable, setIsAvailable] = useState(false); @@ -65,7 +68,7 @@ function AddVideos() { duration || isAvailable) === (undefined || "" || false) ) { - toast.error("champ manquant"); + toast.error("Missing fields"); } else if (isAvailable === "utilisateur") { setIsAvailable(true); } else if (isAvailable === "visiteur") { @@ -82,8 +85,9 @@ function AddVideos() { formData.append("year", year); formData.append("isAvailable", isAvailable); formData.append("duration", duration); - formData.append("images", file); - formData.append("images", cover); + formData.append("miniature", file); + formData.append("cover", cover); + formData.append("videoFile", videoFilename); await axios .post(`${import.meta.env.VITE_BACKEND_URL}/api/films`, formData, { @@ -147,14 +151,43 @@ function AddVideos() {
-
- setVideoUrl(e.target.value)} - placeholder="lien de la video" - /> +
+
+ setVideoUrl(e.target.value)} + placeholder="video url" + /> +
+

+ Or +

+
+
+

Add a video file

+
+ setVideoFilename(e.target.files[0])} + type="file" + accept="video/*" + /> +
diff --git a/frontend/src/pages/EditSection.jsx b/frontend/src/pages/EditSection.jsx index ef07917..92bf7e3 100644 --- a/frontend/src/pages/EditSection.jsx +++ b/frontend/src/pages/EditSection.jsx @@ -83,7 +83,7 @@ function EditSection() { }) .catch((error) => { console.error(error); - toast.error("An error occurred"); + toast.error("Error updating category name"); }); } @@ -127,7 +127,7 @@ function EditSection() { } } catch (error) { console.error(error); - toast.error("An error occurred"); + toast.error("Error saving changes"); } finally { setIsSaving(false); } diff --git a/frontend/src/pages/EditVideo.jsx b/frontend/src/pages/EditVideo.jsx index f8072cf..6a1db0b 100644 --- a/frontend/src/pages/EditVideo.jsx +++ b/frontend/src/pages/EditVideo.jsx @@ -2,24 +2,33 @@ import { useParams, useNavigate, useLocation } from "react-router-dom"; import { useState, useEffect } from "react"; import toast from "react-hot-toast"; import axios from "axios"; +import { useMovies } from "../contexts/MovieContext"; + +// § When uploading a video, I would like to see a preview of the video being a snapshot of the video. How can I do that? function EditVideo() { const { movieId } = useParams(); + const { movies } = useMovies(); + const movie = movies.find((m) => m.id === parseInt(movieId, 10)); const location = useLocation(); const [categories, setCategories] = useState([]); const [categorieVideo, setCategorieVideo] = useState([]); const navigate = useNavigate(); const [video, setVideo] = useState({ - miniature_url: "", title: "", videoUrl: "", + videoFilename: "", duration: "", year: "", - miniature_filename: "", description: "", categorie: "", + cover: "", + miniature: "", + IsAvailable: movie?.IsAvailable, }); const [selectedFile, setSelectedFile] = useState(null); + const [selectedFile2, setSelectedFile2] = useState(null); + const [selectedFile3, setSelectedFile3] = useState(null); const handleDeleteCategorie = async (uniqueKey) => { try { @@ -27,7 +36,7 @@ function EditVideo() { `${import.meta.env.VITE_BACKEND_URL}/api/categoriesParFilm/${uniqueKey}` ); if (response.status === 200) { - toast.success("Success"); + toast.success("Success deleting category"); setCategorieVideo((prevCategorieVideo) => prevCategorieVideo.filter( (categorie) => categorie.unique_key !== uniqueKey @@ -35,7 +44,7 @@ function EditVideo() { ); } } catch (e) { - console.error("Error deleting", e); + console.error("Error deleting category", e); } }; @@ -68,7 +77,32 @@ function EditVideo() { setSelectedFile(file); setVideo((prevVideo) => ({ ...prevVideo, - miniature: URL.createObjectURL(file), + // cover_filename: URL.createObjectURL(file), + images: file, + })); + } + }; + + const handleFileChange2 = (e) => { + const file = e.target.files[0]; + if (file) { + setSelectedFile2(file); + setVideo((prevVideo) => ({ + ...prevVideo, + // miniature_filename: URL.createObjectURL(file), + images: file, + })); + } + }; + + const handleFileChange3 = (e) => { + const file = e.target.files[0]; + if (file) { + setSelectedFile3(file); + setVideo((prevVideo) => ({ + ...prevVideo, + // videoFilename: URL.createObjectURL(file), + images: file, })); } }; @@ -113,7 +147,7 @@ function EditVideo() { } ); if (response.status === 201) { - toast.success("Success"); + toast.success("Success adding category"); fetchCategorieVideo(); } } catch (e) { @@ -123,18 +157,35 @@ function EditVideo() { const handleEditClick = async () => { try { + const formData = new FormData(); + formData.append("title", video.title); + formData.append("videoUrl", video.videoUrl); + formData.append("duration", video.duration); + formData.append("year", video.year); + formData.append("description", video.description); + if (selectedFile) formData.append("cover", selectedFile); + if (selectedFile2) formData.append("miniature", selectedFile2); + if (selectedFile3) formData.append("videoFile", selectedFile3); + formData.append("IsAvailable", movie?.IsAvailable); + formData.append("categorie", video.categorie); const response = await axios.put( `${import.meta.env.VITE_BACKEND_URL}/api/films/${movieId}`, - video + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } ); if (response.status === 204) { - toast.success("Edited video"); + toast.success("Success editing video"); + if (selectedFile) URL.revokeObjectURL(selectedFile); + if (selectedFile2) URL.revokeObjectURL(selectedFile2); + if (selectedFile3) URL.revokeObjectURL(selectedFile3); navigate(`/movies/${movieId}`); - } else { - toast.error("A problem appeared"); } - } catch (e) { - console.error("Error for editing"); + } catch (err) { + console.error("Error editing", err); } }; @@ -144,7 +195,7 @@ function EditVideo() { `${import.meta.env.VITE_BACKEND_URL}/api/films/${movieId}` ); if (response.status === 200) { - toast.success("Success"); + toast.success("Success deleting video"); navigate("/"); } } catch (e) { @@ -156,19 +207,48 @@ function EditVideo() { if (selectedFile) { return URL.createObjectURL(selectedFile); } + if (video.cover_filename) { + return ( + video.cover_filename && + `${import.meta.env.VITE_BACKEND_URL}/assets/images/${ + video?.cover_filename + }` + ); + } + return video.cover_url; + }; + + const imageSrc2 = () => { + if (selectedFile2) { + return URL.createObjectURL(selectedFile2); + } if (video.miniature_filename) { return ( video.miniature_filename && `${import.meta.env.VITE_BACKEND_URL}/assets/images/${ - video.miniature_filename + video?.miniature_filename }` ); } - return video.miniature_url; + return video?.miniature_url; }; + useEffect(() => { + return () => { + if (selectedFile) { + URL.revokeObjectURL(selectedFile); + } + if (selectedFile2) { + URL.revokeObjectURL(selectedFile2); + } + if (selectedFile3) { + URL.revokeObjectURL(selectedFile3); + } + }; + }, [selectedFile, selectedFile2]); + return ( -
{ + e.preventDefault(); + handleEditClick(); + }} >

Edit video

-
- Miniature - -
-
+
+ Miniature +
+
+ cover +
+
+
+ +

+ Or +

+ +