├── .env.sample ├── .gitignore ├── README.md ├── backend ├── config │ ├── db.js │ └── envVars.js ├── controllers │ ├── auth.controller.js │ ├── movie.controller.js │ ├── search.controller.js │ └── tv.controller.js ├── middleware │ └── protectRoute.js ├── models │ └── user.model.js ├── routes │ ├── auth.route.js │ ├── movie.route.js │ ├── search.route.js │ └── tv.route.js ├── server.js ├── services │ └── tmdb.service.js └── utils │ └── generateToken.js ├── frontend ├── .eslintrc.cjs ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── 404.png │ ├── avatar1.png │ ├── avatar2.png │ ├── avatar3.png │ ├── device-pile.png │ ├── download-icon.gif │ ├── extraction.jpg │ ├── favicon.png │ ├── hero-vid.m4v │ ├── hero.png │ ├── kids.png │ ├── netflix-logo.png │ ├── screenshot-for-readme.png │ ├── stranger-things-lg.png │ ├── stranger-things-sm.png │ ├── tv.png │ └── video-devices.m4v ├── src │ ├── App.jsx │ ├── components │ │ ├── Footer.jsx │ │ ├── MovieSlider.jsx │ │ ├── Navbar.jsx │ │ └── skeletons │ │ │ └── WatchPageSkeleton.jsx │ ├── hooks │ │ └── useGetTrendingContent.jsx │ ├── index.css │ ├── main.jsx │ ├── pages │ │ ├── 404.jsx │ │ ├── LoginPage.jsx │ │ ├── SearchHistoryPage.jsx │ │ ├── SearchPage.jsx │ │ ├── SignUpPage.jsx │ │ ├── WatchPage.jsx │ │ └── home │ │ │ ├── AuthScreen.jsx │ │ │ ├── HomePage.jsx │ │ │ └── HomeScreen.jsx │ ├── store │ │ ├── authUser.js │ │ └── content.js │ └── utils │ │ ├── constants.js │ │ └── dateFunction.js ├── tailwind.config.js └── vite.config.js ├── package-lock.json └── package.json /.env.sample: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | MONGO_URI=your_mongo_uri 3 | NODE_ENV=development 4 | JWT_SECRET=your_jwt_secret 5 | TMDB_API_KEY=your_tmdb_api_key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | .env 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MERN Netflix Clone 🎬

