Building Netflix Clone with NextJs 13.4: Part 1
Abhirup Kumar Bhowmick
Posted on December 19, 2023
NextJs 13, React, Tailwind CSS, Firebase, Razorpay.
OTT platforms are the latest craze right now! With 238.39 million paid subscribers in 2023, Netflix is a fairly well-known OTT platform. This is the part 1 of the blog, where I’ll demonstrate how I created a Netflix clone using NextJs 13.4.
Github Link: https://github.com/abhirupkumar/Netflix-Clone
Project Link: https://netflix-akb.vercel.app
Prerequisites
Knowledge of Html, Css, Js ,React and Nextjs.
Getting started
Open your terminal and paste the below command to create the project.
npx create-next-app@latest
Follow these steps for installation:
What is your project named? netflix-clone
Would you like to use TypeScript? Yes
Would you like to use ESLint? No
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? No
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
After successfully creating the project, open the project in VScode and open it’s terminal. To install all the libraries needed for developing this project, start by typing the following command.
npx install @emotion/react @emotion/styled @mui/material firebase razorpay react-hook-form react-hot-toast react-
icons react-player recoil
Now go to https://www.themoviedb.org/ **and Login/Sign Up. We will be using the TMDB API to get all the movies. Then go to **https://www.themoviedb.org/settings/api and get your API key. Create a .env.local file and paste your API key their.
NEXT_PUBLIC_API_KEY=**********************
Now open a merchant account in Razorpay, get the merchant key and secret from your dashboard, and paste them in the .env.local.
RAZORPAY_KEY=****************
RAZORPAY_SECRET=****************
Before diving in Typescript code, write the following in global.css file.
@tailwind base;
@tailwind components;
@tailwind utilities;
/* :root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
} */
@media (prefers-color-scheme: dark) {
/* :root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
} */
}
body {
overflow-x: hidden;
/* color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb)); */
}
::-webkit-scrollbar {
width: 8px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #222222;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #363636;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@layer base {
body {
@apply bg-[#141414] text-white;
}
header {
@apply fixed top-0 z-50 flex w-full items-center justify-between px-4 py-4 transition-all lg:px-10 lg:py-6;
}
}
/* custom classNames */
@layer components {
.headerLink {
@apply cursor-pointer text-sm font-semibold text-white transition duration-[.4s] hover:text-[#d6d6d6];
}
.bannerButton {
@apply flex items-center gap-x-2 rounded px-5 py-1.5 text-sm font-semibold transition hover:opacity-75 md:py-2.5 md:px-8 md:text-xl;
}
.input {
@apply w-full rounded bg-[#333] px-5 py-3.5 placeholder-[gray] outline-none focus:bg-[#454545];
}
.modalButton {
@apply flex h-11 w-11 items-center justify-center rounded-full border-2 border-[gray] bg-[#2a2a2a]/60 transition hover:border-white hover:bg-white/10;
}
.planBox {
@apply relative mx-1.5 flex h-20 w-[calc(100%/4)] cursor-default items-center justify-center rounded-sm bg-[#e50914] font-semibold shadow after:absolute after:top-full after:left-1/2 after:block after:-translate-x-1/2 after:border-8 after:border-b-0 after:border-transparent after:border-t-[#e50914] after:content-[""] md:h-32 lg:mx-8;
}
/* Table */
.tableRow {
@apply flex flex-wrap items-center font-medium;
}
.tableDataTitle {
@apply w-full p-2.5 text-center text-sm font-normal text-white md:w-[40%] md:p-3.5 md:text-left md:text-base;
}
.tableDataFeature {
@apply w-[calc(100%/4)] p-2.5 text-center md:w-[calc(60%/4)] md:p-3.5;
}
.membershipLink {
@apply cursor-pointer text-blue-500 hover:underline;
}
/* MUI Menu */
.menu {
@apply md:hidden;
}
.menu .MuiPaper-root {
@apply !absolute !left-0 !rounded-none !border !border-[gray] !bg-black !text-white;
}
.menu .MuiList-root {
@apply !p-0;
}
.menu .MuiMenuItem-root {
@apply !block !w-72 !py-3.5 !text-center !text-sm !font-light !text-[#b3b3b3] !transition !duration-200 first:cursor-default first:!font-normal first:!text-white hover:!bg-[#11100F];
}
}
.lds-ripple {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
justify-content: center;
align-items: center;
}
.lds-ripple div {
position: absolute;
border: 4px solid #ffffff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 72px;
height: 72px;
opacity: 0;
}
}
Now create an action folder outside of the app directory. The folder will contain an actions.ts file. In action.ts, we will fetch all the movie data from the TMDB APIT.
"use server";
import requests from "@/utils/requests";
export const fetchAllData = async () => {
const [
netflixOriginals,
trendingNow,
topRated,
actionMovies,
comedyMovies,
horrorMovies,
romanceMovies,
documentaries,
] = await Promise.all([
fetch(requests.fetchNetflixOriginals).then((res) => res.json()),
fetch(requests.fetchTrending).then((res) => res.json()),
fetch(requests.fetchTopRated).then((res) => res.json()),
fetch(requests.fetchActionMovies).then((res) => res.json()),
fetch(requests.fetchComedyMovies).then((res) => res.json()),
fetch(requests.fetchHorrorMovies).then((res) => res.json()),
fetch(requests.fetchRomanceMovies).then((res) => res.json()),
fetch(requests.fetchDocumentaries).then((res) => res.json()),
]);
return {
netflixOriginals: netflixOriginals.results,
trendingNow: trendingNow.results,
topRated: topRated.results,
actionMovies: actionMovies.results,
comedyMovies: comedyMovies.results,
horrorMovies: horrorMovies.results,
romanceMovies: romanceMovies.results,
documentaries: documentaries.results,
};
};
Before going any further, Login to Firebase and create a project. Give any project a name, and after creating the project, click on the web app, give the app a nickname, and click on Register the app. The following type of code will appear. Copy the code from their and click on done.
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "**************************************",
authDomain: "*******************.firebaseapp.com",
projectId: "*******************",
storageBucket: "*******************.appspot.com",
messagingSenderId: "*************",
appId: "*:*************:web:*****************"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
Create a firebase.ts file outside of the app folder and paste the copied code. Again, go back to Firebase, click on Authentication, and turn on ‘Email/Password’ Authentication. Click on Firestore Database and create a database in production mode. Now we are all set to proceed with our hooks.
Create a constants folder outside of the app directory and create an app.tsx file and a movie.ts file in that folder.
// movie.ts
export const baseUrl = 'https://image.tmdb.org/t/p/original/';
//App.tsx
"use client";
import React from 'react'
import { RecoilRoot } from 'recoil'
const App = ({
children,
}: {
children: React.ReactNode
}) => {
return (
<html lang="en">
<body>
<RecoilRoot>
{children}
</RecoilRoot>
</body>
</html>
)
}
export default App;
In the app folder, change the code for layout.tsx to the following:
import './globals.css'
import type { Metadata } from 'next'
import App from '@/constants/App';
export const metadata: Metadata = {
title: 'NETFLIX',
description: 'Watch your favorite movies and TV shows on Netflix.',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<App children={children} />
)
}
Outside of app firectory create typings.d.ts file.
// typings.d.ts
export interface Genre {
id: number
name: string
}
export interface Plan {
id: string
name: string
amount: number
description: string
videoQuality: string
resolution: string
}
export interface Movie {
title: string
backdrop_path: string
media_type?: string
release_date?: string
first_air_date: string
genre_ids: number[]
id: number
name: string
origin_country: string[]
original_language: string
original_name: string
overview: string
popularity: number
poster_path: string
vote_average: number
vote_count: number
}
export interface Element {
type:
| 'Bloopers'
| 'Featurette'
| 'Behind the Scenes'
| 'Clip'
| 'Trailer'
| 'Teaser'
}
Now create an atoms folder outside of the app directory. The atoms folder will contain only one file, modalAtom.ts, which contains the following:
import { DocumentData } from 'firebase/firestore'
import { atom } from 'recoil'
import { Movie } from '../typings'
export const modalState = atom({
key: 'modalState',
default: false,
})
export const movieState = atom<Movie | DocumentData | null>({
key: 'movieState',
default: null,
})
Create a utils folder outside of the app folder. In the utils folder, create data.ts and requests.ts files. The first file data.ts will contain our custom plan data. Feel free to change any items in the plan. And requests.ts file will contain all the API endpoints that we will be using in our application.
// data.ts
const plan = [
{
id: "netflix-plan-1-mobile",
name: "Mobile",
amount: 149,
description: "Watch on your mobile phone and tablet",
videoQuality: "Average",
resolution: "480p"
},
{
id: "netflix-plan-2-basic",
name: "Basic",
amount: 199,
description: "Watch on your TV, computer, mobile phone and tablet",
videoQuality: "Good",
resolution: "720p"
},
{
id: "netflix-plan-3-standard",
name: "Standard",
amount: 499,
description: "Watch on your TV, computer, mobile phone and tablet",
videoQuality: "Great",
resolution: "720p"
},
{
id: "netflix-plan-4-premium",
name: "Premium",
amount: 649,
description: "Watch on your TV, computer, mobile phone and tablet",
videoQuality: "Best",
resolution: "720p"
},
]
export default plan
// requests.ts
const API_KEY = process.env.NEXT_PUBLIC_API_KEY
const BASE_URL = 'https://api.themoviedb.org/3'
const requests = {
fetchTrending: `${BASE_URL}/trending/all/week?api_key=${API_KEY}&language=en-IN`,
fetchNetflixOriginals: `${BASE_URL}/discover/movie?api_key=${API_KEY}&with_networks=213`,
fetchTopRated: `${BASE_URL}/movie/top_rated?api_key=${API_KEY}&language=en-IN`,
fetchActionMovies: `${BASE_URL}/discover/movie?api_key=${API_KEY}&language=en-IN&with_genres=28`,
fetchComedyMovies: `${BASE_URL}/discover/movie?api_key=${API_KEY}&language=en-IN&with_genres=35`,
fetchHorrorMovies: `${BASE_URL}/discover/movie?api_key=${API_KEY}&language=en-IN&with_genres=27`,
fetchRomanceMovies: `${BASE_URL}/discover/movie?api_key=${API_KEY}&language=en-IN&with_genres=10749`,
fetchDocumentaries: `${BASE_URL}/discover/movie?api_key=${API_KEY}&language=en-IN&with_genres=99`,
}
export default requests
Hence, we have set up all the utility files for our projects. In the next part, we will continue with the frontend and backend of our website.
Guys, if you enjoyed this, please give it a clap and share it with your friends who also want to learn and implement Next.js 13. If you missed anything or want to check out the full code here. And if you want more articles like this, follow me on dev.to. Link for Part 2.
Guys, I have some good news for those who are preparing for interviews. Checkout PrepiQ (https://prepiq.vercel.app/), your comprehensive interview prep guide.
Thanks for reading!
Posted on December 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.