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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
================================================
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 (
<>
<ToastContainer />
<Navigation />
<main className="py-3">
<Outlet />
</main>
</>
);
};
export default App;
================================================
FILE: frontend/src/component/GenreForm.jsx
================================================
const GenreForm = ({
value,
setValue,
handleSubmit,
buttonText = "Submit",
handleDelete,
}) => {
return (
<div className="p-3">
<form onSubmit={handleSubmit} className="space-y-3">
<input
type="text"
className="py-3 px-4 border rounded-lg w-[60rem]"
placeholder="Write genre name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<div className="flex justify-between">
<button className="bg-teal-500 text-white py-2 px-4 rounded-lg hover:bg-teal-600 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-opacity-50">
{buttonText}
</button>
{handleDelete && (
<button
onClick={handleDelete}
className="bg-red-500 text-white py-2 px-4 rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
>
Delete
</button>
)}
</div>
</form>
</div>
);
};
export default GenreForm;
================================================
FILE: frontend/src/component/Loader.jsx
================================================
const Loader = () => {
return (
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-teal-500 border-opacity-50"></div>
);
};
export default Loader;
================================================
FILE: frontend/src/component/Modal.jsx
================================================
const Modal = ({ isOpen, onClose, children }) => {
return (
<>
{isOpen && (
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-black opacity-50"></div>
<div className="absolute top-[40%] left-[20%] bg-white p-4 rounded-lg z-10 text-right">
<button
className="text-black font-semibold hover:text-gray-700 focus:outline-none mr-2"
onClick={onClose}
>
X
</button>
{children}
</div>
</div>
)}
</>
);
};
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 (
<Slider {...settings}>
{data?.map((movie) => (
<MovieCard key={movie._id} movie={movie} />
))}
</Slider>
);
};
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(
<Route path="/" element={<App />}>
<Route index={true} path="/" element={<Home />} />
<Route path="/movies" element={<AllMovies />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/movies/:id" element={<MovieDetails />} />
<Route path="" element={<PrivateRoute />}>
<Route path="/profile" element={<Profile />} />
</Route>
<Route path="" element={<AdminRoute />}>
<Route path="/admin/movies/genre" element={<GenreList />} />
<Route path="/admin/movies/create" element={<CreateMovie />} />
<Route path="/admin/movies-list" element={<AdminMoviesList />} />
<Route path="/admin/movies/update/:id" element={<UpdateMovie />} />
<Route path="/admin/movies/dashboard" element={<AdminDashboard />} />
<Route path="/admin/movies/comments" element={<AllComments />} />
</Route>
</Route>
)
);
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
);
================================================
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 (
<div className="container mx-[9rem]">
<div className="flex flex-col md:flex-row">
<div className="p-3">
<div className="ml-[2rem] text-xl font-bold h-12">
All Movies ({movies?.length})
</div>
<div className="flex flex-wrap justify-around items-center p-[2rem]">
{movies?.map((movie) => (
<Link
key={movie._id}
to={`/admin/movies/update/${movie._id}`}
className="block mb-4 overflow-hidden"
>
<div className="flex">
<div
key={movie._id}
className="max-w-sm m-[2rem] rounded overflow-hidden shadow-lg"
>
<img
src={movie.image}
alt={movie.name}
className="w-full h-48 object-cover"
/>
<div className="px-6 py-4 border border-gray-400">
<div className="font-bold text-xl mb-2">{movie.name}</div>
</div>
<p className="text-gray-700 text-base">{movie.detail}</p>
<div className="mt-[2rem] mb-[1rem]">
<Link
to={`/admin/movies/update/${movie._id}`}
className="bg-teal-500 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded"
>
Update Movie
</Link>
</div>
</div>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
);
};
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 ? (
<Outlet />
) : (
<Navigate to="/login" replace />
);
};
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 (
<div>
{movie?.map((m) => (
<section
key={m._id}
className="flex flex-col justify-center items-center"
>
{m?.reviews.map((review) => (
<div
key={review._id}
className="bg-[#1A1A1A] p-4 rounded-lg w-[50%] mt-[2rem]"
>
<div className="flex justify-between">
<strong className="text-[#B0B0B0]">{review.name}</strong>
<p className="text-[#B0B0B0]">
{review.createdAt.substring(0, 10)}
</p>
</div>
<p className="my-4">{review.comment}</p>
<button
className="text-red-500"
onClick={() => handleDeleteComment(m._id, review._id)}
>
Delete
</button>
</div>
))}
</section>
))}
</div>
);
};
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 (
<div className="container flex justify-center items-center mt-4">
<form>
<p className="text-green-200 w-[50rem] text-2xl mb-4">Create Movie</p>
<div className="mb-4">
<label className="block">
Name:
<input
type="text"
name="name"
value={movieData.name}
onChange={handleChange}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label className="block">
Year:
<input
type="number"
name="year"
value={movieData.year}
onChange={handleChange}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label className="block">
Detail:
<textarea
name="detail"
value={movieData.detail}
onChange={handleChange}
className="border px-2 py-1 w-full"
></textarea>
</label>
</div>
<div className="mb-4">
<label className="block">
Cast (comma-separated):
<input
type="text"
name="cast"
value={movieData.cast.join(", ")}
onChange={(e) =>
setMovieData({ ...movieData, cast: e.target.value.split(", ") })
}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label className="block">
Genre:
<select
name="genre"
value={movieData.genre}
onChange={handleChange}
className="border px-2 py-1 w-full"
>
{isLoadingGenres ? (
<option>Loading genres...</option>
) : (
genres.map((genre) => (
<option key={genre.id} value={genre.id}>
{genre.name}
</option>
))
)}
</select>
</label>
</div>
<div className="mb-4">
<label
style={
!selectedImage
? {
border: "1px solid #888",
borderRadius: "5px",
padding: "8px",
}
: {
border: "0",
borderRadius: "0",
padding: "0",
}
}
>
{!selectedImage && "Upload Image"}
<input
type="file"
accept="image/*"
onChange={handleImageChange}
style={{ display: !selectedImage ? "none" : "block" }}
/>
</label>
</div>
<button
type="button"
onClick={handleCreateMovie}
className="bg-teal-500 text-white px-4 py-2 rounded"
disabled={isCreatingMovie || isUploadingImage}
>
{isCreatingMovie || isUploadingImage ? "Creating..." : "Create Movie"}
</button>
</form>
</div>
);
};
export default CreateMovie;
================================================
FILE: frontend/src/pages/Admin/Dashboard/AdminDashboard.jsx
================================================
import Main from "./Main/Main";
import Sidebar from "./Sidebar/Sidebar";
const AdminDashboard = () => {
return (
<>
<Sidebar />
<Main />
</>
);
};
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 (
<div>
<section className="flex justify-around">
<div className="ml-[14rem] mt-10">
<div className="-translate-x-4 flex">
<SecondaryCard
pill="Users"
content={visitors?.length}
info="20.2k more then usual"
gradient="from-teal-500 to-lime-400"
/>
<SecondaryCard
pill="Comments"
content={sumOfCommentsLength}
info="742.8 more then usual"
gradient="from-[#CCC514] to-[#CDCB8E]"
/>
<SecondaryCard
pill="Movies"
content={allMovies?.length}
info="372+ more then usual"
gradient="from-green-500 to-lime-400"
/>
</div>
<div className="flex justify-between w-[90%] text-white mt-10 font-bold">
<p>Top Content</p>
<p>Comments</p>
</div>
{topMovies?.map((movie) => (
<VideoCard
key={movie._id}
image={movie.image}
title={movie.name}
date={movie.year}
comments={movie.numReviews}
/>
))}
</div>
<div>
<RealTimeCard />
</div>
</section>
</div>
);
};
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 (
<div className="w-[100%] h-[10%] bg-[#282828] text-white rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Congratulations!</h2>
<p>You have {visitors?.length} new users, watching your content.</p>
</div>
);
};
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 (
<div className="w-[30rem] mt-10 bg-[#282828] text-[#fff] rounded-lg shadow-lg p-4">
<h2 className="text-2xl font-bold mb-2">Realtime</h2>
<p className="text-gray-500 mb-4">Update Live</p>
<div className="border-t border-[#666] my-7"></div>
<h2 className="text-2xl font-bold mb-2">{visitors?.length}</h2>
<p className="text-gray-500 mb-2">Subscribe</p>
<hr />
<PrimaryCard />
</div>
);
};
export default RealTimeCard;
================================================
FILE: frontend/src/pages/Admin/Dashboard/Main/SecondaryCard.jsx
================================================
const SecondaryCard = ({ pill, content, info, gradient }) => {
return (
<div
className={`w-[15rem] h-[12rem] relative mt-10 bg-gradient-to-b ${gradient} rounded-lg shadow-lg ml-5`}
>
<div
className={`absolute -top-4 left-[5rem] border bg-gradient-to-b ${gradient} rounded-full py-2 px-5 text-sm text-gray-800 font-semibold`}
>
{pill}
</div>
<div className="flex items-center justify-center h-full">
<h2 className="text-5xl font-bold text-white">{content}</h2>
</div>
<div className="absolute bottom-4 left-12 text-sm text-white">{info}</div>
</div>
);
};
export default SecondaryCard;
================================================
FILE: frontend/src/pages/Admin/Dashboard/Main/VideoCard.jsx
================================================
const VideoCard = ({ image, title, date, comments }) => {
return (
<>
<div className="flex items-center w-[90%] mt-5">
<div>
<img src={image} alt="Card Image" className="h-[3rem]" />
</div>
<div className="ml-4">
<h2 className="text-lg text-white">{title}</h2>
<p className="text-gray-500 mb-3">{date}</p>
</div>
<div className="flex-grow mb-5 flex justify-end items-center">
<div className="text-white text-lg">{comments}</div>
</div>
</div>
</>
);
};
export default VideoCard;
================================================
FILE: frontend/src/pages/Admin/Dashboard/Sidebar/Sidebar.jsx
================================================
import { Link } from "react-router-dom";
const Sidebar = () => {
return (
<div className="-translate-y-10 flex h-screen fixed mt-10 border-r-2 border-[#242424]">
<aside className="text-white w-64 flex-shrink-0">
<ul className="py-4">
<li className="text-lg bg-gradient-to-b from-green-500 to-lime-400 rounded-full -translate-x-6">
<Link
to="/admin/movies/dashboard"
className="block p-2 ml-20 mb-10"
>
Dashboard
</Link>
</li>
<li className="text-lg -translate-x-6 hover:bg-gradient-to-b from-green-500 to-lime-400 rounded-full">
<Link to="/admin/movies/create" className="block p-2 ml-20 mb-10">
Create Movie
</Link>
</li>
<li className="text-lg -translate-x-6 hover:bg-gradient-to-b from-green-500 to-lime-400 rounded-full">
<Link to="/admin/movies/genre" className="block p-2 ml-20 mb-10">
Create Genre
</Link>
</li>
<li className="text-lg -translate-x-6 hover:bg-gradient-to-b from-green-500 to-lime-400 rounded-full">
<Link to="/admin/movies-list" className="block p-2 ml-20 mb-10">
Update Movie
</Link>
</li>
<li className="text-lg -translate-x-6 hover:bg-gradient-to-b from-green-500 to-lime-400 rounded-full">
<Link to="/admin/movies/comments" className="block p-2 ml-20 mb-10">
Comments
</Link>
</li>
</ul>
</aside>
</div>
);
};
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 (
<div className="ml-[10rem] flex flex-col md:flex-row">
<div className="md:w-3/4 p-3">
<h1 className="h-12">Manage Genres</h1>
<GenreForm
value={name}
setValue={setName}
handleSubmit={handleCreateGenre}
/>
<br />
<div className="flex flex-wrap">
{genres?.map((genre) => (
<div key={genre._id}>
<button
className="bg-white border border-teal-500 text-teal-500 py-2 px-4 rounded-lg m-3 hover:bg-teal-500 hover:text-white focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-opacity-50"
onClick={() => {
{
setModalVisible(true);
setSelectedGenre(genre);
setUpdatingName(genre.name);
}
}}
>
{genre.name}
</button>
</div>
))}
</div>
<Modal isOpen={modalVisible} onClose={() => setModalVisible(false)}>
<GenreForm
value={updatingName}
setValue={(value) => setUpdatingName(value)}
handleSubmit={handleUpdateGenre}
buttonText="Update"
handleDelete={handleDeleteGenre}
/>
</Modal>
</div>
</div>
);
};
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 (
<div className="container flex justify-center items-center mt-4">
<form>
<p className="text-green-200 w-[50rem] text-2xl mb-4">Update Movie</p>
<div className="mb-4">
<label className="block">
Name:
<input
type="text"
name="name"
value={movieData.name}
onChange={handleChange}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label className="block">
Year:
<input
type="number"
name="year"
value={movieData.year}
onChange={handleChange}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label className="block">
Detail:
<textarea
name="detail"
value={movieData.detail}
onChange={handleChange}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label className="block">
Cast (comma-separated):
<input
type="text"
name="cast"
value={movieData.cast.join(", ")}
onChange={(e) =>
setMovieData({ ...movieData, cast: e.target.value.split(", ") })
}
className="border px-2 py-1 w-full"
/>
</label>
</div>
<div className="mb-4">
<label
style={
!selectedImage
? {
border: "1px solid #888",
borderRadius: "5px",
padding: "8px",
}
: {
border: "0",
borderRadius: "0",
padding: "0",
}
}
>
{!selectedImage && "Upload Image"}
<input
type="file"
accept="image/*"
onChange={handleImageChange}
style={{ display: !selectedImage ? "none" : "block" }}
/>
</label>
</div>
<button
type="button"
onClick={handleUpdateMovie}
className="bg-teal-500 text-white px-4 py-2 rounded"
disabled={isUpdatingMovie || isUploadingImage}
>
{isUpdatingMovie || isUploadingImage ? "Updating..." : "Update Movie"}
</button>
<button
type="button"
onClick={handleDeleteMovie}
className="bg-red-500 text-white px-4 py-2 rounded ml-2"
disabled={isUpdatingMovie || isUploadingImage}
>
{isUpdatingMovie || isUploadingImage ? "Deleting..." : "Delete Movie"}
</button>
</form>
</div>
);
};
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 (
<div>
<section className="pl-[10rem] flex flex-wrap">
<div className="mr-[4rem] mt-[5rem]">
<h1 className="text-2xl font-semibold mb-4">Sign In</h1>
<form onSubmit={submitHandler} className="container w-[40rem]">
<div className="my-[2rem]">
<label
htmlFor="email"
className="block text-sm font-medium text-white"
>
Email Address
</label>
<input
type="email"
id="email"
className="mt-1 p-2 border rounded w-full"
placeholder="Enter Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label
htmlFor="password"
className="block text-sm font-medium text-white"
>
Password
</label>
<input
type="password"
id="password"
className="mt-1 p-2 border rounded w-full"
placeholder="Enter Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button
disabled={isLoading}
type="submit"
className="bg-teal-500 text-white px-4 py-2 rounded cursor-pointer my-[1rem]"
>
{isLoading ? "Signing In ..." : "Sign In"}
</button>
{isLoading && <Loader />}
</form>
<div className="mt-4">
<p className="text-white">
New Customer?{" "}
<Link
to={redirect ? `/register?redirect=${redirect}` : "/register"}
className="text-teal-500 hover:underline"
>
Register
</Link>
</p>
</div>
</div>
<img
src="https://images.unsplash.com/photo-1485095329183-d0797cdc5676?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt=""
className="h-[65rem] w-[55%] xl:block md:hidden sm:hidden rounded-lg"
/>
</section>
</div>
);
};
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 (
<div className="fixed bottom-10 left-[30rem] transform translate-x-1/2 translate-y-1/2 z-50 bg-[#0f0f0f] border w-[30%] px-[4rem] mb-[2rem] rounded">
<section className="flex justify-between items-center">
{/* Section 1 */}
<div className="flex justify-center items-center mb-[2rem]">
<Link
to="/"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<AiOutlineHome className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">Home</span>
</Link>
<Link
to="/movies"
className="flex items-center transition-transform transform hover:translate-x-2 ml-[1rem]"
>
<MdOutlineLocalMovies className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">SHOP</span>
</Link>
</div>
{/* Section 2 */}
<div className="relative">
<button
onClick={toggleDropdown}
className="text-gray-800 focus:outline-none"
>
{userInfo ? (
<span className="text-white">{userInfo.username}</span>
) : (
<></>
)}
{userInfo && (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-4 w-4 ml-1 ${
dropdownOpen ? "transform rotate-180" : ""
}`}
fill="none"
viewBox="0 0 24 24"
stroke="white"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d={dropdownOpen ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"}
/>
</svg>
)}
</button>
{dropdownOpen && userInfo && (
<ul
className={`absolute right-0 mt-2 mr-14 w-[10rem] space-y-2 bg-white text-gray-600 ${
!userInfo.isAdmin ? "-top-20" : "-top-24"
}`}
>
{userInfo.isAdmin && (
<>
<li>
<Link
to="/admin/movies/dashboard"
className="block px-4 py-2 hover:bg-gray-100"
>
Dashboard
</Link>
</li>
</>
)}
<li>
<Link
to="/profile"
className="block px-4 py-2 hover:bg-gray-100"
>
Profile
</Link>
</li>
<li>
<button
onClick={logoutHandler}
className="block w-full px-4 py-2 text-left hover:bg-gray-100"
>
Logout
</button>
</li>
</ul>
)}
{!userInfo && (
<ul className="flex">
<li>
<Link
to="/login"
className="flex items-center mt-5 transition-transform transform hover:translate-x-2 mb-[2rem]"
>
<AiOutlineLogin className="mr-2 mt-[4px]" size={26} />
<span className="hidden nav-item-name">LOGIN</span>
</Link>
</li>
<li>
<Link
to="/register"
className="flex items-center mt-5 transition-transform transform hover:translate-x-2 ml-[1rem]"
>
<AiOutlineUserAdd size={26} />
<span className="hidden nav-item-name">REGISTER</span>
</Link>
</li>
</ul>
)}
</div>
</section>
</div>
);
};
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 ? <Outlet /> : <Navigate to="/login" replace />;
};
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 (
<div className="pl-[10rem] flex flex-wrap">
<div className="mr-[4rem] mt-[5rem]">
<h1 className="text-2xl font-semibold mb-4">Register</h1>
<form onSubmit={submitHandler} className="container w-[40rem]">
<div className="my-[2rem]">
<label
htmlFor="name"
className="block text-sm font-medium text-white"
>
Name
</label>
<input
type="text"
id="name"
className="mt-1 p-2 border rounded w-full"
placeholder="Enter Name"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label
htmlFor="email"
className="block text-sm font-medium text-white"
>
Email Address
</label>
<input
type="email"
id="email"
className="mt-1 p-2 border rounded w-full"
placeholder="Enter Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label
htmlFor="password"
className="block text-sm font-medium text-white"
>
Password
</label>
<input
type="password"
id="password"
className="mt-1 p-2 border rounded w-full"
placeholder="Enter Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-white"
>
Confirm Password
</label>
<input
type="password"
id="confirmPassword"
className="mt-1 p-2 border rounded w-full"
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<button
disabled={isLoading}
type="submit"
className="bg-teal-500 text-white px-4 py-2 rounded cursor-pointer my-[1rem]"
>
{isLoading ? "Registering..." : "Register"}
</button>
{isLoading && <Loader />}
</form>
<div className="mt-4">
<p className="text-white">
Already have an account?{" "}
<Link
to={redirect ? `/login?redirect=${redirect}` : "/login"}
className="text-teal-500 hover:underline"
>
Login
</Link>
</p>
</div>
</div>
<img
src="https://images.unsplash.com/photo-1489599849927-2ee91cede3ba?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt=""
className="h-[65rem] w-[55%] xl:block md:hidden sm:hidden rounded-lg"
/>
</div>
);
};
export default Register;
================================================
FILE: frontend/src/pages/Home.jsx
================================================
import Header from "./Movies/Header";
import MoviesContainerPage from "./Movies/MoviesContainerPage";
const Home = () => {
return (
<>
<Header />
<section className="mt-[10rem]">
<MoviesContainerPage />
</section>
</>
);
};
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 (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 -translate-y-[5rem]">
<>
<section>
<div
className="relative h-[50rem] w-screen mb-10 flex items-center justify-center bg-cover"
style={{ backgroundImage: `url(${banner})` }}
>
<div className="absolute inset-0 bg-gradient-to-b from-gray-800 to-black opacity-60"></div>
<div className="relative z-10 text-center text-white mt-[10rem]">
<h1 className="text-8xl font-bold mb-4">The Movies Hub</h1>
<p className="text-2xl">
Cinematic Odyssey: Unveiling the Magic of Movies
</p>
</div>
<section className="absolute -bottom-[5rem]">
<input
type="text"
className="w-[100%] h-[5rem] border px-10 outline-none rounded"
placeholder="Search Movie"
value={moviesFilter.searchTerm}
onChange={handleSearchChange}
/>
<section className="sorts-container mt-[2rem] ml-[10rem] w-[30rem]">
<select
className="border p-2 rounded text-black"
value={moviesFilter.selectedGenre}
onChange={(e) => handleGenreClick(e.target.value)}
>
<option value="">Genres</option>
{genres?.map((genre) => (
<option key={genre._id} value={genre._id}>
{genre.name}
</option>
))}
</select>
<select
className="border p-2 rounded ml-4 text-black"
value={moviesFilter.selectedYear}
onChange={(e) => handleYearChange(e.target.value)}
>
<option value="">Year</option>
{uniqueYears.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
<select
className="border p-2 rounded ml-4 text-black"
value={moviesFilter.selectedSort}
onChange={(e) => handleSortChange(e.target.value)}
>
<option value="">Sort By</option>
<option value="new">New Movies</option>
<option value="top">Top Movies</option>
<option value="random">Random Movies</option>
</select>
</section>
</section>
</div>
<section className="mt-[10rem] w-screen flex justify-center items-center flex-wrap">
{filteredMovies?.map((movie) => (
<MovieCard key={movie._id} movie={movie} />
))}
</section>
</section>
</>
</div>
);
};
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 (
<div className="flex flex-col mt-[2rem] ml-[2rem] md:flex-row justify-between items-center md:items-start">
<nav className="w-full md:w-[10rem] ml-0 md:ml-2 mb-4 md:mb-0">
<Link
to="/"
className="transition duration-300 ease-in-out hover:bg-teal-200 block p-2 rounded mb-1 md:mb-2 text-lg"
>
Home
</Link>
<Link
to="/movies"
className="transition duration-300 ease-in-out hover:bg-teal-200 block p-2 rounded mb-1 md:mb-2 text-lg"
>
Browse Movies
</Link>
</nav>
<div className="w-full md:w-[80%] mr-0 md:mr-2">
<SliderUtil data={data} />
</div>
</div>
);
};
export default Header;
================================================
FILE: frontend/src/pages/Movies/MovieCard.jsx
================================================
import { Link } from "react-router-dom";
const MovieCard = ({ movie }) => {
return (
<div key={movie._id} className="relative group m-[2rem]">
<Link to={`/movies/${movie._id}`}>
<img
src={movie.image}
alt={movie.name}
className="w-[20rem] h-[20rem] rounded m-0 p-0 transition duration-300 ease-in-out transform group-hover:opacity-50"
/>
</Link>
<p className="absolute top-[85%] left-[2rem] right-0 bottom-0 opacity-0 transition duration-300 ease-in-out group-hover:opacity-100">
{movie.name}
</p>
</div>
);
};
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 (
<>
<div>
<Link
to="/"
className=" text-white font-semibold hover:underline ml-[20rem]"
>
Go Back
</Link>
</div>
<div className="mt-[2rem]">
<div className="flex justify-center items-center">
<img
src={movie?.image}
alt={movie?.name}
className="w-[70%] rounded"
/>
</div>
{/* Container One */}
<div className="container flex justify-between ml-[20rem] mt-[3rem]">
<section>
<h2 className="text-5xl my-4 font-extrabold">{movie?.name}</h2>
<p className="my-4 xl:w-[35rem] lg:w-[35rem] md:w-[30rem] text-[#B0B0B0]">
{movie?.detail}
</p>
</section>
<div className="mr-[5rem]">
<p className="text-2xl font-semibold">
Releasing Date: {movie?.year}
</p>
<div>
{movie?.cast.map((c) => (
<ul key={c._id}>
<li className="mt-[1rem]">{c}</li>
</ul>
))}
</div>
</div>
</div>
<div className="container ml-[20rem]">
<MovieTabs
loadingMovieReview={loadingMovieReview}
userInfo={userInfo}
submitHandler={submitHandler}
rating={rating}
setRating={setRating}
comment={comment}
setComment={setComment}
movie={movie}
/>
</div>
</div>
</>
);
};
export default MovieDetails;
================================================
FILE: frontend/src/pages/Movies/MovieTabs.jsx
================================================
import { Link } from "react-router-dom";
const MovieTabs = ({ userInfo, submitHandler, comment, setComment, movie }) => {
return (
<div>
<section>
{userInfo ? (
<form onSubmit={submitHandler}>
<div className="my-2">
<label htmlFor="comment" className="block text-xl mb-2">
Write Your Review
</label>
<textarea
id="comment"
rows="3"
required
value={comment}
onChange={(e) => setComment(e.target.value)}
className="p-2 border rounded-lg xl:w-[40rem] text-black"
></textarea>
</div>
<button
type="submit"
className="bg-teal-600 text-white py-2 px-4 rounded-lg"
>
Submit
</button>
</form>
) : (
<p>
Please <Link to="/login">Sign In</Link> to write a review
</p>
)}
</section>
<section className="mt-[3rem]">
<div>{movie?.reviews.length === 0 && <p>No Reviews</p>}</div>
<div>
{movie?.reviews.map((review) => (
<div
key={review._id}
className="bg-[#1A1A1A] p-4 rounded-lg w-[50%] mt-[2rem]"
>
<div className="flex justify-between">
<strong className="text-[#B0B0B0]">{review.name}</strong>
<p className="text-[#B0B0B0]">
{review.createdAt.substring(0, 10)}
</p>
</div>
<p className="my-4">{review.comment}</p>
</div>
))}
</div>
</section>
</div>
);
};
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 (
<div className="flex flex-col lg:flex-row lg:justify-between items-center">
<nav className=" ml-[4rem] flex flex-row xl:flex-col lg:flex-col md:flex-row sm:flex-row">
{genres?.map((g) => (
<button
key={g._id}
className={`transition duration-300 ease-in-out hover:bg-gray-200 block p-2 rounded mb-[1rem] text-lg ${
selectedGenre === g._id ? "bg-gray-200" : ""
}`}
onClick={() => handleGenreClick(g._id)}
>
{g.name}
</button>
))}
</nav>
<section className="flex flex-col justify-center items-center w-full lg:w-auto">
<div className="w-full lg:w-[100rem] mb-8 ">
<h1 className="mb-5">Choose For You</h1>
<SliderUtil data={randomMovies} />
</div>
<div className="w-full lg:w-[100rem] mb-8">
<h1 className="mb-5">Top Movies</h1>
<SliderUtil data={topMovies} />
</div>
<div className="w-full lg:w-[100rem] mb-8">
<h1 className="mb-5">Choose Movie</h1>
<SliderUtil data={filteredMovies} />
</div>
</section>
</div>
);
};
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 (
<div>
<div className="container mx-auto p-4 mt-[10rem]">
<div className="flex justify-center align-center md:flex md:space-x-4">
<div className="md:w-1/3">
<h2 className="text-2xl font-semibold mb-4">Update Profile</h2>
<form onSubmit={submitHandler}>
<div className="mb-4">
<label className="block text-white mb-2">Name</label>
<input
type="text"
placeholder="Enter name"
className="form-input p-4 rounded-sm w-full"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block text-white mb-2">Email Address</label>
<input
type="email"
placeholder="Enter email"
className="form-input p-4 rounded-sm w-full"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block text-white mb-2">Password</label>
<input
type="password"
placeholder="Enter password"
className="form-input p-4 rounded-sm w-full"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block text-white mb-2">
Confirm Password
</label>
<input
type="password"
placeholder="Confirm Password"
className="form-input p-4 rounded-sm w-full"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<div className="flex justify-between">
<button
type="submit"
className="bg-teal-500 w-screen mt-[2rem] font-bold text-white py-2 px-4 rounded hover:bg-teal-600"
>
Update
</button>
{loadingUpdateProfile && <Loader />}
</div>
</form>
</div>
</div>
</div>
</div>
);
};
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"
}
}
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
SYMBOL INDEX (7 symbols across 3 files)
FILE: backend/index.js
constant PORT (line 25) | const PORT = process.env.PORT || 3000;
FILE: backend/middlewares/checkId.js
function checkId (line 3) | function checkId(req, res, next) {
FILE: frontend/src/redux/constants.js
constant BASE_URL (line 1) | const BASE_URL = "";
constant USERS_URL (line 2) | const USERS_URL = "/api/v1/users";
constant GENRE_URL (line 3) | const GENRE_URL = "/api/v1/genre";
constant MOVIE_URL (line 4) | const MOVIE_URL = "/api/v1/movies";
constant UPLOAD_URL (line 5) | const UPLOAD_URL = "/api/v1/upload";
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
{
"path": ".gitignore",
"chars": 13,
"preview": "/node_modules"
},
{
"path": "backend/config/db.js",
"chars": 302,
"preview": "import mongoose from \"mongoose\";\n\nconst connectDB = async () => {\n try {\n await mongoose.connect(process.env.MONGO_U"
},
{
"path": "backend/controllers/genreController.js",
"chars": 1985,
"preview": "import Genre from \"../models/Genre.js\";\nimport asyncHandler from \"../middlewares/asyncHandler.js\";\n\nconst createGenre = "
},
{
"path": "backend/controllers/movieController.js",
"chars": 4380,
"preview": "import Movie from \"../models/Movie.js\";\n\nconst createMovie = async (req, res) => {\n try {\n const newMovie = new Movi"
},
{
"path": "backend/controllers/userController.js",
"chars": 3178,
"preview": "import User from \"../models/User.js\";\nimport bcrypt from \"bcryptjs\";\nimport asyncHandler from \"../middlewares/asyncHandl"
},
{
"path": "backend/index.js",
"chars": 974,
"preview": "// Packages\nimport express from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport dotenv from \"dotenv\";\nimport"
},
{
"path": "backend/middlewares/asyncHandler.js",
"chars": 201,
"preview": "const asyncHandler = (fn) => (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch((error) => {\n res.stat"
},
{
"path": "backend/middlewares/authMiddleware.js",
"chars": 925,
"preview": "import jwt from \"jsonwebtoken\";\nimport User from \"../models/User.js\";\nimport asyncHandler from \"./asyncHandler.js\";\n\n// "
},
{
"path": "backend/middlewares/checkId.js",
"chars": 243,
"preview": "import { isValidObjectId } from \"mongoose\";\n\nfunction checkId(req, res, next) {\n if (!isValidObjectId(req.params.id)) {"
},
{
"path": "backend/models/Genre.js",
"chars": 240,
"preview": "import mongoose from \"mongoose\";\n\nconst genreSchema = new mongoose.Schema({\n name: {\n type: String,\n trim: true,\n"
},
{
"path": "backend/models/Movie.js",
"chars": 933,
"preview": "import mongoose from \"mongoose\";\nconst { ObjectId } = mongoose.Schema;\n\nconst reviewSchema = mongoose.Schema(\n {\n na"
},
{
"path": "backend/models/User.js",
"chars": 478,
"preview": "import mongoose from \"mongoose\";\n\nconst userSchema = mongoose.Schema(\n {\n username: {\n type: String,\n requ"
},
{
"path": "backend/routes/genreRoutes.js",
"chars": 613,
"preview": "import express from \"express\";\nconst router = express.Router();\n\n// Controllers\nimport {\n createGenre,\n updateGenre,\n "
},
{
"path": "backend/routes/moviesRoutes.js",
"chars": 1110,
"preview": "import express from \"express\";\nconst router = express.Router();\n\n// Controllers\nimport {\n createMovie,\n getAllMovies,\n"
},
{
"path": "backend/routes/uploadRoutes.js",
"chars": 1258,
"preview": "import path from \"path\";\nimport express from \"express\";\nimport multer from \"multer\";\n\nconst router = express.Router();\n\n"
},
{
"path": "backend/routes/userRoutes.js",
"chars": 660,
"preview": "import express from \"express\";\n// controllers\nimport {\n createUser,\n loginUser,\n logoutCurrentUser,\n getAllUsers,\n "
},
{
"path": "backend/utils/createToken.js",
"chars": 422,
"preview": "import jwt from \"jsonwebtoken\";\n\nconst generateToken = (res, userId) => {\n const token = jwt.sign({ userId }, process.e"
},
{
"path": "frontend/.eslintrc.cjs",
"chars": 526,
"preview": "module.exports = {\n root: true,\n env: { browser: true, es2020: true },\n extends: [\n 'eslint:recommended',\n 'plu"
},
{
"path": "frontend/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "frontend/README.md",
"chars": 451,
"preview": "# React + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCur"
},
{
"path": "frontend/index.html",
"chars": 361,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "frontend/package.json",
"chars": 993,
"preview": "{\n \"name\": \"frontend\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n "
},
{
"path": "frontend/postcss.config.js",
"chars": 80,
"preview": "export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "frontend/src/App.jsx",
"chars": 375,
"preview": "import { Outlet } from \"react-router-dom\";\nimport { ToastContainer } from \"react-toastify\";\nimport \"react-toastify/dist/"
},
{
"path": "frontend/src/component/GenreForm.jsx",
"chars": 1080,
"preview": "const GenreForm = ({\n value,\n setValue,\n handleSubmit,\n buttonText = \"Submit\",\n handleDelete,\n}) => {\n return (\n "
},
{
"path": "frontend/src/component/Loader.jsx",
"chars": 174,
"preview": "const Loader = () => {\n return (\n <div className=\"animate-spin rounded-full h-16 w-16 border-t-4 border-teal-500 bor"
},
{
"path": "frontend/src/component/Modal.jsx",
"chars": 633,
"preview": "const Modal = ({ isOpen, onClose, children }) => {\n return (\n <>\n {isOpen && (\n <div className=\"fixed in"
},
{
"path": "frontend/src/component/SliderUtil.jsx",
"chars": 512,
"preview": "import Slider from \"react-slick\";\nimport \"slick-carousel/slick/slick.css\";\nimport \"slick-carousel/slick/slick-theme.css\""
},
{
"path": "frontend/src/index.css",
"chars": 178,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n background: #0f0f10;\n color: #fff;\n}\n\ninput,\ntexta"
},
{
"path": "frontend/src/main.jsx",
"chars": 2275,
"preview": "import ReactDOM from \"react-dom/client\";\nimport App from \"./App.jsx\";\nimport \"./index.css\";\nimport store from \"./redux/s"
},
{
"path": "frontend/src/pages/Admin/AdminMoviesList.jsx",
"chars": 1946,
"preview": "import { Link } from \"react-router-dom\";\nimport { useGetAllMoviesQuery } from \"../../redux/api/movies\";\n\nconst AdminMovi"
},
{
"path": "frontend/src/pages/Admin/AdminRoute.jsx",
"chars": 321,
"preview": "import { Navigate, Outlet } from \"react-router-dom\";\nimport { useSelector } from \"react-redux\";\n\nconst AdminRoute = () ="
},
{
"path": "frontend/src/pages/Admin/AllComments.jsx",
"chars": 1514,
"preview": "import {\n useDeleteCommentMutation,\n useGetAllMoviesQuery,\n} from \"../../redux/api/movies\";\nimport { toast } from \"rea"
},
{
"path": "frontend/src/pages/Admin/CreateMovie.jsx",
"chars": 6305,
"preview": "import { useEffect, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport {\n useCreateMovieMu"
},
{
"path": "frontend/src/pages/Admin/Dashboard/AdminDashboard.jsx",
"chars": 204,
"preview": "import Main from \"./Main/Main\";\nimport Sidebar from \"./Sidebar/Sidebar\";\n\nconst AdminDashboard = () => {\n return (\n "
},
{
"path": "frontend/src/pages/Admin/Dashboard/Main/Main.jsx",
"chars": 2035,
"preview": "import SecondaryCard from \"./SecondaryCard\";\nimport VideoCard from \"./VideoCard\";\nimport ReactTimeCard from \"./RealTimeC"
},
{
"path": "frontend/src/pages/Admin/Dashboard/Main/PrimaryCard.jsx",
"chars": 424,
"preview": "import { useGetUsersQuery } from \"../../../../redux/api/users\";\n\nconst PrimaryCard = () => {\n const { data: visitors } "
},
{
"path": "frontend/src/pages/Admin/Dashboard/Main/RealTimeCard.jsx",
"chars": 667,
"preview": "import { useGetUsersQuery } from \"../../../../redux/api/users\";\nimport PrimaryCard from \"./PrimaryCard\";\n\nconst RealTime"
},
{
"path": "frontend/src/pages/Admin/Dashboard/Main/SecondaryCard.jsx",
"chars": 670,
"preview": "const SecondaryCard = ({ pill, content, info, gradient }) => {\n return (\n <div\n className={`w-[15rem] h-[12rem]"
},
{
"path": "frontend/src/pages/Admin/Dashboard/Main/VideoCard.jsx",
"chars": 594,
"preview": "const VideoCard = ({ image, title, date, comments }) => {\n return (\n <>\n <div className=\"flex items-center w-[9"
},
{
"path": "frontend/src/pages/Admin/Dashboard/Sidebar/Sidebar.jsx",
"chars": 1634,
"preview": "import { Link } from \"react-router-dom\";\n\nconst Sidebar = () => {\n return (\n <div className=\"-translate-y-10 flex h-"
},
{
"path": "frontend/src/pages/Admin/GenreList.jsx",
"chars": 3758,
"preview": "import { useState } from \"react\";\nimport {\n useCreateGenreMutation,\n useUpdateGenreMutation,\n useDeleteGenreMutation,"
},
{
"path": "frontend/src/pages/Admin/UpdateMovie.jsx",
"chars": 5660,
"preview": "import { useEffect, useState } from \"react\";\nimport { useParams, useNavigate } from \"react-router-dom\";\nimport {\n useGe"
},
{
"path": "frontend/src/pages/Auth/Login.jsx",
"chars": 3603,
"preview": "import { useState, useEffect } from \"react\";\nimport { Link, useLocation, useNavigate } from \"react-router-dom\";\nimport {"
},
{
"path": "frontend/src/pages/Auth/Navigation.jsx",
"chars": 4954,
"preview": "import { useState } from \"react\";\nimport {\n AiOutlineHome,\n AiOutlineLogin,\n AiOutlineUserAdd,\n} from \"react-icons/ai"
},
{
"path": "frontend/src/pages/Auth/PrivateRoute.jsx",
"chars": 285,
"preview": "import { Navigate, Outlet } from \"react-router-dom\";\nimport { useSelector } from \"react-redux\";\n\nconst PrivateRoute = ()"
},
{
"path": "frontend/src/pages/Auth/Register.jsx",
"chars": 4813,
"preview": "import { useState, useEffect } from \"react\";\nimport { Link, useLocation, useNavigate } from \"react-router-dom\";\nimport {"
},
{
"path": "frontend/src/pages/Home.jsx",
"chars": 286,
"preview": "import Header from \"./Movies/Header\";\nimport MoviesContainerPage from \"./Movies/MoviesContainerPage\";\n\nconst Home = () ="
},
{
"path": "frontend/src/pages/Movies/AllMovies.jsx",
"chars": 5264,
"preview": "import { useGetAllMoviesQuery } from \"../../redux/api/movies\";\nimport { useFetchGenresQuery } from \"../../redux/api/genr"
},
{
"path": "frontend/src/pages/Movies/Header.jsx",
"chars": 969,
"preview": "import SliderUtil from \"../../component/SliderUtil\";\nimport { useGetNewMoviesQuery } from \"../../redux/api/movies\";\nimpo"
},
{
"path": "frontend/src/pages/Movies/MovieCard.jsx",
"chars": 630,
"preview": "import { Link } from \"react-router-dom\";\n\nconst MovieCard = ({ movie }) => {\n return (\n <div key={movie._id} classNa"
},
{
"path": "frontend/src/pages/Movies/MovieDetails.jsx",
"chars": 2628,
"preview": "import { useState } from \"react\";\nimport { useParams, Link } from \"react-router-dom\";\nimport { useSelector } from \"react"
},
{
"path": "frontend/src/pages/Movies/MovieTabs.jsx",
"chars": 1778,
"preview": "import { Link } from \"react-router-dom\";\n\nconst MovieTabs = ({ userInfo, submitHandler, comment, setComment, movie }) =>"
},
{
"path": "frontend/src/pages/Movies/MoviesContainerPage.jsx",
"chars": 1995,
"preview": "import { useState } from \"react\";\nimport {\n useGetNewMoviesQuery,\n useGetTopMoviesQuery,\n useGetRandomMoviesQuery,\n} "
},
{
"path": "frontend/src/pages/User/Profile.jsx",
"chars": 3883,
"preview": "import { useEffect, useState } from \"react\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { toast } fr"
},
{
"path": "frontend/src/redux/api/apiSlice.js",
"chars": 252,
"preview": "import { fetchBaseQuery, createApi } from \"@reduxjs/toolkit/query/react\";\nimport { BASE_URL } from \"../constants\";\n\ncons"
},
{
"path": "frontend/src/redux/api/genre.js",
"chars": 879,
"preview": "import { apiSlice } from \"./apiSlice\";\nimport { GENRE_URL } from \"../constants\";\n\nexport const genreApiSlice = apiSlice."
},
{
"path": "frontend/src/redux/api/movies.js",
"chars": 2087,
"preview": "import { apiSlice } from \"./apiSlice\";\nimport { MOVIE_URL, UPLOAD_URL } from \"../constants\";\n\nexport const moviesApiSlic"
},
{
"path": "frontend/src/redux/api/users.js",
"chars": 1003,
"preview": "import { apiSlice } from \"./apiSlice\";\nimport { USERS_URL } from \"../constants\";\n\nexport const userApiSlice = apiSlice.i"
},
{
"path": "frontend/src/redux/constants.js",
"chars": 199,
"preview": "export const BASE_URL = \"\";\nexport const USERS_URL = \"/api/v1/users\";\nexport const GENRE_URL = \"/api/v1/genre\";\nexport c"
},
{
"path": "frontend/src/redux/features/auth/authSlice.js",
"chars": 757,
"preview": "import { createSlice } from \"@reduxjs/toolkit\";\n\nconst initialState = {\n userInfo: localStorage.getItem(\"userInfo\")\n "
},
{
"path": "frontend/src/redux/features/movies/moviesSlice.js",
"chars": 886,
"preview": "import { createSlice } from \"@reduxjs/toolkit\";\n\nconst moviesSlice = createSlice({\n name: \"movies\",\n initialState: {\n "
},
{
"path": "frontend/src/redux/store.js",
"chars": 595,
"preview": "import { configureStore } from \"@reduxjs/toolkit\";\nimport { setupListeners } from \"@reduxjs/toolkit/query/react\";\nimport"
},
{
"path": "frontend/tailwind.config.js",
"chars": 170,
"preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n"
},
{
"path": "frontend/vite.config.js",
"chars": 287,
"preview": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vitejs.dev/config/\nexport def"
},
{
"path": "package.json",
"chars": 650,
"preview": "{\n \"name\": \"my-movies\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"type\": \"module\",\n \"main\": \"index.js\",\n \"scripts"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the HuXn-WebDev/MERN-Movies-App GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (88.3 KB), approximately 23.9k tokens, and a symbol index with 7 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.