2 | 3 | 4 | About This Course: 5 | 6 | - ⚛️ Tech Stack: React.js, Node.js, Express.js, MongoDB, Tailwind 7 | - 🔐 Authentication with JWT 8 | - 📱 Responsive UI 9 | - 🎬 Fetch Movies and Tv Show 10 | - 🔎 Search for Actors and Movies 11 | - 🎥 Watch Trailers 12 | - 🔥 Fetch Search History 13 | - 🐱‍👤 Get Similar Movies/Tv Shows 14 | - 💙 Awesome Landing Page 15 | - 🌐 Deployment 16 | 17 | 18 | ### Setup .env file 19 | 20 | ```bash 21 | PORT=5000 22 | MONGO_URI=your_mongo_uri 23 | NODE_ENV=development 24 | JWT_SECRET=your_jwt_secre 25 | TMDB_API_KEY=your_tmdb_api_key 26 | ``` 27 | 28 | ### Run this app locally 29 | 30 | ```shell 31 | npm run build 32 | ``` 33 | 34 | ### Start the app 35 | 36 | ```shell 37 | npm run start 38 | ``` 39 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { ENV_VARS } from "./envVars.js"; 3 | 4 | export const connectDB = async () => { 5 | try { 6 | const conn = await mongoose.connect(ENV_VARS.MONGO_URI); 7 | console.log("MongoDB connected: " + conn.connection.host); 8 | } catch (error) { 9 | console.error("Error connecting to MONGODB: " + error.message); 10 | process.exit(1); // 1 means there was an error, 0 means success 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /backend/config/envVars.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | 3 | dotenv.config(); 4 | 5 | export const ENV_VARS = { 6 | MONGO_URI: process.env.MONGO_URI, 7 | PORT: process.env.PORT || 5000, 8 | JWT_SECRET: process.env.JWT_SECRET, 9 | NODE_ENV: process.env.NODE_ENV, 10 | TMDB_API_KEY: process.env.TMDB_API_KEY, 11 | }; 12 | -------------------------------------------------------------------------------- /backend/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import { User } from "../models/user.model.js"; 2 | import bcryptjs from "bcryptjs"; 3 | import { generateTokenAndSetCookie } from "../utils/generateToken.js"; 4 | 5 | export async function signup(req, res) { 6 | try { 7 | const { email, password, username } = req.body; 8 | 9 | if (!email || !password || !username) { 10 | return res.status(400).json({ success: false, message: "All fields are required" }); 11 | } 12 | 13 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 14 | 15 | if (!emailRegex.test(email)) { 16 | return res.status(400).json({ success: false, message: "Invalid email" }); 17 | } 18 | 19 | if (password.length < 6) { 20 | return res.status(400).json({ success: false, message: "Password must be at least 6 characters" }); 21 | } 22 | 23 | const existingUserByEmail = await User.findOne({ email: email }); 24 | 25 | if (existingUserByEmail) { 26 | return res.status(400).json({ success: false, message: "Email already exists" }); 27 | } 28 | 29 | const existingUserByUsername = await User.findOne({ username: username }); 30 | 31 | if (existingUserByUsername) { 32 | return res.status(400).json({ success: false, message: "Username already exists" }); 33 | } 34 | 35 | const salt = await bcryptjs.genSalt(10); 36 | const hashedPassword = await bcryptjs.hash(password, salt); 37 | 38 | const PROFILE_PICS = ["/avatar1.png", "/avatar2.png", "/avatar3.png"]; 39 | 40 | const image = PROFILE_PICS[Math.floor(Math.random() * PROFILE_PICS.length)]; 41 | 42 | const newUser = new User({ 43 | email, 44 | password: hashedPassword, 45 | username, 46 | image, 47 | }); 48 | 49 | generateTokenAndSetCookie(newUser._id, res); 50 | await newUser.save(); 51 | 52 | res.status(201).json({ 53 | success: true, 54 | user: { 55 | ...newUser._doc, 56 | password: "", 57 | }, 58 | }); 59 | } catch (error) { 60 | console.log("Error in signup controller", error.message); 61 | res.status(500).json({ success: false, message: "Internal server error" }); 62 | } 63 | } 64 | 65 | export async function login(req, res) { 66 | try { 67 | const { email, password } = req.body; 68 | 69 | if (!email || !password) { 70 | return res.status(400).json({ success: false, message: "All fields are required" }); 71 | } 72 | 73 | const user = await User.findOne({ email: email }); 74 | if (!user) { 75 | return res.status(404).json({ success: false, message: "Invalid credentials" }); 76 | } 77 | 78 | const isPasswordCorrect = await bcryptjs.compare(password, user.password); 79 | 80 | if (!isPasswordCorrect) { 81 | return res.status(400).json({ success: false, message: "Invalid credentials" }); 82 | } 83 | 84 | generateTokenAndSetCookie(user._id, res); 85 | 86 | res.status(200).json({ 87 | success: true, 88 | user: { 89 | ...user._doc, 90 | password: "", 91 | }, 92 | }); 93 | } catch (error) { 94 | console.log("Error in login controller", error.message); 95 | res.status(500).json({ success: false, message: "Internal server error" }); 96 | } 97 | } 98 | 99 | export async function logout(req, res) { 100 | try { 101 | res.clearCookie("jwt-netflix"); 102 | res.status(200).json({ success: true, message: "Logged out successfully" }); 103 | } catch (error) { 104 | console.log("Error in logout controller", error.message); 105 | res.status(500).json({ success: false, message: "Internal server error" }); 106 | } 107 | } 108 | 109 | export async function authCheck(req, res) { 110 | try { 111 | console.log("req.user:", req.user); 112 | res.status(200).json({ success: true, user: req.user }); 113 | } catch (error) { 114 | console.log("Error in authCheck controller", error.message); 115 | res.status(500).json({ success: false, message: "Internal server error" }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /backend/controllers/movie.controller.js: -------------------------------------------------------------------------------- 1 | import { fetchFromTMDB } from "../services/tmdb.service.js"; 2 | 3 | export async function getTrendingMovie(req, res) { 4 | try { 5 | const data = await fetchFromTMDB("https://api.themoviedb.org/3/trending/movie/day?language=en-US"); 6 | const randomMovie = data.results[Math.floor(Math.random() * data.results?.length)]; 7 | 8 | res.json({ success: true, content: randomMovie }); 9 | } catch (error) { 10 | res.status(500).json({ success: false, message: "Internal Server Error" }); 11 | } 12 | } 13 | 14 | export async function getMovieTrailers(req, res) { 15 | const { id } = req.params; 16 | try { 17 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${id}/videos?language=en-US`); 18 | res.json({ success: true, trailers: data.results }); 19 | } catch (error) { 20 | if (error.message.includes("404")) { 21 | return res.status(404).send(null); 22 | } 23 | 24 | res.status(500).json({ success: false, message: "Internal Server Error" }); 25 | } 26 | } 27 | 28 | export async function getMovieDetails(req, res) { 29 | const { id } = req.params; 30 | try { 31 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${id}?language=en-US`); 32 | res.status(200).json({ success: true, content: data }); 33 | } catch (error) { 34 | if (error.message.includes("404")) { 35 | return res.status(404).send(null); 36 | } 37 | 38 | res.status(500).json({ success: false, message: "Internal Server Error" }); 39 | } 40 | } 41 | 42 | export async function getSimilarMovies(req, res) { 43 | const { id } = req.params; 44 | try { 45 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${id}/similar?language=en-US&page=1`); 46 | res.status(200).json({ success: true, similar: data.results }); 47 | } catch (error) { 48 | res.status(500).json({ success: false, message: "Internal Server Error" }); 49 | } 50 | } 51 | 52 | export async function getMoviesByCategory(req, res) { 53 | const { category } = req.params; 54 | try { 55 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${category}?language=en-US&page=1`); 56 | res.status(200).json({ success: true, content: data.results }); 57 | } catch (error) { 58 | res.status(500).json({ success: false, message: "Internal Server Error" }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/controllers/search.controller.js: -------------------------------------------------------------------------------- 1 | import { User } from "../models/user.model.js"; 2 | import { fetchFromTMDB } from "../services/tmdb.service.js"; 3 | 4 | export async function searchPerson(req, res) { 5 | const { query } = req.params; 6 | try { 7 | const response = await fetchFromTMDB( 8 | `https://api.themoviedb.org/3/search/person?query=${query}&include_adult=false&language=en-US&page=1` 9 | ); 10 | 11 | if (response.results.length === 0) { 12 | return res.status(404).send(null); 13 | } 14 | 15 | await User.findByIdAndUpdate(req.user._id, { 16 | $push: { 17 | searchHistory: { 18 | id: response.results[0].id, 19 | image: response.results[0].profile_path, 20 | title: response.results[0].name, 21 | searchType: "person", 22 | createdAt: new Date(), 23 | }, 24 | }, 25 | }); 26 | 27 | res.status(200).json({ success: true, content: response.results }); 28 | } catch (error) { 29 | console.log("Error in searchPerson controller: ", error.message); 30 | res.status(500).json({ success: false, message: "Internal Server Error" }); 31 | } 32 | } 33 | 34 | export async function searchMovie(req, res) { 35 | const { query } = req.params; 36 | 37 | try { 38 | const response = await fetchFromTMDB( 39 | `https://api.themoviedb.org/3/search/movie?query=${query}&include_adult=false&language=en-US&page=1` 40 | ); 41 | 42 | if (response.results.length === 0) { 43 | return res.status(404).send(null); 44 | } 45 | 46 | await User.findByIdAndUpdate(req.user._id, { 47 | $push: { 48 | searchHistory: { 49 | id: response.results[0].id, 50 | image: response.results[0].poster_path, 51 | title: response.results[0].title, 52 | searchType: "movie", 53 | createdAt: new Date(), 54 | }, 55 | }, 56 | }); 57 | res.status(200).json({ success: true, content: response.results }); 58 | } catch (error) { 59 | console.log("Error in searchMovie controller: ", error.message); 60 | res.status(500).json({ success: false, message: "Internal Server Error" }); 61 | } 62 | } 63 | 64 | export async function searchTv(req, res) { 65 | const { query } = req.params; 66 | 67 | try { 68 | const response = await fetchFromTMDB( 69 | `https://api.themoviedb.org/3/search/tv?query=${query}&include_adult=false&language=en-US&page=1` 70 | ); 71 | 72 | if (response.results.length === 0) { 73 | return res.status(404).send(null); 74 | } 75 | 76 | await User.findByIdAndUpdate(req.user._id, { 77 | $push: { 78 | searchHistory: { 79 | id: response.results[0].id, 80 | image: response.results[0].poster_path, 81 | title: response.results[0].name, 82 | searchType: "tv", 83 | createdAt: new Date(), 84 | }, 85 | }, 86 | }); 87 | res.json({ success: true, content: response.results }); 88 | } catch (error) { 89 | console.log("Error in searchTv controller: ", error.message); 90 | res.status(500).json({ success: false, message: "Internal Server Error" }); 91 | } 92 | } 93 | 94 | export async function getSearchHistory(req, res) { 95 | try { 96 | res.status(200).json({ success: true, content: req.user.searchHistory }); 97 | } catch (error) { 98 | res.status(500).json({ success: false, message: "Internal Server Error" }); 99 | } 100 | } 101 | 102 | export async function removeItemFromSearchHistory(req, res) { 103 | let { id } = req.params; 104 | 105 | id = parseInt(id); 106 | 107 | try { 108 | await User.findByIdAndUpdate(req.user._id, { 109 | $pull: { 110 | searchHistory: { id: id }, 111 | }, 112 | }); 113 | 114 | res.status(200).json({ success: true, message: "Item removed from search history" }); 115 | } catch (error) { 116 | console.log("Error in removeItemFromSearchHistory controller: ", error.message); 117 | res.status(500).json({ success: false, message: "Internal Server Error" }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /backend/controllers/tv.controller.js: -------------------------------------------------------------------------------- 1 | import { fetchFromTMDB } from "../services/tmdb.service.js"; 2 | 3 | export async function getTrendingTv(req, res) { 4 | try { 5 | const data = await fetchFromTMDB("https://api.themoviedb.org/3/trending/tv/day?language=en-US"); 6 | const randomMovie = data.results[Math.floor(Math.random() * data.results?.length)]; 7 | 8 | res.json({ success: true, content: randomMovie }); 9 | } catch (error) { 10 | res.status(500).json({ success: false, message: "Internal Server Error" }); 11 | } 12 | } 13 | 14 | export async function getTvTrailers(req, res) { 15 | const { id } = req.params; 16 | try { 17 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${id}/videos?language=en-US`); 18 | res.json({ success: true, trailers: data.results }); 19 | } catch (error) { 20 | if (error.message.includes("404")) { 21 | return res.status(404).send(null); 22 | } 23 | 24 | res.status(500).json({ success: false, message: "Internal Server Error" }); 25 | } 26 | } 27 | 28 | export async function getTvDetails(req, res) { 29 | const { id } = req.params; 30 | try { 31 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${id}?language=en-US`); 32 | res.status(200).json({ success: true, content: data }); 33 | } catch (error) { 34 | if (error.message.includes("404")) { 35 | return res.status(404).send(null); 36 | } 37 | 38 | res.status(500).json({ success: false, message: "Internal Server Error" }); 39 | } 40 | } 41 | 42 | export async function getSimilarTvs(req, res) { 43 | const { id } = req.params; 44 | try { 45 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${id}/similar?language=en-US&page=1`); 46 | res.status(200).json({ success: true, similar: data.results }); 47 | } catch (error) { 48 | res.status(500).json({ success: false, message: "Internal Server Error" }); 49 | } 50 | } 51 | 52 | export async function getTvsByCategory(req, res) { 53 | const { category } = req.params; 54 | try { 55 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${category}?language=en-US&page=1`); 56 | res.status(200).json({ success: true, content: data.results }); 57 | } catch (error) { 58 | res.status(500).json({ success: false, message: "Internal Server Error" }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/middleware/protectRoute.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { User } from "../models/user.model.js"; 3 | import { ENV_VARS } from "../config/envVars.js"; 4 | 5 | export const protectRoute = async (req, res, next) => { 6 | try { 7 | const token = req.cookies["jwt-netflix"]; 8 | 9 | if (!token) { 10 | return res.status(401).json({ success: false, message: "Unauthorized - No Token Provided" }); 11 | } 12 | 13 | const decoded = jwt.verify(token, ENV_VARS.JWT_SECRET); 14 | 15 | if (!decoded) { 16 | return res.status(401).json({ success: false, message: "Unauthorized - Invalid Token" }); 17 | } 18 | 19 | const user = await User.findById(decoded.userId).select("-password"); 20 | 21 | if (!user) { 22 | return res.status(404).json({ success: false, message: "User not found" }); 23 | } 24 | 25 | req.user = user; 26 | 27 | next(); 28 | } catch (error) { 29 | console.log("Error in protectRoute middleware: ", error.message); 30 | res.status(500).json({ success: false, message: "Internal Server Error" }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /backend/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = mongoose.Schema({ 4 | username: { 5 | type: String, 6 | required: true, 7 | unique: true, 8 | }, 9 | email: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | }, 18 | image: { 19 | type: String, 20 | default: "", 21 | }, 22 | searchHistory: { 23 | type: Array, 24 | default: [], 25 | }, 26 | }); 27 | 28 | export const User = mongoose.model("User", userSchema); 29 | -------------------------------------------------------------------------------- /backend/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authCheck, login, logout, signup } from "../controllers/auth.controller.js"; 3 | import { protectRoute } from "../middleware/protectRoute.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post("/signup", signup); 8 | router.post("/login", login); 9 | router.post("/logout", logout); 10 | 11 | router.get("/authCheck", protectRoute, authCheck); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /backend/routes/movie.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getMovieDetails, 4 | getMoviesByCategory, 5 | getMovieTrailers, 6 | getSimilarMovies, 7 | getTrendingMovie, 8 | } from "../controllers/movie.controller.js"; 9 | 10 | const router = express.Router(); 11 | 12 | router.get("/trending", getTrendingMovie); 13 | router.get("/:id/trailers", getMovieTrailers); 14 | router.get("/:id/details", getMovieDetails); 15 | router.get("/:id/similar", getSimilarMovies); 16 | router.get("/:category", getMoviesByCategory); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /backend/routes/search.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getSearchHistory, 4 | removeItemFromSearchHistory, 5 | searchMovie, 6 | searchPerson, 7 | searchTv, 8 | } from "../controllers/search.controller.js"; 9 | 10 | const router = express.Router(); 11 | 12 | router.get("/person/:query", searchPerson); 13 | router.get("/movie/:query", searchMovie); 14 | router.get("/tv/:query", searchTv); 15 | 16 | router.get("/history", getSearchHistory); 17 | 18 | router.delete("/history/:id", removeItemFromSearchHistory); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /backend/routes/tv.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getSimilarTvs, 4 | getTrendingTv, 5 | getTvDetails, 6 | getTvsByCategory, 7 | getTvTrailers, 8 | } from "../controllers/tv.controller.js"; 9 | 10 | const router = express.Router(); 11 | 12 | router.get("/trending", getTrendingTv); 13 | router.get("/:id/trailers", getTvTrailers); 14 | router.get("/:id/details", getTvDetails); 15 | router.get("/:id/similar", getSimilarTvs); 16 | router.get("/:category", getTvsByCategory); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cookieParser from "cookie-parser"; 3 | import path from "path"; 4 | 5 | import authRoutes from "./routes/auth.route.js"; 6 | import movieRoutes from "./routes/movie.route.js"; 7 | import tvRoutes from "./routes/tv.route.js"; 8 | import searchRoutes from "./routes/search.route.js"; 9 | 10 | import { ENV_VARS } from "./config/envVars.js"; 11 | import { connectDB } from "./config/db.js"; 12 | import { protectRoute } from "./middleware/protectRoute.js"; 13 | 14 | const app = express(); 15 | 16 | const PORT = ENV_VARS.PORT; 17 | const __dirname = path.resolve(); 18 | 19 | app.use(express.json()); // will allow us to parse req.body 20 | app.use(cookieParser()); 21 | 22 | app.use("/api/v1/auth", authRoutes); 23 | app.use("/api/v1/movie", protectRoute, movieRoutes); 24 | app.use("/api/v1/tv", protectRoute, tvRoutes); 25 | app.use("/api/v1/search", protectRoute, searchRoutes); 26 | 27 | if (ENV_VARS.NODE_ENV === "production") { 28 | app.use(express.static(path.join(__dirname, "/frontend/dist"))); 29 | 30 | app.get("*", (req, res) => { 31 | res.sendFile(path.resolve(__dirname, "frontend", "dist", "index.html")); 32 | }); 33 | } 34 | 35 | app.listen(PORT, () => { 36 | console.log("Server started at http://localhost:" + PORT); 37 | connectDB(); 38 | }); 39 | -------------------------------------------------------------------------------- /backend/services/tmdb.service.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { ENV_VARS } from "../config/envVars.js"; 3 | 4 | export const fetchFromTMDB = async (url) => { 5 | const options = { 6 | headers: { 7 | accept: "application/json", 8 | Authorization: "Bearer " + ENV_VARS.TMDB_API_KEY, 9 | }, 10 | }; 11 | 12 | const response = await axios.get(url, options); 13 | 14 | if (response.status !== 200) { 15 | throw new Error("Failed to fetch data from TMDB" + response.statusText); 16 | } 17 | 18 | return response.data; 19 | }; 20 | -------------------------------------------------------------------------------- /backend/utils/generateToken.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { ENV_VARS } from "../config/envVars.js"; 3 | 4 | export const generateTokenAndSetCookie = (userId, res) => { 5 | const token = jwt.sign({ userId }, ENV_VARS.JWT_SECRET, { expiresIn: "15d" }); 6 | 7 | res.cookie("jwt-netflix", token, { 8 | maxAge: 15 * 24 * 60 * 60 * 1000, // 15 days in MS 9 | httpOnly: true, // prevent XSS attacks cross-site scripting attacks, make it not be accessed by JS 10 | sameSite: "strict", // CSRF attacks cross-site request forgery attacks 11 | secure: ENV_VARS.NODE_ENV !== "development", 12 | }); 13 | 14 | return token; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:react/jsx-runtime", 8 | "plugin:react-hooks/recommended", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs"], 11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 12 | settings: { react: { version: "18.2" } }, 13 | plugins: ["react-refresh"], 14 | rules: { 15 | "react/jsx-no-target-blank": "off", 16 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], 17 | "react/no-unescaped-entities": "off", 18 | "react/prop-types": "off", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@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 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.2", 14 | "lucide-react": "^0.408.0", 15 | "netflix-clone": "file:..", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "react-hot-toast": "^2.4.1", 19 | "react-player": "^2.16.0", 20 | "react-router-dom": "^6.25.0", 21 | "tailwind-scrollbar-hide": "^1.1.7", 22 | "zustand": "^4.5.4" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "^18.3.3", 26 | "@types/react-dom": "^18.3.0", 27 | "@vitejs/plugin-react": "^4.3.1", 28 | "autoprefixer": "^10.4.19", 29 | "eslint": "^8.57.0", 30 | "eslint-plugin-react": "^7.34.3", 31 | "eslint-plugin-react-hooks": "^4.6.2", 32 | "eslint-plugin-react-refresh": "^0.4.7", 33 | "postcss": "^8.4.39", 34 | "tailwindcss": "^3.4.6", 35 | "vite": "^5.3.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/404.png -------------------------------------------------------------------------------- /frontend/public/avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/avatar1.png -------------------------------------------------------------------------------- /frontend/public/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/avatar2.png -------------------------------------------------------------------------------- /frontend/public/avatar3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/avatar3.png -------------------------------------------------------------------------------- /frontend/public/device-pile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/device-pile.png -------------------------------------------------------------------------------- /frontend/public/download-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/download-icon.gif -------------------------------------------------------------------------------- /frontend/public/extraction.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/extraction.jpg -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/hero-vid.m4v: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/hero-vid.m4v -------------------------------------------------------------------------------- /frontend/public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/hero.png -------------------------------------------------------------------------------- /frontend/public/kids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/kids.png -------------------------------------------------------------------------------- /frontend/public/netflix-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/netflix-logo.png -------------------------------------------------------------------------------- /frontend/public/screenshot-for-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/screenshot-for-readme.png -------------------------------------------------------------------------------- /frontend/public/stranger-things-lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/stranger-things-lg.png -------------------------------------------------------------------------------- /frontend/public/stranger-things-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/stranger-things-sm.png -------------------------------------------------------------------------------- /frontend/public/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/tv.png -------------------------------------------------------------------------------- /frontend/public/video-devices.m4v: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/video-devices.m4v -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from "react-router-dom"; 2 | import HomePage from "./pages/home/HomePage"; 3 | import LoginPage from "./pages/LoginPage"; 4 | import SignUpPage from "./pages/SignUpPage"; 5 | import WatchPage from "./pages/WatchPage"; 6 | import Footer from "./components/Footer"; 7 | import { Toaster } from "react-hot-toast"; 8 | import { useAuthStore } from "./store/authUser"; 9 | import { useEffect } from "react"; 10 | import { Loader } from "lucide-react"; 11 | import SearchPage from "./pages/SearchPage"; 12 | import SearchHistoryPage from "./pages/SearchHistoryPage"; 13 | import NotFoundPage from "./pages/404"; 14 | 15 | function App() { 16 | const { user, isCheckingAuth, authCheck } = useAuthStore(); 17 | 18 | useEffect(() => { 19 | authCheck(); 20 | }, [authCheck]); 21 | 22 | if (isCheckingAuth) { 23 | return ( 24 |
25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | return ( 33 | <> 34 | 35 | } /> 36 | : } /> 37 | : } /> 38 | : } /> 39 | : } /> 40 | : } /> 41 | } /> 42 | 43 |