├── frontend
├── public
│ ├── 404.png
│ ├── tv.png
│ ├── hero.png
│ ├── kids.png
│ ├── avatar1.png
│ ├── avatar2.png
│ ├── avatar3.png
│ ├── favicon.png
│ ├── hero-vid.m4v
│ ├── device-pile.png
│ ├── extraction.jpg
│ ├── download-icon.gif
│ ├── netflix-logo.png
│ ├── video-devices.m4v
│ ├── stranger-things-lg.png
│ ├── stranger-things-sm.png
│ └── screenshot-for-readme.png
├── postcss.config.js
├── src
│ ├── utils
│ │ ├── dateFunction.js
│ │ └── constants.js
│ ├── store
│ │ ├── content.js
│ │ └── authUser.js
│ ├── pages
│ │ ├── home
│ │ │ ├── HomePage.jsx
│ │ │ ├── HomeScreen.jsx
│ │ │ └── AuthScreen.jsx
│ │ ├── 404.jsx
│ │ ├── LoginPage.jsx
│ │ ├── SignUpPage.jsx
│ │ ├── SearchHistoryPage.jsx
│ │ ├── SearchPage.jsx
│ │ └── WatchPage.jsx
│ ├── main.jsx
│ ├── components
│ │ ├── skeletons
│ │ │ └── WatchPageSkeleton.jsx
│ │ ├── Footer.jsx
│ │ ├── Navbar.jsx
│ │ └── MovieSlider.jsx
│ ├── hooks
│ │ └── useGetTrendingContent.jsx
│ ├── index.css
│ └── App.jsx
├── tailwind.config.js
├── vite.config.js
├── index.html
├── README.md
├── .eslintrc.cjs
└── package.json
├── .env.sample
├── backend
├── config
│ ├── envVars.js
│ └── db.js
├── routes
│ ├── auth.route.js
│ ├── tv.route.js
│ ├── movie.route.js
│ └── search.route.js
├── services
│ └── tmdb.service.js
├── models
│ └── user.model.js
├── utils
│ └── generateToken.js
├── middleware
│ └── protectRoute.js
├── server.js
└── controllers
│ ├── tv.controller.js
│ ├── movie.controller.js
│ ├── auth.controller.js
│ └── search.controller.js
├── .gitignore
├── package.json
└── README.md
/frontend/public/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/404.png
--------------------------------------------------------------------------------
/frontend/public/tv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/tv.png
--------------------------------------------------------------------------------
/frontend/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/hero.png
--------------------------------------------------------------------------------
/frontend/public/kids.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/kids.png
--------------------------------------------------------------------------------
/frontend/public/avatar1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/avatar1.png
--------------------------------------------------------------------------------
/frontend/public/avatar2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/avatar2.png
--------------------------------------------------------------------------------
/frontend/public/avatar3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/avatar3.png
--------------------------------------------------------------------------------
/frontend/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/favicon.png
--------------------------------------------------------------------------------
/frontend/public/hero-vid.m4v:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/hero-vid.m4v
--------------------------------------------------------------------------------
/frontend/public/device-pile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/device-pile.png
--------------------------------------------------------------------------------
/frontend/public/extraction.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/extraction.jpg
--------------------------------------------------------------------------------
/frontend/public/download-icon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/download-icon.gif
--------------------------------------------------------------------------------
/frontend/public/netflix-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/netflix-logo.png
--------------------------------------------------------------------------------
/frontend/public/video-devices.m4v:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/video-devices.m4v
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/stranger-things-lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/stranger-things-lg.png
--------------------------------------------------------------------------------
/frontend/public/stranger-things-sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/stranger-things-sm.png
--------------------------------------------------------------------------------
/frontend/public/screenshot-for-readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/HEAD/frontend/public/screenshot-for-readme.png
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/frontend/src/utils/dateFunction.js:
--------------------------------------------------------------------------------
1 | export function formatReleaseDate(date) {
2 | return new Date(date).toLocaleDateString("en-US", {
3 | year: "numeric",
4 | month: "long",
5 | day: "numeric",
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/store/content.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | export const useContentStore = create((set) => ({
4 | contentType: "movie",
5 | setContentType: (type) => set({ contentType: type }),
6 | }));
7 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import tailwindScrollbarHide from "tailwind-scrollbar-hide";
2 | /** @type {import('tailwindcss').Config} */
3 | export default {
4 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [tailwindScrollbarHide],
9 | };
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | proxy: {
9 | "/api": {
10 | target: "http://localhost:5000",
11 | },
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from "../../store/authUser";
2 | import AuthScreen from "./AuthScreen";
3 | import HomeScreen from "./HomeScreen";
4 |
5 | const HomePage = () => {
6 | const { user } = useAuthStore();
7 |
8 | return <>{user ? : }>;
9 | };
10 | export default HomePage;
11 |
--------------------------------------------------------------------------------
/frontend/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const SMALL_IMG_BASE_URL = "https://image.tmdb.org/t/p/w500";
2 | export const ORIGINAL_IMG_BASE_URL = "https://image.tmdb.org/t/p/original";
3 |
4 | export const MOVIE_CATEGORIES = ["now_playing", "top_rated", "popular", "upcoming"];
5 | export const TV_CATEGORIES = ["airing_today", "on_the_air", "popular", "top_rated"];
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 | import { BrowserRouter } from "react-router-dom";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/frontend/src/components/skeletons/WatchPageSkeleton.jsx:
--------------------------------------------------------------------------------
1 | const WatchPageSkeleton = () => {
2 | return (
3 |
10 | );
11 | };
12 | export default WatchPageSkeleton;
13 |
--------------------------------------------------------------------------------
/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/src/hooks/useGetTrendingContent.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useContentStore } from "../store/content";
3 | import axios from "axios";
4 |
5 | const useGetTrendingContent = () => {
6 | const [trendingContent, setTrendingContent] = useState(null);
7 | const { contentType } = useContentStore();
8 |
9 | useEffect(() => {
10 | const getTrendingContent = async () => {
11 | const res = await axios.get(`/api/v1/${contentType}/trending`);
12 | setTrendingContent(res.data.content);
13 | };
14 |
15 | getTrendingContent();
16 | }, [contentType]);
17 |
18 | return { trendingContent };
19 | };
20 | export default useGetTrendingContent;
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netflix-clone",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "backend/server.js",
6 | "scripts": {
7 | "dev": "NODE_ENV=development nodemon backend/server.js",
8 | "start": "NODE_ENV=production node backend/server.js",
9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "type": "module",
14 | "license": "ISC",
15 | "dependencies": {
16 | "axios": "^1.7.2",
17 | "bcryptjs": "^2.4.3",
18 | "cookie-parser": "^1.4.6",
19 | "dotenv": "^16.4.5",
20 | "express": "^4.19.2",
21 | "jsonwebtoken": "^9.0.2",
22 | "mongoose": "^8.5.1"
23 | },
24 | "devDependencies": {
25 | "nodemon": "^3.1.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return (
3 |
27 | );
28 | };
29 | export default Footer;
30 |
--------------------------------------------------------------------------------
/frontend/src/pages/404.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const NotFoundPage = () => {
4 | return (
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Lost your way?
16 |
17 | Sorry, we can't find that page. You'll find lots to explore on the home page.
18 |
19 |
20 | Netflix Home
21 |
22 |
23 |
24 | );
25 | };
26 | export default NotFoundPage;
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .hero-bg {
6 | background-image: linear-gradient(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.1)), url("/hero.png");
7 | }
8 |
9 | .shimmer {
10 | animation: shimmer 2s infinite linear;
11 | background: linear-gradient(to right, #2c2c2c 4%, #333 25%, #2c2c2c 36%);
12 | background-size: 1000px 100%;
13 | }
14 |
15 | @keyframes shimmer {
16 | 0% {
17 | background-position: -1000px 0;
18 | }
19 | 100% {
20 | background-position: 1000px 0;
21 | }
22 | }
23 |
24 | .error-page--content::before {
25 | background: radial-gradient(
26 | ellipse at center,
27 | rgba(0, 0, 0, 0.5) 0,
28 | rgba(0, 0, 0, 0.2) 45%,
29 | rgba(0, 0, 0, 0.1) 55%,
30 | transparent 70%
31 | );
32 | bottom: -10vw;
33 | content: "";
34 | left: 10vw;
35 | position: absolute;
36 | right: 10vw;
37 | top: -10vw;
38 | z-index: -1;
39 | }
40 |
41 | ::-webkit-scrollbar {
42 | width: 8px;
43 | }
44 |
45 | ::-webkit-scrollbar-thumb {
46 | background-color: #4b5563;
47 | border-radius: 6px;
48 | }
49 |
50 | ::-webkit-scrollbar-track {
51 | background-color: #1a202c;
52 | }
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
29 | );
30 | }
31 |
32 | return (
33 | <>
34 |
35 | } />
36 | : } />
37 | : } />
38 | : } />
39 | : } />
40 | : } />
41 | } />
42 |
43 |
44 |
45 |
46 | >
47 | );
48 | }
49 |
50 | export default App;
51 |
--------------------------------------------------------------------------------
/frontend/src/store/authUser.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import toast from "react-hot-toast";
3 | import { create } from "zustand";
4 |
5 | export const useAuthStore = create((set) => ({
6 | user: null,
7 | isSigningUp: false,
8 | isCheckingAuth: true,
9 | isLoggingOut: false,
10 | isLoggingIn: false,
11 | signup: async (credentials) => {
12 | set({ isSigningUp: true });
13 | try {
14 | const response = await axios.post("/api/v1/auth/signup", credentials);
15 | set({ user: response.data.user, isSigningUp: false });
16 | toast.success("Account created successfully");
17 | } catch (error) {
18 | toast.error(error.response.data.message || "Signup failed");
19 | set({ isSigningUp: false, user: null });
20 | }
21 | },
22 | login: async (credentials) => {
23 | set({ isLoggingIn: true });
24 | try {
25 | const response = await axios.post("/api/v1/auth/login", credentials);
26 | set({ user: response.data.user, isLoggingIn: false });
27 | } catch (error) {
28 | set({ isLoggingIn: false, user: null });
29 | toast.error(error.response.data.message || "Login failed");
30 | }
31 | },
32 | logout: async () => {
33 | set({ isLoggingOut: true });
34 | try {
35 | await axios.post("/api/v1/auth/logout");
36 | set({ user: null, isLoggingOut: false });
37 | toast.success("Logged out successfully");
38 | } catch (error) {
39 | set({ isLoggingOut: false });
40 | toast.error(error.response.data.message || "Logout failed");
41 | }
42 | },
43 | authCheck: async () => {
44 | set({ isCheckingAuth: true });
45 | try {
46 | const response = await axios.get("/api/v1/auth/authCheck");
47 |
48 | set({ user: response.data.user, isCheckingAuth: false });
49 | } catch (error) {
50 | set({ isCheckingAuth: false, user: null });
51 | // toast.error(error.response.data.message || "An error occurred");
52 | }
53 | },
54 | }));
55 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { LogOut, Menu, Search } from "lucide-react";
4 | import { useAuthStore } from "../store/authUser";
5 | import { useContentStore } from "../store/content";
6 |
7 | const Navbar = () => {
8 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
9 | const { user, logout } = useAuthStore();
10 |
11 | const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
12 |
13 | const { setContentType } = useContentStore();
14 |
15 | return (
16 |
17 |
18 |
19 |

20 |
21 |
22 | {/* desktop navbar items */}
23 |
24 | setContentType("movie")}>
25 | Movies
26 |
27 | setContentType("tv")}>
28 | Tv Shows
29 |
30 |
31 | Search History
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |

