Repository: HuXn-WebDev/MERN-Movies-App Branch: main Commit: 16ea9e7ec9ee Files: 67 Total size: 88.3 KB Directory structure: gitextract_b6iznmx0/ ├── .gitignore ├── README.md ├── backend/ │ ├── config/ │ │ └── db.js │ ├── controllers/ │ │ ├── genreController.js │ │ ├── movieController.js │ │ └── userController.js │ ├── index.js │ ├── middlewares/ │ │ ├── asyncHandler.js │ │ ├── authMiddleware.js │ │ └── checkId.js │ ├── models/ │ │ ├── Genre.js │ │ ├── Movie.js │ │ └── User.js │ ├── routes/ │ │ ├── genreRoutes.js │ │ ├── moviesRoutes.js │ │ ├── uploadRoutes.js │ │ └── userRoutes.js │ └── utils/ │ └── createToken.js ├── frontend/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src/ │ │ ├── App.jsx │ │ ├── component/ │ │ │ ├── GenreForm.jsx │ │ │ ├── Loader.jsx │ │ │ ├── Modal.jsx │ │ │ └── SliderUtil.jsx │ │ ├── index.css │ │ ├── main.jsx │ │ ├── pages/ │ │ │ ├── Admin/ │ │ │ │ ├── AdminMoviesList.jsx │ │ │ │ ├── AdminRoute.jsx │ │ │ │ ├── AllComments.jsx │ │ │ │ ├── CreateMovie.jsx │ │ │ │ ├── Dashboard/ │ │ │ │ │ ├── AdminDashboard.jsx │ │ │ │ │ ├── Main/ │ │ │ │ │ │ ├── Main.jsx │ │ │ │ │ │ ├── PrimaryCard.jsx │ │ │ │ │ │ ├── RealTimeCard.jsx │ │ │ │ │ │ ├── SecondaryCard.jsx │ │ │ │ │ │ └── VideoCard.jsx │ │ │ │ │ └── Sidebar/ │ │ │ │ │ └── Sidebar.jsx │ │ │ │ ├── GenreList.jsx │ │ │ │ └── UpdateMovie.jsx │ │ │ ├── Auth/ │ │ │ │ ├── Login.jsx │ │ │ │ ├── Navigation.jsx │ │ │ │ ├── PrivateRoute.jsx │ │ │ │ └── Register.jsx │ │ │ ├── Home.jsx │ │ │ ├── Movies/ │ │ │ │ ├── AllMovies.jsx │ │ │ │ ├── Header.jsx │ │ │ │ ├── MovieCard.jsx │ │ │ │ ├── MovieDetails.jsx │ │ │ │ ├── MovieTabs.jsx │ │ │ │ └── MoviesContainerPage.jsx │ │ │ └── User/ │ │ │ └── Profile.jsx │ │ └── redux/ │ │ ├── api/ │ │ │ ├── apiSlice.js │ │ │ ├── genre.js │ │ │ ├── movies.js │ │ │ └── users.js │ │ ├── constants.js │ │ ├── features/ │ │ │ ├── auth/ │ │ │ │ └── authSlice.js │ │ │ └── movies/ │ │ │ └── moviesSlice.js │ │ └── store.js │ ├── tailwind.config.js │ └── vite.config.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /node_modules ================================================ FILE: backend/config/db.js ================================================ import mongoose from "mongoose"; const connectDB = async () => { try { await mongoose.connect(process.env.MONGO_URI); console.log(`Successfully connected to MongoDB 👍`); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }; export default connectDB; ================================================ FILE: backend/controllers/genreController.js ================================================ import Genre from "../models/Genre.js"; import asyncHandler from "../middlewares/asyncHandler.js"; const createGenre = asyncHandler(async (req, res) => { try { const { name } = req.body; if (!name) { return res.json({ error: "Name is required" }); } const existingGenre = await Genre.findOne({ name }); if (existingGenre) { return res.json({ error: "Already exists" }); } const genre = await new Genre({ name }).save(); res.json(genre); } catch (error) { console.log(error); return res.status(400).json(error); } }); const updateGenre = asyncHandler(async (req, res) => { try { const { name } = req.body; const { id } = req.params; const genre = await Genre.findOne({ _id: id }); if (!genre) { return res.status(404).json({ error: "Genre not found" }); } genre.name = name; const updatedGenre = await genre.save(); res.json(updatedGenre); } catch (error) { console.error(error); res.status(500).json({ error: "Internal server error" }); } }); const removeGenre = asyncHandler(async (req, res) => { try { const { id } = req.params; const removed = await Genre.findByIdAndDelete(id); if (!removed) { return res.status(404).json({ error: "Genre not found" }); } res.json(removed); } catch (error) { console.error(error); res.status(500).json({ error: "Interval server error" }); } }); const listGenres = asyncHandler(async (req, res) => { try { const all = await Genre.find({}); res.json(all); } catch (error) { console.log(error); return res.status(400).json(error.message); } }); const readGenre = asyncHandler(async (req, res) => { try { const genre = await Genre.findOne({ _id: req.params.id }); res.json(genre); } catch (error) { console.log(error); return res.status(400).json(error.message); } }); export { createGenre, updateGenre, removeGenre, listGenres, readGenre }; ================================================ FILE: backend/controllers/movieController.js ================================================ import Movie from "../models/Movie.js"; const createMovie = async (req, res) => { try { const newMovie = new Movie(req.body); const savedMovie = await newMovie.save(); res.json(savedMovie); } catch (error) { res.status(500).json({ error: error.message }); } }; const getAllMovies = async (req, res) => { try { const movies = await Movie.find(); res.json(movies); } catch (error) { res.status(500).json({ error: error.message }); } }; const getSpecificMovie = async (req, res) => { try { const { id } = req.params; const specificMovie = await Movie.findById(id); if (!specificMovie) { return res.status(404).json({ message: "Movie not found" }); } res.json(specificMovie); } catch (error) { res.status(500).json({ error: error.message }); } }; const updateMovie = async (req, res) => { try { const { id } = req.params; const updatedMovie = await Movie.findByIdAndUpdate(id, req.body, { new: true, }); if (!updatedMovie) { return res.status(404).json({ message: "Movie not found" }); } res.json(updatedMovie); } catch (error) { res.status(500).json({ error: error.message }); } }; const movieReview = async (req, res) => { try { const { rating, comment } = req.body; const movie = await Movie.findById(req.params.id); if (movie) { const alreadyReviewed = movie.reviews.find( (r) => r.user.toString() === req.user._id.toString() ); if (alreadyReviewed) { res.status(400); throw new Error("Movie already reviewed"); } const review = { name: req.user.username, rating: Number(rating), comment, user: req.user._id, }; movie.reviews.push(review); movie.numReviews = movie.reviews.length; movie.rating = movie.reviews.reduce((acc, item) => item.rating + acc, 0) / movie.reviews.length; await movie.save(); res.status(201).json({ message: "Review Added" }); } else { res.status(404); throw new Error("Movie not found"); } } catch (error) { console.error(error); res.status(400).json(error.message); } }; const deleteMovie = async (req, res) => { try { const { id } = req.params; const deleteMovie = await Movie.findByIdAndDelete(id); if (!deleteMovie) { return res.status(404).json({ message: "Movie not found" }); } res.json({ message: "Movie Deleted Successfully" }); } catch (error) { res.status(500).json({ error: error.message }); } }; const deleteComment = async (req, res) => { try { const { movieId, reviewId } = req.body; const movie = await Movie.findById(movieId); if (!movie) { return res.status(404).json({ message: "Movie not found" }); } const reviewIndex = movie.reviews.findIndex( (r) => r._id.toString() === reviewId ); if (reviewIndex === -1) { return res.status(404).json({ message: "Comment not found" }); } movie.reviews.splice(reviewIndex, 1); movie.numReviews = movie.reviews.length; movie.rating = movie.reviews.length > 0 ? movie.reviews.reduce((acc, item) => item.rating + acc, 0) / movie.reviews.length : 0; await movie.save(); res.json({ message: "Comment Deleted Successfully" }); } catch (error) { console.error(error); res.status(500).json({ error: error.message }); } }; const getNewMovies = async (req, res) => { try { const newMovies = await Movie.find().sort({ createdAt: -1 }).limit(10); res.json(newMovies); } catch (error) { res.status(500).json({ error: error.message }); } }; const getTopMovies = async (req, res) => { try { const topRatedMovies = await Movie.find() .sort({ numReviews: -1 }) .limit(10); res.json(topRatedMovies); } catch (error) { res.status(500).json({ error: error.message }); } }; const getRandomMovies = async (req, res) => { try { const randomMovies = await Movie.aggregate([{ $sample: { size: 10 } }]); res.json(randomMovies); } catch (error) { res.status(500).json({ error: error.message }); } }; export { createMovie, getAllMovies, getSpecificMovie, updateMovie, movieReview, deleteMovie, deleteComment, getNewMovies, getTopMovies, getRandomMovies, }; ================================================ FILE: backend/controllers/userController.js ================================================ import User from "../models/User.js"; import bcrypt from "bcryptjs"; import asyncHandler from "../middlewares/asyncHandler.js"; import createToken from "../utils/createToken.js"; const createUser = asyncHandler(async (req, res) => { const { username, email, password } = req.body; if (!username || !email || !password) { throw new Error("Please fill all the fields"); } const userExists = await User.findOne({ email }); if (userExists) res.status(400).send("User already exists"); // Hash the user password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); const newUser = new User({ username, email, password: hashedPassword }); try { await newUser.save(); createToken(res, newUser._id); res.status(201).json({ _id: newUser._id, username: newUser.username, email: newUser.email, isAdmin: newUser.isAdmin, }); } catch (error) { res.status(400); throw new Error("Invalid user data"); } }); const loginUser = asyncHandler(async (req, res) => { const { email, password } = req.body; const existingUser = await User.findOne({ email }); if (existingUser) { const isPasswordValid = await bcrypt.compare( password, existingUser.password ); if (isPasswordValid) { createToken(res, existingUser._id); res.status(201).json({ _id: existingUser._id, username: existingUser.username, email: existingUser.email, isAdmin: existingUser.isAdmin, }); } else { res.status(401).json({ message: "Invalid Password" }); } } else { res.status(401).json({ message: "User not found" }); } }); const logoutCurrentUser = asyncHandler(async (req, res) => { res.cookie("jwt", "", { httpOnly: true, expires: new Date(0), }); res.status(200).json({ message: "Logged out successfully" }); }); const getAllUsers = asyncHandler(async (req, res) => { const users = await User.find({}); res.json(users); }); const getCurrentUserProfile = asyncHandler(async (req, res) => { const user = await User.findById(req.user._id); if (user) { res.json({ _id: user._id, username: user.username, email: user.email, }); } else { res.status(404); throw new Error("User not found."); } }); const updateCurrentUserProfile = asyncHandler(async (req, res) => { const user = await User.findById(req.user._id); if (user) { user.username = req.body.username || user.username; user.email = req.body.email || user.email; if (req.body.password) { const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(req.body.password, salt); user.password = hashedPassword; } const updatedUser = await user.save(); res.json({ _id: updatedUser._id, username: updatedUser.username, email: updatedUser.email, isAdmin: updatedUser.isAdmin, }); } else { res.status(404); throw new Error("User not found"); } }); export { createUser, loginUser, logoutCurrentUser, getAllUsers, getCurrentUserProfile, updateCurrentUserProfile, }; ================================================ FILE: backend/index.js ================================================ // Packages import express from "express"; import cookieParser from "cookie-parser"; import dotenv from "dotenv"; import path from "path"; // Files import connectDB from "./config/db.js"; import userRoutes from "./routes/userRoutes.js"; import genreRoutes from "./routes/genreRoutes.js"; import moviesRoutes from "./routes/moviesRoutes.js"; import uploadRoutes from "./routes/uploadRoutes.js"; // Configuration dotenv.config(); connectDB(); const app = express(); // middlewares app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); const PORT = process.env.PORT || 3000; // Routes app.use("/api/v1/users", userRoutes); app.use("/api/v1/genre", genreRoutes); app.use("/api/v1/movies", moviesRoutes); app.use("/api/v1/upload", uploadRoutes); const __dirname = path.resolve(); app.use("/uploads", express.static(path.join(__dirname + "/uploads"))); app.listen(PORT, () => console.log(`Server is running on port ${PORT}`)); ================================================ FILE: backend/middlewares/asyncHandler.js ================================================ const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch((error) => { res.status(500).json({ message: error.message }); }); }; export default asyncHandler; ================================================ FILE: backend/middlewares/authMiddleware.js ================================================ import jwt from "jsonwebtoken"; import User from "../models/User.js"; import asyncHandler from "./asyncHandler.js"; // Check if the user is authenticated or not const authenticate = asyncHandler(async (req, res, next) => { let token; // Read JWT from the 'jwt' cookie token = req.cookies.jwt; if (token) { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = await User.findById(decoded.userId).select("-password"); next(); } catch (error) { res.status(401); throw new Error("Not authorized, token failed."); } } else { res.status(401); throw new Error("Not authorized, no token"); } }); // Check if the user is admin or not const authorizeAdmin = (req, res, next) => { if (req.user && req.user.isAdmin) { next(); } else { res.status(401).send("Not authorized as an admin"); } }; export { authenticate, authorizeAdmin }; ================================================ FILE: backend/middlewares/checkId.js ================================================ import { isValidObjectId } from "mongoose"; function checkId(req, res, next) { if (!isValidObjectId(req.params.id)) { res.status(404); throw new Error(`Invalid Object Of: ${req.params.id}`); } next(); } export default checkId; ================================================ FILE: backend/models/Genre.js ================================================ import mongoose from "mongoose"; const genreSchema = new mongoose.Schema({ name: { type: String, trim: true, required: true, maxLength: 32, unique: true, }, }); export default mongoose.model("Genre", genreSchema); ================================================ FILE: backend/models/Movie.js ================================================ import mongoose from "mongoose"; const { ObjectId } = mongoose.Schema; const reviewSchema = mongoose.Schema( { name: { type: String, required: true }, rating: { type: Number, required: true }, comment: { type: String, required: true }, user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: "User", }, }, { timestamps: true } ); const movieSchema = new mongoose.Schema( { name: { type: String, required: true }, image: { type: String }, year: { type: Number, required: true }, genre: { type: ObjectId, ref: "Genre", required: true }, detail: { type: String, required: true }, cast: [{ type: String }], reviews: [reviewSchema], numReviews: { type: Number, required: true, default: 0 }, createdAt: { type: Date, default: Date.now }, }, { timestamps: true } ); const Movie = mongoose.model("Movie", movieSchema); export default Movie; ================================================ FILE: backend/models/User.js ================================================ import mongoose from "mongoose"; const userSchema = mongoose.Schema( { username: { type: String, required: true, }, email: { type: String, required: true, unique: true, }, password: { type: String, required: true, }, isAdmin: { type: Boolean, required: true, default: false, }, }, { timestamps: true } ); const User = mongoose.model("User", userSchema); export default User; ================================================ FILE: backend/routes/genreRoutes.js ================================================ import express from "express"; const router = express.Router(); // Controllers import { createGenre, updateGenre, removeGenre, listGenres, readGenre, } from "../controllers/genreController.js"; // Middlewares import { authenticate, authorizeAdmin } from "../middlewares/authMiddleware.js"; router.route("/").post(authenticate, authorizeAdmin, createGenre); router.route("/:id").put(authenticate, authorizeAdmin, updateGenre); router.route("/:id").delete(authenticate, authorizeAdmin, removeGenre); router.route("/genres").get(listGenres); router.route("/:id").get(readGenre); export default router; ================================================ FILE: backend/routes/moviesRoutes.js ================================================ import express from "express"; const router = express.Router(); // Controllers import { createMovie, getAllMovies, getSpecificMovie, updateMovie, movieReview, deleteMovie, deleteComment, getNewMovies, getTopMovies, getRandomMovies, } from "../controllers/movieController.js"; // Middlewares import { authenticate, authorizeAdmin } from "../middlewares/authMiddleware.js"; import checkId from "../middlewares/checkId.js"; // Public Routes router.get("/all-movies", getAllMovies); router.get("/specific-movie/:id", getSpecificMovie); router.get("/new-movies", getNewMovies); router.get("/top-movies", getTopMovies); router.get("/random-movies", getRandomMovies); // Restricted Routes router.post("/:id/reviews", authenticate, checkId, movieReview); // Admin router.post("/create-movie", authenticate, authorizeAdmin, createMovie); router.put("/update-movie/:id", authenticate, authorizeAdmin, updateMovie); router.delete("/delete-movie/:id", authenticate, authorizeAdmin, deleteMovie); router.delete("/delete-comment", authenticate, authorizeAdmin, deleteComment); export default router; ================================================ FILE: backend/routes/uploadRoutes.js ================================================ import path from "path"; import express from "express"; import multer from "multer"; const router = express.Router(); const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "uploads/"); }, filename: (req, file, cb) => { const extname = path.extname(file.originalname); cb(null, `${file.fieldname}-${Date.now()}${extname}`); }, }); const fileFilter = (req, file, cb) => { const filetypes = /jpe?g|png|webp/; const mimetypes = /image\/jpe?g|image\/png||image\/webp/; const extname = path.extname(file.originalname); const mimetype = file.mimetype; if (filetypes.test(extname) && mimetypes.test(mimetype)) { cb(null, true); } else { cb(new Error("Images only"), false); } }; const upload = multer({ storage, fileFilter }); const uploadSingleImage = upload.single("image"); router.post("/", (req, res) => { uploadSingleImage(req, res, (err) => { if (err) { res.status(400).send({ message: err.message }); } else if (req.file) { res.status(200).send({ message: "Image uploaded successfully", image: `/${req.file.path}`, }); } else { res.status(400).send({ message: "No image file provided" }); } }); }); export default router; ================================================ FILE: backend/routes/userRoutes.js ================================================ import express from "express"; // controllers import { createUser, loginUser, logoutCurrentUser, getAllUsers, getCurrentUserProfile, updateCurrentUserProfile, } from "../controllers/userController.js"; // middlewares import { authenticate, authorizeAdmin } from "../middlewares/authMiddleware.js"; const router = express.Router(); router .route("/") .post(createUser) .get(authenticate, authorizeAdmin, getAllUsers); router.post("/auth", loginUser); router.post("/logout", logoutCurrentUser); router .route("/profile") .get(authenticate, getCurrentUserProfile) .put(authenticate, updateCurrentUserProfile); export default router; ================================================ FILE: backend/utils/createToken.js ================================================ import jwt from "jsonwebtoken"; const generateToken = (res, userId) => { const token = jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: "30d", }); // Set JWT as an HTTP-Only Cookie res.cookie("jwt", token, { httpOnly: true, secure: process.env.NODE_ENV !== "development", sameSite: "strict", maxAge: 30 * 24 * 60 * 60 * 1000, }); return token; }; export default generateToken; ================================================ FILE: frontend/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, settings: { react: { version: '18.2' } }, plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ================================================ FILE: frontend/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: frontend/README.md ================================================ # React + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ================================================ FILE: frontend/index.html ================================================ Vite + React
================================================ FILE: frontend/package.json ================================================ { "name": "frontend", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@reduxjs/toolkit": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-redux": "^9.1.0", "react-router": "^6.21.3", "react-router-dom": "^6.21.3", "react-slick": "^0.30.1", "react-toastify": "^10.0.4", "slick-carousel": "^1.8.1" }, "devDependencies": { "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.17", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.33", "tailwindcss": "^3.4.1", "vite": "^5.0.8" } } ================================================ FILE: frontend/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: frontend/src/App.jsx ================================================ import { Outlet } from "react-router-dom"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import Navigation from "./pages/Auth/Navigation"; const App = () => { return ( <>
); }; export default App; ================================================ FILE: frontend/src/component/GenreForm.jsx ================================================ const GenreForm = ({ value, setValue, handleSubmit, buttonText = "Submit", handleDelete, }) => { return (
setValue(e.target.value)} />
{handleDelete && ( )}
); }; export default GenreForm; ================================================ FILE: frontend/src/component/Loader.jsx ================================================ const Loader = () => { return (
); }; export default Loader; ================================================ FILE: frontend/src/component/Modal.jsx ================================================ const Modal = ({ isOpen, onClose, children }) => { return ( <> {isOpen && (
{children}
)} ); }; export default Modal; ================================================ FILE: frontend/src/component/SliderUtil.jsx ================================================ import Slider from "react-slick"; import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import MovieCard from "../pages/Movies/MovieCard"; const SliderUtil = ({ data }) => { const settings = { dots: true, infinite: true, speed: 500, slidesToShow: 4, slidesToScroll: 2, }; return ( {data?.map((movie) => ( ))} ); }; export default SliderUtil; ================================================ FILE: frontend/src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; body { background: #0f0f10; color: #fff; } input, textarea { color: #000; } select option { color: black; } ================================================ FILE: frontend/src/main.jsx ================================================ import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; import store from "./redux/store.js"; import { Provider } from "react-redux"; import { Route, RouterProvider, createRoutesFromElements } from "react-router"; import { createBrowserRouter } from "react-router-dom"; // Auth import AdminRoute from "./pages/Admin/AdminRoute.jsx"; import GenreList from "./pages/Admin/GenreList.jsx"; // Restricted import Login from "./pages/Auth/Login.jsx"; import Register from "./pages/Auth/Register.jsx"; import PrivateRoute from "./pages/Auth/PrivateRoute.jsx"; import Home from "./pages/Home.jsx"; import Profile from "./pages/User/Profile.jsx"; import AdminMoviesList from "./pages/Admin/AdminMoviesList.jsx"; import UpdateMovie from "./pages/Admin/UpdateMovie.jsx"; import CreateMovie from "./pages/Admin/CreateMovie.jsx"; import AllMovies from "./pages/Movies/AllMovies.jsx"; import MovieDetails from "./pages/Movies/MovieDetails.jsx"; import AllComments from "./pages/Admin/AllComments.jsx"; import AdminDashboard from "./pages/Admin/Dashboard/AdminDashboard.jsx"; const router = createBrowserRouter( createRoutesFromElements( }> } /> } /> } /> } /> } /> }> } /> }> } /> } /> } /> } /> } /> } /> ) ); ReactDOM.createRoot(document.getElementById("root")).render( ); ================================================ FILE: frontend/src/pages/Admin/AdminMoviesList.jsx ================================================ import { Link } from "react-router-dom"; import { useGetAllMoviesQuery } from "../../redux/api/movies"; const AdminMoviesList = () => { const { data: movies } = useGetAllMoviesQuery(); return (
All Movies ({movies?.length})
{movies?.map((movie) => (
{movie.name}
{movie.name}

{movie.detail}

Update Movie
))}
); }; export default AdminMoviesList; ================================================ FILE: frontend/src/pages/Admin/AdminRoute.jsx ================================================ import { Navigate, Outlet } from "react-router-dom"; import { useSelector } from "react-redux"; const AdminRoute = () => { const { userInfo } = useSelector((state) => state.auth); return userInfo && userInfo.isAdmin ? ( ) : ( ); }; export default AdminRoute; ================================================ FILE: frontend/src/pages/Admin/AllComments.jsx ================================================ import { useDeleteCommentMutation, useGetAllMoviesQuery, } from "../../redux/api/movies"; import { toast } from "react-toastify"; const AllComments = () => { const { data: movie, refetch } = useGetAllMoviesQuery(); const [deleteComment] = useDeleteCommentMutation(); const handleDeleteComment = async (movieId, reviewId) => { try { await deleteComment({ movieId, reviewId }); toast.success("Comment Deleted"); refetch(); } catch (error) { console.error("Error deleting comment: ", error); } }; return (
{movie?.map((m) => (
{m?.reviews.map((review) => (
{review.name}

{review.createdAt.substring(0, 10)}

{review.comment}

))}
))}
); }; export default AllComments; ================================================ FILE: frontend/src/pages/Admin/CreateMovie.jsx ================================================ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useCreateMovieMutation, useUploadImageMutation, } from "../../redux/api/movies"; import { useFetchGenresQuery } from "../../redux/api/genre"; import { toast } from "react-toastify"; const CreateMovie = () => { const navigate = useNavigate(); const [movieData, setMovieData] = useState({ name: "", year: 0, detail: "", cast: [], rating: 0, image: null, genre: "", }); const [selectedImage, setSelectedImage] = useState(null); const [ createMovie, { isLoading: isCreatingMovie, error: createMovieErrorDetail }, ] = useCreateMovieMutation(); const [ uploadImage, { isLoading: isUploadingImage, error: uploadImageErrorDetails }, ] = useUploadImageMutation(); const { data: genres, isLoading: isLoadingGenres } = useFetchGenresQuery(); useEffect(() => { if (genres) { setMovieData((prevData) => ({ ...prevData, genre: genres[0]?._id || "", })); console.log(genres[0]?._id); } }, [genres]); const handleChange = (e) => { const { name, value } = e.target; if (name === "genre") { const selectedGenre = genres.find((genre) => genre.name === value); setMovieData((prevData) => ({ ...prevData, genre: selectedGenre ? selectedGenre._id : "", })); } else { setMovieData((prevData) => ({ ...prevData, [name]: value, })); } }; const handleImageChange = (e) => { const file = e.target.files[0]; setSelectedImage(file); }; const handleCreateMovie = async () => { try { if ( !movieData.name || !movieData.year || !movieData.detail || !movieData.cast || !selectedImage ) { toast.error("Please fill all required fields"); return; } let uploadedImagePath = null; if (selectedImage) { const formData = new FormData(); formData.append("image", selectedImage); const uploadImageResponse = await uploadImage(formData); if (uploadImageResponse.data) { uploadedImagePath = uploadImageResponse.data.image; } else { console.error("Failed to upload image: ", uploadImageErrorDetails); toast.error("Failed to upload image"); return; } await createMovie({ ...movieData, image: uploadedImagePath, }); navigate("/admin/movies-list"); setMovieData({ name: "", year: 0, detail: "", cast: [], ratings: 0, image: null, genre: "", }); toast.success("Movie Added To Database"); } } catch (error) { console.error("Failed to create movie: ", createMovieErrorDetail); toast.error(`Failed to create movie: ${createMovieErrorDetail?.message}`); } }; return (

Create Movie

); }; export default CreateMovie; ================================================ FILE: frontend/src/pages/Admin/Dashboard/AdminDashboard.jsx ================================================ import Main from "./Main/Main"; import Sidebar from "./Sidebar/Sidebar"; const AdminDashboard = () => { return ( <>
); }; export default AdminDashboard; ================================================ FILE: frontend/src/pages/Admin/Dashboard/Main/Main.jsx ================================================ import SecondaryCard from "./SecondaryCard"; import VideoCard from "./VideoCard"; import ReactTimeCard from "./RealTimeCard"; import { useGetTopMoviesQuery, useGetAllMoviesQuery, } from "../../../../redux/api/movies"; import { useGetUsersQuery } from "../../../../redux/api/users"; import RealTimeCard from "./RealTimeCard"; const Main = () => { const { data: topMovies } = useGetTopMoviesQuery(); const { data: visitors } = useGetUsersQuery(); const { data: allMovies } = useGetAllMoviesQuery(); const totalCommentsLength = allMovies?.map((m) => m.numReviews); const sumOfCommentsLength = totalCommentsLength?.reduce( (acc, length) => acc + length, 0 ); return (

Top Content

Comments

{topMovies?.map((movie) => ( ))}
); }; export default Main; ================================================ FILE: frontend/src/pages/Admin/Dashboard/Main/PrimaryCard.jsx ================================================ import { useGetUsersQuery } from "../../../../redux/api/users"; const PrimaryCard = () => { const { data: visitors } = useGetUsersQuery(); return (

Congratulations!

You have {visitors?.length} new users, watching your content.

); }; export default PrimaryCard; ================================================ FILE: frontend/src/pages/Admin/Dashboard/Main/RealTimeCard.jsx ================================================ import { useGetUsersQuery } from "../../../../redux/api/users"; import PrimaryCard from "./PrimaryCard"; const RealTimeCard = () => { const { data: visitors } = useGetUsersQuery(); return (

Realtime

Update Live

{visitors?.length}

Subscribe


); }; export default RealTimeCard; ================================================ FILE: frontend/src/pages/Admin/Dashboard/Main/SecondaryCard.jsx ================================================ const SecondaryCard = ({ pill, content, info, gradient }) => { return (
{pill}

{content}

{info}
); }; export default SecondaryCard; ================================================ FILE: frontend/src/pages/Admin/Dashboard/Main/VideoCard.jsx ================================================ const VideoCard = ({ image, title, date, comments }) => { return ( <>
Card Image

{title}

{date}

{comments}
); }; export default VideoCard; ================================================ FILE: frontend/src/pages/Admin/Dashboard/Sidebar/Sidebar.jsx ================================================ import { Link } from "react-router-dom"; const Sidebar = () => { return (
); }; export default Sidebar; ================================================ FILE: frontend/src/pages/Admin/GenreList.jsx ================================================ import { useState } from "react"; import { useCreateGenreMutation, useUpdateGenreMutation, useDeleteGenreMutation, useFetchGenresQuery, } from "../../redux/api/genre"; import { toast } from "react-toastify"; import GenreForm from "../../component/GenreForm"; import Modal from "../../component/Modal"; const GenreList = () => { const { data: genres, refetch } = useFetchGenresQuery(); const [name, setName] = useState(""); const [selectedGenre, setSelectedGenre] = useState(null); const [updatingName, setUpdatingName] = useState(""); const [modalVisible, setModalVisible] = useState(false); const [createGenre] = useCreateGenreMutation(); const [updateGenre] = useUpdateGenreMutation(); const [deleteGenre] = useDeleteGenreMutation(); const handleCreateGenre = async (e) => { e.preventDefault(); if (!name) { toast.error("Genre name is required"); return; } try { const result = await createGenre({ name }).unwrap(); if (result.error) { toast.error(result.error); } else { setName(""); toast.success(`${result.name} is created.`); refetch(); } } catch (error) { console.error(error); toast.error("Creating genre failed, try again."); } }; const handleUpdateGenre = async (e) => { e.preventDefault(); if (!updateGenre) { toast.error("Genre name is required"); return; } try { const result = await updateGenre({ id: selectedGenre._id, updateGenre: { name: updatingName, }, }).unwrap(); if (result.error) { toast.error(result.error); } else { toast.success(`${result.name} is updated`); refetch(); setSelectedGenre(null); setUpdatingName(""); setModalVisible(false); } } catch (error) { console.error(error); } }; const handleDeleteGenre = async () => { try { const result = await deleteGenre(selectedGenre._id).unwrap(); if (result.error) { toast.error(result.error); } else { toast.success(`${result.name} is deleted.`); refetch(); setSelectedGenre(null); setModalVisible(false); } } catch (error) { console.error(error); toast.error("Genre deletion failed. Tray again."); } }; return (

Manage Genres


{genres?.map((genre) => (
))}
setModalVisible(false)}> setUpdatingName(value)} handleSubmit={handleUpdateGenre} buttonText="Update" handleDelete={handleDeleteGenre} />
); }; export default GenreList; ================================================ FILE: frontend/src/pages/Admin/UpdateMovie.jsx ================================================ import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useGetSpecificMovieQuery, useUpdateMovieMutation, useUploadImageMutation, useDeleteMovieMutation, } from "../../redux/api/movies"; import { toast } from "react-toastify"; const UpdateMovie = () => { const { id } = useParams(); const navigate = useNavigate(); const [movieData, setMovieData] = useState({ name: "", year: 0, detail: "", cast: [], ratings: 0, image: null, }); const [selectedImage, setSelectedImage] = useState(null); const { data: initialMovieData } = useGetSpecificMovieQuery(id); useEffect(() => { if (initialMovieData) { setMovieData(initialMovieData); } }, [initialMovieData]); const [updateMovie, { isLoading: isUpdatingMovie }] = useUpdateMovieMutation(); const [ uploadImage, { isLoading: isUploadingImage, error: uploadImageErrorDetails }, ] = useUploadImageMutation(); const [deleteMovie] = useDeleteMovieMutation(); const handleChange = (e) => { const { name, value } = e.target; setMovieData((prevData) => ({ ...prevData, [name]: value, })); }; const handleImageChange = (e) => { const file = e.target.files[0]; setSelectedImage(file); }; const handleUpdateMovie = async () => { try { if ( !movieData.name || !movieData.year || !movieData.detail || !movieData.cast ) { toast.error("Please fill in all required fields"); return; } let uploadedImagePath = movieData.image; if (selectedImage) { const formData = new FormData(); formData.append("image", selectedImage); const uploadImageResponse = await uploadImage(formData); if (uploadImageResponse.data) { uploadedImagePath = uploadImageResponse.data.image; } else { console.error("Failed to upload image:", uploadImageErrorDetails); toast.error("Failed to upload image"); return; } } await updateMovie({ id: id, updatedMovie: { ...movieData, image: uploadedImagePath, }, }); navigate("/movies"); } catch (error) { console.error("Failed to update movie:", error); } }; const handleDeleteMovie = async () => { try { toast.success("Movie deleted successfully"); await deleteMovie(id); navigate("/movies"); } catch (error) { console.error("Failed to delete movie:", error); toast.error(`Failed to delete movie: ${error?.message}`); } }; return (

Update Movie

) : (

Please Sign In to write a review

)}
{movie?.reviews.length === 0 &&

No Reviews

}
{movie?.reviews.map((review) => (
{review.name}

{review.createdAt.substring(0, 10)}

{review.comment}

))}
); }; export default MovieTabs; ================================================ FILE: frontend/src/pages/Movies/MoviesContainerPage.jsx ================================================ import { useState } from "react"; import { useGetNewMoviesQuery, useGetTopMoviesQuery, useGetRandomMoviesQuery, } from "../../redux/api/movies"; import { useFetchGenresQuery } from "../../redux/api/genre"; import SliderUtil from "../../component/SliderUtil"; const MoviesContainerPage = () => { const { data } = useGetNewMoviesQuery(); const { data: topMovies } = useGetTopMoviesQuery(); const { data: genres } = useFetchGenresQuery(); const { data: randomMovies } = useGetRandomMoviesQuery(); const [selectedGenre, setSelectedGenre] = useState(null); const handleGenreClick = (genreId) => { setSelectedGenre(genreId); }; const filteredMovies = data?.filter( (movie) => selectedGenre === null || movie.genre === selectedGenre ); return (

Choose For You

Top Movies

Choose Movie

); }; export default MoviesContainerPage; ================================================ FILE: frontend/src/pages/User/Profile.jsx ================================================ import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { toast } from "react-toastify"; import Loader from "../../component/Loader"; import { useProfileMutation } from "../../redux/api/users"; import { setCredentials } from "../../redux/features/auth/authSlice"; const Profile = () => { const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const { userInfo } = useSelector((state) => state.auth); const [updateProfile, { isLoading: loadingUpdateProfile }] = useProfileMutation(); useEffect(() => { setUsername(userInfo.username); setEmail(userInfo.email); }, [userInfo.email, userInfo.username]); const dispatch = useDispatch(); const submitHandler = async (e) => { e.preventDefault(); if (password !== confirmPassword) { toast.error("Passwords do not match"); } else { try { const res = await updateProfile({ _id: userInfo._id, username, email, password, }).unwrap(); dispatch(setCredentials({ ...res })); toast.success("Profile updated successfully"); } catch (err) { toast.error(err?.data?.message || err.error); } } }; return (

Update Profile

setUsername(e.target.value)} />
setEmail(e.target.value)} />
setPassword(e.target.value)} />
setConfirmPassword(e.target.value)} />
{loadingUpdateProfile && }
); }; export default Profile; ================================================ FILE: frontend/src/redux/api/apiSlice.js ================================================ import { fetchBaseQuery, createApi } from "@reduxjs/toolkit/query/react"; import { BASE_URL } from "../constants"; const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL }); export const apiSlice = createApi({ baseQuery, endpoints: () => ({}), }); ================================================ FILE: frontend/src/redux/api/genre.js ================================================ import { apiSlice } from "./apiSlice"; import { GENRE_URL } from "../constants"; export const genreApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ createGenre: builder.mutation({ query: (newGenre) => ({ url: `${GENRE_URL}`, method: "POST", body: newGenre, }), }), updateGenre: builder.mutation({ query: ({ id, updateGenre }) => ({ url: `${GENRE_URL}/${id}`, method: "PUT", body: updateGenre, }), }), deleteGenre: builder.mutation({ query: (id) => ({ url: `${GENRE_URL}/${id}`, method: "DELETE", }), }), fetchGenres: builder.query({ query: () => `${GENRE_URL}/genres`, }), }), }); export const { useCreateGenreMutation, useUpdateGenreMutation, useDeleteGenreMutation, useFetchGenresQuery, } = genreApiSlice; ================================================ FILE: frontend/src/redux/api/movies.js ================================================ import { apiSlice } from "./apiSlice"; import { MOVIE_URL, UPLOAD_URL } from "../constants"; export const moviesApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ getAllMovies: builder.query({ query: () => `${MOVIE_URL}/all-movies`, }), createMovie: builder.mutation({ query: (newMovie) => ({ url: `${MOVIE_URL}/create-movie`, method: "POST", body: newMovie, }), }), updateMovie: builder.mutation({ query: ({ id, updatedMovie }) => ({ url: `${MOVIE_URL}/update-movie/${id}`, method: "PUT", body: updatedMovie, }), }), addMovieReview: builder.mutation({ query: ({ id, rating, comment }) => ({ url: `${MOVIE_URL}/${id}/reviews`, method: "POST", body: { rating, id, comment }, }), }), deleteComment: builder.mutation({ query: ({ movieId, reviewId }) => ({ url: `${MOVIE_URL}/delete-comment`, method: "DELETE", body: { movieId, reviewId }, }), }), deleteMovie: builder.mutation({ query: (id) => ({ url: `${MOVIE_URL}/delete-movie/${id}`, method: "DELETE", }), }), getSpecificMovie: builder.query({ query: (id) => `${MOVIE_URL}/specific-movie/${id}`, }), uploadImage: builder.mutation({ query: (formData) => ({ url: `${UPLOAD_URL}`, method: "POST", body: formData, }), }), getNewMovies: builder.query({ query: () => `${MOVIE_URL}/new-movies`, }), getTopMovies: builder.query({ query: () => `${MOVIE_URL}/top-movies`, }), getRandomMovies: builder.query({ query: () => `${MOVIE_URL}/random-movies`, }), }), }); export const { useGetAllMoviesQuery, useCreateMovieMutation, useUpdateMovieMutation, useAddMovieReviewMutation, useDeleteCommentMutation, useGetSpecificMovieQuery, useUploadImageMutation, useDeleteMovieMutation, // useGetNewMoviesQuery, useGetTopMoviesQuery, useGetRandomMoviesQuery, } = moviesApiSlice; ================================================ FILE: frontend/src/redux/api/users.js ================================================ import { apiSlice } from "./apiSlice"; import { USERS_URL } from "../constants"; export const userApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ login: builder.mutation({ query: (data) => ({ url: `${USERS_URL}/auth`, method: "POST", body: data, }), }), register: builder.mutation({ query: (data) => ({ url: `${USERS_URL}`, method: "POST", body: data, }), }), logout: builder.mutation({ query: () => ({ url: `${USERS_URL}/logout`, method: "POST", }), }), profile: builder.mutation({ query: (data) => ({ url: `${USERS_URL}/profile`, method: "PUT", body: data, }), }), getUsers: builder.query({ query: () => ({ url: USERS_URL, }), }), }), }); export const { useLoginMutation, useRegisterMutation, useLogoutMutation, useProfileMutation, useGetUsersQuery, } = userApiSlice; ================================================ FILE: frontend/src/redux/constants.js ================================================ export const BASE_URL = ""; export const USERS_URL = "/api/v1/users"; export const GENRE_URL = "/api/v1/genre"; export const MOVIE_URL = "/api/v1/movies"; export const UPLOAD_URL = "/api/v1/upload"; ================================================ FILE: frontend/src/redux/features/auth/authSlice.js ================================================ import { createSlice } from "@reduxjs/toolkit"; const initialState = { userInfo: localStorage.getItem("userInfo") ? JSON.parse(localStorage.getItem("userInfo")) : null, }; const authSlice = createSlice({ name: "auth", initialState, reducers: { setCredentials: (state, action) => { state.userInfo = action.payload; localStorage.setItem("userInfo", JSON.stringify(action.payload)); const expirationTime = new Date().getTime() + 30 * 24 * 60 * 60 * 1000; localStorage.setItem("expirationTime", expirationTime); }, logout: (state) => { state.userInfo = null; localStorage.clear(); }, }, }); export const { setCredentials, logout } = authSlice.actions; export default authSlice.reducer; ================================================ FILE: frontend/src/redux/features/movies/moviesSlice.js ================================================ import { createSlice } from "@reduxjs/toolkit"; const moviesSlice = createSlice({ name: "movies", initialState: { moviesFilter: { searchTerm: "", selectedGenre: "", selectedYear: "", selectedSort: [], }, filteredMovies: [], movieYears: [], uniqueYear: [], }, reducers: { setMoviesFilter: (state, action) => { state.moviesFilter = { ...state.moviesFilter, ...action.payload }; }, setFilteredMovies: (state, action) => { state.filteredMovies = action.payload; }, setMovieYears: (state, action) => { state.movieYears = action.payload; }, setUniqueYears: (state, action) => { state.uniqueYear = action.payload; }, }, }); export const { setMoviesFilter, setFilteredMovies, setMovieYears, setUniqueYears, } = moviesSlice.actions; export default moviesSlice.reducer; ================================================ FILE: frontend/src/redux/store.js ================================================ import { configureStore } from "@reduxjs/toolkit"; import { setupListeners } from "@reduxjs/toolkit/query/react"; import { apiSlice } from "./api/apiSlice"; import authReducer from "./features/auth/authSlice"; import moviesReducer from "../redux/features/movies/moviesSlice"; const store = configureStore({ reducer: { [apiSlice.reducerPath]: apiSlice.reducer, auth: authReducer, movies: moviesReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware), devTools: true, }); setupListeners(store.dispatch); export default store; ================================================ FILE: frontend/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; ================================================ FILE: frontend/vite.config.js ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { proxy: { "/api/": "http://localhost:3000", "/uploads/": "http://localhost:3000", }, }, }); ================================================ FILE: package.json ================================================ { "name": "my-movies", "version": "1.0.0", "description": "", "type": "module", "main": "index.js", "scripts": { "fullstack": "concurrently \"npm run backend\" \"npm run frontend\"", "backend": "nodemon backend/index.js", "frontend": "cd frontend && npm run dev" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "concurrently": "^8.2.2", "cookie-parser": "^1.4.6", "dotenv": "^16.4.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.1", "multer": "^1.4.5-lts.1", "nodemon": "^3.0.3" } }