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 (
);
};
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 && (
)}
>
);
};
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.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 (
);
};
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 (
{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 (
);
};
export default SecondaryCard;
================================================
FILE: frontend/src/pages/Admin/Dashboard/Main/VideoCard.jsx
================================================
const VideoCard = ({ image, title, date, comments }) => {
return (
<>
>
);
};
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 (
);
};
export default UpdateMovie;
================================================
FILE: frontend/src/pages/Auth/Login.jsx
================================================
import { useState, useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import Loader from "../../component/Loader";
import { setCredentials } from "../../redux/features/auth/authSlice";
import { useLoginMutation } from "../../redux/api/users";
import { toast } from "react-toastify";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const [login, { isLoading }] = useLoginMutation();
const { userInfo } = useSelector((state) => state.auth);
const { search } = useLocation();
const sp = new URLSearchParams(search);
const redirect = sp.get("redirect") || "/";
useEffect(() => {
if (userInfo) {
navigate(redirect);
}
}, [navigate, redirect, userInfo]);
const submitHandler = async (e) => {
e.preventDefault();
try {
const res = await login({ email, password }).unwrap();
dispatch(setCredentials({ ...res }));
navigate(redirect);
} catch (err) {
toast.error(err?.data?.message || err.error);
}
};
return (
);
};
export default Login;
================================================
FILE: frontend/src/pages/Auth/Navigation.jsx
================================================
import { useState } from "react";
import {
AiOutlineHome,
AiOutlineLogin,
AiOutlineUserAdd,
} from "react-icons/ai";
import { MdOutlineLocalMovies } from "react-icons/md";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useLogoutMutation } from "../../redux/api/users";
import { logout } from "../../redux/features/auth/authSlice";
const Navigation = () => {
const { userInfo } = useSelector((state) => state.auth);
const [dropdownOpen, setDropdownOpen] = useState(false);
const toggleDropdown = () => {
setDropdownOpen(!dropdownOpen);
};
const dispatch = useDispatch();
const navigate = useNavigate();
const [logoutApiCall] = useLogoutMutation();
const logoutHandler = async () => {
try {
await logoutApiCall().unwrap();
dispatch(logout());
navigate("/login");
} catch (error) {
console.error(error);
}
};
return (
{/* Section 1 */}
{/* Section 2 */}
{dropdownOpen && userInfo && (
{userInfo.isAdmin && (
<>
-
Dashboard
>
)}
-
Profile
-
)}
{!userInfo && (
)}
);
};
export default Navigation;
================================================
FILE: frontend/src/pages/Auth/PrivateRoute.jsx
================================================
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const PrivateRoute = () => {
const { userInfo } = useSelector((state) => state.auth);
return userInfo ? : ;
};
export default PrivateRoute;
================================================
FILE: frontend/src/pages/Auth/Register.jsx
================================================
import { useState, useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import Loader from "../../component/Loader";
import { setCredentials } from "../../redux/features/auth/authSlice";
import { useRegisterMutation } from "../../redux/api/users";
import { toast } from "react-toastify";
const Register = () => {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const [register, { isLoading }] = useRegisterMutation();
const { userInfo } = useSelector((state) => state.auth);
const { search } = useLocation();
const sp = new URLSearchParams(search);
const redirect = sp.get("redirect") || "/";
useEffect(() => {
if (userInfo) {
navigate(redirect);
}
}, [navigate, redirect, userInfo]);
const submitHandler = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
toast.error("Password do not match");
} else {
try {
const res = await register({ username, email, password }).unwrap();
dispatch(setCredentials({ ...res }));
navigate(redirect);
toast.success("User successfully registered.");
} catch (err) {
console.log(err);
toast.error(err.data.message);
}
}
};
return (
);
};
export default Register;
================================================
FILE: frontend/src/pages/Home.jsx
================================================
import Header from "./Movies/Header";
import MoviesContainerPage from "./Movies/MoviesContainerPage";
const Home = () => {
return (
<>
>
);
};
export default Home;
================================================
FILE: frontend/src/pages/Movies/AllMovies.jsx
================================================
import { useGetAllMoviesQuery } from "../../redux/api/movies";
import { useFetchGenresQuery } from "../../redux/api/genre";
import {
useGetNewMoviesQuery,
useGetTopMoviesQuery,
useGetRandomMoviesQuery,
} from "../../redux/api/movies";
import MovieCard from "./MovieCard";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import banner from "../../assets/banner.jpg";
import {
setMoviesFilter,
setFilteredMovies,
setMovieYears,
setUniqueYears,
} from "../../redux/features/movies/moviesSlice";
const AllMovies = () => {
const dispatch = useDispatch();
const { data } = useGetAllMoviesQuery();
const { data: genres } = useFetchGenresQuery();
const { data: newMovies } = useGetNewMoviesQuery();
const { data: topMovies } = useGetTopMoviesQuery();
const { data: randomMovies } = useGetRandomMoviesQuery();
const { moviesFilter, filteredMovies } = useSelector((state) => state.movies);
const movieYears = data?.map((movie) => movie.year);
const uniqueYears = Array.from(new Set(movieYears));
useEffect(() => {
dispatch(setFilteredMovies(data || []));
dispatch(setMovieYears(movieYears));
dispatch(setUniqueYears(uniqueYears));
}, [data, dispatch]);
const handleSearchChange = (e) => {
dispatch(setMoviesFilter({ searchTerm: e.target.value }));
const filteredMovies = data.filter((movie) =>
movie.name.toLowerCase().includes(e.target.value.toLowerCase())
);
dispatch(setFilteredMovies(filteredMovies));
};
const handleGenreClick = (genreId) => {
const filterByGenre = data.filter((movie) => movie.genre === genreId);
dispatch(setFilteredMovies(filterByGenre));
};
const handleYearChange = (year) => {
const filterByYear = data.filter((movie) => movie.year === +year);
dispatch(setFilteredMovies(filterByYear));
};
const handleSortChange = (sortOption) => {
switch (sortOption) {
case "new":
dispatch(setFilteredMovies(newMovies));
break;
case "top":
dispatch(setFilteredMovies(topMovies));
break;
case "random":
dispatch(setFilteredMovies(randomMovies));
break;
default:
dispatch(setFilteredMovies([]));
break;
}
};
return (
<>
The Movies Hub
Cinematic Odyssey: Unveiling the Magic of Movies
{filteredMovies?.map((movie) => (
))}
>
);
};
export default AllMovies;
================================================
FILE: frontend/src/pages/Movies/Header.jsx
================================================
import SliderUtil from "../../component/SliderUtil";
import { useGetNewMoviesQuery } from "../../redux/api/movies";
import { Link } from "react-router-dom";
const Header = () => {
const { data } = useGetNewMoviesQuery();
return (
);
};
export default Header;
================================================
FILE: frontend/src/pages/Movies/MovieCard.jsx
================================================
import { Link } from "react-router-dom";
const MovieCard = ({ movie }) => {
return (
{movie.name}
);
};
export default MovieCard;
================================================
FILE: frontend/src/pages/Movies/MovieDetails.jsx
================================================
import { useState } from "react";
import { useParams, Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { toast } from "react-toastify";
import {
useGetSpecificMovieQuery,
useAddMovieReviewMutation,
} from "../../redux/api/movies";
import MovieTabs from "./MovieTabs";
const MovieDetails = () => {
const { id: movieId } = useParams();
const [rating, setRating] = useState(0);
const [comment, setComment] = useState("");
const { data: movie, refetch } = useGetSpecificMovieQuery(movieId);
const { userInfo } = useSelector((state) => state.auth);
const [createReview, { isLoading: loadingMovieReview }] =
useAddMovieReviewMutation();
const submitHandler = async (e) => {
e.preventDefault();
try {
await createReview({
id: movieId,
rating,
comment,
}).unwrap();
refetch();
toast.success("Review created successfully");
} catch (error) {
toast.error(error.data || error.message);
}
};
return (
<>
Go Back
{/* Container One */}
{movie?.name}
{movie?.detail}
Releasing Date: {movie?.year}
{movie?.cast.map((c) => (
))}
>
);
};
export default MovieDetails;
================================================
FILE: frontend/src/pages/Movies/MovieTabs.jsx
================================================
import { Link } from "react-router-dom";
const MovieTabs = ({ userInfo, submitHandler, comment, setComment, movie }) => {
return (
{userInfo ? (
) : (
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 (
);
};
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"
}
}