41 |
42 |
43 |
44 |
45 |
46 |
47 | {/* mobile navbar items */}
48 | {isMobileMenuOpen && (
49 |
50 |
51 | Movies
52 |
53 |
54 | Tv Shows
55 |
56 |
57 | Search History
58 |
59 |
60 | )}
61 |
62 | );
63 | };
64 | export default Navbar;
65 |
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useAuthStore } from "../store/authUser";
4 |
5 | const LoginPage = () => {
6 | const [email, setEmail] = useState("");
7 | const [password, setPassword] = useState("");
8 |
9 | const { login, isLoggingIn } = useAuthStore();
10 |
11 | const handleLogin = (e) => {
12 | e.preventDefault();
13 | login({ email, password });
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Login
27 |
28 |
66 |
67 | Don't have an account?{" "}
68 |
69 | Sign Up
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 | export default LoginPage;
78 |
--------------------------------------------------------------------------------
/frontend/src/components/MovieSlider.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { useContentStore } from "../store/content";
3 | import axios from "axios";
4 | import { Link } from "react-router-dom";
5 | import { SMALL_IMG_BASE_URL } from "../utils/constants";
6 | import { ChevronLeft, ChevronRight } from "lucide-react";
7 |
8 | const MovieSlider = ({ category }) => {
9 | const { contentType } = useContentStore();
10 | const [content, setContent] = useState([]);
11 | const [showArrows, setShowArrows] = useState(false);
12 |
13 | const sliderRef = useRef(null);
14 |
15 | const formattedCategoryName =
16 | category.replaceAll("_", " ")[0].toUpperCase() + category.replaceAll("_", " ").slice(1);
17 | const formattedContentType = contentType === "movie" ? "Movies" : "TV Shows";
18 |
19 | useEffect(() => {
20 | const getContent = async () => {
21 | const res = await axios.get(`/api/v1/${contentType}/${category}`);
22 | setContent(res.data.content);
23 | };
24 |
25 | getContent();
26 | }, [contentType, category]);
27 |
28 | const scrollLeft = () => {
29 | if (sliderRef.current) {
30 | sliderRef.current.scrollBy({ left: -sliderRef.current.offsetWidth, behavior: "smooth" });
31 | }
32 | };
33 | const scrollRight = () => {
34 | sliderRef.current.scrollBy({ left: sliderRef.current.offsetWidth, behavior: "smooth" });
35 | };
36 |
37 | return (
38 | setShowArrows(true)}
41 | onMouseLeave={() => setShowArrows(false)}
42 | >
43 |
44 | {formattedCategoryName} {formattedContentType}
45 |
46 |
47 |
48 | {content.map((item) => (
49 |
50 |
51 |

56 |
57 |
{item.title || item.name}
58 |
59 | ))}
60 |
61 |
62 | {showArrows && (
63 | <>
64 |
72 |
73 |
81 | >
82 | )}
83 |
84 | );
85 | };
86 | export default MovieSlider;
87 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useAuthStore } from "../store/authUser";
4 |
5 | const SignUpPage = () => {
6 | const { searchParams } = new URL(document.location);
7 | const emailValue = searchParams.get("email");
8 |
9 | const [email, setEmail] = useState(emailValue || "");
10 | const [username, setUsername] = useState("");
11 | const [password, setPassword] = useState("");
12 |
13 | const { signup, isSigningUp } = useAuthStore();
14 |
15 | const handleSignUp = (e) => {
16 | e.preventDefault();
17 | signup({ email, username, password });
18 | };
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Sign Up
31 |
32 |
84 |
85 | Already a member?{" "}
86 |
87 | Sign in
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 | export default SignUpPage;
96 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/HomeScreen.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import Navbar from "../../components/Navbar";
3 | import { Info, Play } from "lucide-react";
4 | import useGetTrendingContent from "../../hooks/useGetTrendingContent";
5 | import { MOVIE_CATEGORIES, ORIGINAL_IMG_BASE_URL, TV_CATEGORIES } from "../../utils/constants";
6 | import { useContentStore } from "../../store/content";
7 | import MovieSlider from "../../components/MovieSlider";
8 | import { useState } from "react";
9 |
10 | const HomeScreen = () => {
11 | const { trendingContent } = useGetTrendingContent();
12 | const { contentType } = useContentStore();
13 | const [imgLoading, setImgLoading] = useState(true);
14 |
15 | if (!trendingContent)
16 | return (
17 |
21 | );
22 |
23 | return (
24 | <>
25 |
26 |
27 |
28 | {/* COOL OPTIMIZATION HACK FOR IMAGES */}
29 | {imgLoading && (
30 |
31 | )}
32 |
33 |

{
38 | setImgLoading(false);
39 | }}
40 | />
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 | {trendingContent?.title || trendingContent?.name}
53 |
54 |
55 | {trendingContent?.release_date?.split("-")[0] ||
56 | trendingContent?.first_air_date.split("-")[0]}{" "}
57 | | {trendingContent?.adult ? "18+" : "PG-13"}
58 |
59 |
60 |
61 | {trendingContent?.overview.length > 200
62 | ? trendingContent?.overview.slice(0, 200) + "..."
63 | : trendingContent?.overview}
64 |
65 |
66 |
67 |
68 |
73 |
74 | Play
75 |
76 |
77 |
81 |
82 | More Info
83 |
84 |
85 |
86 |
87 |
88 |
89 | {contentType === "movie"
90 | ? MOVIE_CATEGORIES.map((category) => )
91 | : TV_CATEGORIES.map((category) => )}
92 |
93 | >
94 | );
95 | };
96 | export default HomeScreen;
97 |
--------------------------------------------------------------------------------
/frontend/src/pages/SearchHistoryPage.jsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { useEffect, useState } from "react";
3 | import Navbar from "../components/Navbar";
4 | import { SMALL_IMG_BASE_URL } from "../utils/constants";
5 | import { Trash } from "lucide-react";
6 | import toast from "react-hot-toast";
7 |
8 | function formatDate(dateString) {
9 | // Create a Date object from the input date string
10 | const date = new Date(dateString);
11 |
12 | const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
13 |
14 | // Extract the month, day, and year from the Date object
15 | const month = monthNames[date.getUTCMonth()];
16 | const day = date.getUTCDate();
17 | const year = date.getUTCFullYear();
18 |
19 | // Return the formatted date string
20 | return `${month} ${day}, ${year}`;
21 | }
22 |
23 | const SearchHistoryPage = () => {
24 | const [searchHistory, setSearchHistory] = useState([]);
25 |
26 | useEffect(() => {
27 | const getSearchHistory = async () => {
28 | try {
29 | const res = await axios.get(`/api/v1/search/history`);
30 | setSearchHistory(res.data.content);
31 | } catch (error) {
32 | setSearchHistory([]);
33 | }
34 | };
35 | getSearchHistory();
36 | }, []);
37 |
38 | const handleDelete = async (entry) => {
39 | try {
40 | await axios.delete(`/api/v1/search/history/${entry.id}`);
41 | setSearchHistory(searchHistory.filter((item) => item.id !== entry.id));
42 | } catch (error) {
43 | toast.error("Failed to delete search item");
44 | }
45 | };
46 |
47 | if (searchHistory?.length === 0) {
48 | return (
49 |
50 |
51 |
52 |
Search History
53 |
54 |
No search history found
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |
Search History
67 |
68 | {searchHistory?.map((entry) => (
69 |
70 |

75 |
76 | {entry.title}
77 | {formatDate(entry.createdAt)}
78 |
79 |
80 |
89 | {entry.searchType[0].toUpperCase() + entry.searchType.slice(1)}
90 |
91 |
handleDelete(entry)}
94 | />
95 |
96 | ))}
97 |
98 |
99 |
100 | );
101 | };
102 | export default SearchHistoryPage;
103 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/frontend/src/pages/SearchPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useContentStore } from "../store/content";
3 | import Navbar from "../components/Navbar";
4 | import { Search } from "lucide-react";
5 | import toast from "react-hot-toast";
6 | import axios from "axios";
7 | import { ORIGINAL_IMG_BASE_URL } from "../utils/constants";
8 | import { Link } from "react-router-dom";
9 |
10 | const SearchPage = () => {
11 | const [activeTab, setActiveTab] = useState("movie");
12 | const [searchTerm, setSearchTerm] = useState("");
13 |
14 | const [results, setResults] = useState([]);
15 | const { setContentType } = useContentStore();
16 |
17 | const handleTabClick = (tab) => {
18 | setActiveTab(tab);
19 | tab === "movie" ? setContentType("movie") : setContentType("tv");
20 | setResults([]);
21 | };
22 |
23 | const handleSearch = async (e) => {
24 | e.preventDefault();
25 | try {
26 | const res = await axios.get(`/api/v1/search/${activeTab}/${searchTerm}`);
27 | setResults(res.data.content);
28 | } catch (error) {
29 | if (error.response.status === 404) {
30 | toast.error("Nothing found, make sure you are searching under the right category");
31 | } else {
32 | toast.error("An error occurred, please try again later");
33 | }
34 | }
35 | };
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
50 |
58 |
66 |
67 |
68 |
80 |
81 |
82 | {results.map((result) => {
83 | if (!result.poster_path && !result.profile_path) return null;
84 |
85 | return (
86 |
87 | {activeTab === "person" ? (
88 |
89 |

94 |
{result.name}
95 |
96 | ) : (
97 |
{
100 | setContentType(activeTab);
101 | }}
102 | >
103 |

108 |
{result.title || result.name}
109 |
110 | )}
111 |
112 | );
113 | })}
114 |
115 |
116 |
117 | );
118 | };
119 | export default SearchPage;
120 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/AuthScreen.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link, useNavigate } from "react-router-dom";
3 | import { ChevronRight } from "lucide-react";
4 |
5 | const AuthScreen = () => {
6 | const [email, setEmail] = useState("");
7 | const navigate = useNavigate();
8 |
9 | const handleFormSubmit = (e) => {
10 | e.preventDefault();
11 | navigate("/signup?email=" + email);
12 | };
13 |
14 | return (
15 |
16 | {/* Navbar */}
17 |
18 |
19 |
20 | Sign In
21 |
22 |
23 |
24 | {/* hero section */}
25 |
26 |
Unlimited movies, TV shows, and more
27 |
Watch anywhere. Cancel anytime.
28 |
Ready to watch? Enter your email to create or restart your membership.
29 |
30 |
43 |
44 |
45 | {/* separator */}
46 |
47 |
48 | {/* 1st section */}
49 |
50 |
51 | {/* left side */}
52 |
53 |
Enjoy on your TV
54 |
55 | Watch on Smart TVs, PlayStation, Xbox, Chromecast, Apple TV, Blu-ray players, and more.
56 |
57 |
58 | {/* right side */}
59 |
60 |

61 |
70 |
71 |
72 |
73 |
74 | {/* separator */}
75 |
76 |
77 | {/* 2nd section */}
78 |
79 |
80 | {/* left side */}
81 |
82 |
83 |

84 |
85 |
90 |

91 |
92 |
93 | Stranger Things
94 | Downloading...
95 |
96 |
97 |

98 |
99 |
100 |
101 |
102 | {/* right side */}
103 |
104 |
105 |
106 | Download your shows to watch offline
107 |
108 |
109 | Save your favorites easily and always have something to watch.
110 |
111 |
112 |
113 |
114 |
115 | {/* separator */}
116 |
117 |
118 |
119 | {/* 3rd section */}
120 |
121 |
122 | {/* left side */}
123 |
124 |
Watch everywhere
125 |
126 | Stream unlimited movies and TV shows on your phone, tablet, laptop, and TV.
127 |
128 |
129 |
130 | {/* right side */}
131 |
132 |

133 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | {/* 4th section*/}
151 |
152 |
157 | {/* left */}
158 |
159 |

160 |
161 | {/* right */}
162 |
163 |
Create profiles for kids
164 |
165 | Send kids on adventures with their favorite characters in a space made just for them—free
166 | with your membership.
167 |
168 |
169 |
170 |
171 |
172 | );
173 | };
174 | export default AuthScreen;
175 |
--------------------------------------------------------------------------------
/frontend/src/pages/WatchPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { Link, useParams } from "react-router-dom";
3 | import { useContentStore } from "../store/content";
4 | import axios from "axios";
5 | import Navbar from "../components/Navbar";
6 | import { ChevronLeft, ChevronRight } from "lucide-react";
7 | import ReactPlayer from "react-player";
8 | import { ORIGINAL_IMG_BASE_URL, SMALL_IMG_BASE_URL } from "../utils/constants";
9 | import { formatReleaseDate } from "../utils/dateFunction";
10 | import WatchPageSkeleton from "../components/skeletons/WatchPageSkeleton";
11 |
12 | const WatchPage = () => {
13 | const { id } = useParams();
14 | const [trailers, setTrailers] = useState([]);
15 | const [currentTrailerIdx, setCurrentTrailerIdx] = useState(0);
16 | const [loading, setLoading] = useState(true);
17 | const [content, setContent] = useState({});
18 | const [similarContent, setSimilarContent] = useState([]);
19 | const { contentType } = useContentStore();
20 |
21 | const sliderRef = useRef(null);
22 |
23 | useEffect(() => {
24 | const getTrailers = async () => {
25 | try {
26 | const res = await axios.get(`/api/v1/${contentType}/${id}/trailers`);
27 | setTrailers(res.data.trailers);
28 | } catch (error) {
29 | if (error.message.includes("404")) {
30 | setTrailers([]);
31 | }
32 | }
33 | };
34 |
35 | getTrailers();
36 | }, [contentType, id]);
37 |
38 | useEffect(() => {
39 | const getSimilarContent = async () => {
40 | try {
41 | const res = await axios.get(`/api/v1/${contentType}/${id}/similar`);
42 | setSimilarContent(res.data.similar);
43 | } catch (error) {
44 | if (error.message.includes("404")) {
45 | setSimilarContent([]);
46 | }
47 | }
48 | };
49 |
50 | getSimilarContent();
51 | }, [contentType, id]);
52 |
53 | useEffect(() => {
54 | const getContentDetails = async () => {
55 | try {
56 | const res = await axios.get(`/api/v1/${contentType}/${id}/details`);
57 | setContent(res.data.content);
58 | } catch (error) {
59 | if (error.message.includes("404")) {
60 | setContent(null);
61 | }
62 | } finally {
63 | setLoading(false);
64 | }
65 | };
66 |
67 | getContentDetails();
68 | }, [contentType, id]);
69 |
70 | const handleNext = () => {
71 | if (currentTrailerIdx < trailers.length - 1) setCurrentTrailerIdx(currentTrailerIdx + 1);
72 | };
73 | const handlePrev = () => {
74 | if (currentTrailerIdx > 0) setCurrentTrailerIdx(currentTrailerIdx - 1);
75 | };
76 |
77 | const scrollLeft = () => {
78 | if (sliderRef.current) sliderRef.current.scrollBy({ left: -sliderRef.current.offsetWidth, behavior: "smooth" });
79 | };
80 | const scrollRight = () => {
81 | if (sliderRef.current) sliderRef.current.scrollBy({ left: sliderRef.current.offsetWidth, behavior: "smooth" });
82 | };
83 |
84 | if (loading)
85 | return (
86 |
87 |
88 |
89 | );
90 |
91 | if (!content) {
92 | return (
93 |
94 |
95 |
96 |
97 |
Content not found 😥
98 |
99 |
100 |
101 | );
102 | }
103 |
104 | return (
105 |
106 |
107 |
108 |
109 | {trailers.length > 0 && (
110 |
111 |
122 |
123 |
134 |
135 | )}
136 |
137 |
138 | {trailers.length > 0 && (
139 |
146 | )}
147 |
148 | {trailers?.length === 0 && (
149 |
150 | No trailers available for{" "}
151 | {content?.title || content?.name} 😥
152 |
153 | )}
154 |
155 |
156 | {/* movie details */}
157 |
161 |
162 |
{content?.title || content?.name}
163 |
164 |
165 | {formatReleaseDate(content?.release_date || content?.first_air_date)} |{" "}
166 | {content?.adult ? (
167 | 18+
168 | ) : (
169 | PG-13
170 | )}{" "}
171 |
172 |
{content?.overview}
173 |
174 |

179 |
180 |
181 | {similarContent.length > 0 && (
182 |
183 |
Similar Movies/Tv Show
184 |
185 |
186 | {similarContent.map((content) => {
187 | if (content.poster_path === null) return null;
188 | return (
189 |
190 |

195 |
{content.title || content.name}
196 |
197 | );
198 | })}
199 |
200 |
206 |
212 |
213 |
214 | )}
215 |
216 |
217 | );
218 | };
219 | export default WatchPage;
220 |
--------------------------------------------------------------------------------