Netflix Clone Build 2.0 With React-Redux and Firebase

Netflix Clone Build 2.0 With React-Redux and Firebase

Hello there! How do you do? I hope you're extremely doing well. Well, this time I've come up with a cloning project. And yes. It's the all time famous "netflix clone". What makes you a web developer without having atleast one clone webapp in your resume? A good one.

Tools we're going to use:

  • React JS
  • Redux
  • Firebase
  • TMDB API
  • HTML & CSS

Features:

  • Responsiveness
  • Dynamic
  • Realtime data fetching
  • Updated new list of movies and webseries
  • Authenticated users

Let's Start Building

Firstly we need to create a project folder and name it netflix-clone (anything you want) and open cmd in that directory.

Now, let's install react redux: npx create-react-app netflix-clone --template redux

Open the directory with a code editor (I'd prefer vscode) and let's get started.

At the end of the project, we will have the directories heirarichy as shown below: . └── netflix-clone/

└── src/

    ├── app/

    │   └── store.js

    ├── components/

    │   ├── Banner.js

    │   ├── Nav.js

    │   ├── Row.js

    │   ├── HomeScreen.js

    │   ├── LoginScreen.js

    │   ├── SignUpScreen.js

    │   ├── ProfileScreen.js

    │   └── Requests.js

    ├── features/counter/

    │   └── userSlice.js

    ├── img/

    │   └── logo.png

    ├── App.js

    ├── axios.js

    ├── firebase.js

    ├── index.css

    └── index.js

Meanwhile, let's create a project in firebase console.

. ├── Firebase Console/

│ └── Add Project/

│ └── Project Name (Anything you prefer)

└── Create a web app/

├── Give your webapp a name

├── Copy the script 

└── Create a firebase.js and paste it there.

firebase.js

import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';


const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

const firebaseApp = firebase.initializeApp(firebaseConfig);
const db = firebaseApp.firestore();
const auth = firebase.auth();

export { auth };
export default db;

Do me a favour by creating this firebase configuration with your webapp script, would you?

You'll see something like this.

firebase

We do need authentication activated for our webapp and you can find it in the left side of your firebase website (Build).

Open it and enable Email and Password auth.

email

Now we're using TMDB API to fetch the latest movies and webseries data from it. Go to tmdb API and create a account. It's free. Now we need a API key for our project.

TMDB API/ └── Click on your profile / └── Settings/ └── API (left navbar)

api key

Get back to your code editor and let's make some magic happen.

App.js

import { useEffect } from 'react';
import {
  BrowserRouter as Router,
  Routes,
  Route,
} from "react-router-dom";
import HomeScreen from './components/HomeScreen';
import LoginScreen from './components/LoginScreen';
import { auth } from './firebase';
import { useDispatch, useSelector } from 'react-redux';
import { login, logout, selectUser } from './features/counter/userSlice';
import ProfileScreen from './components/ProfileScreen';

function App() {
  const user = useSelector(selectUser);
  const dispatch = useDispatch();
  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged(userAuth => {
      if(userAuth) {
        dispatch(login({
          uid: userAuth.uid,
          email: userAuth.email,
        }));
      } else {
        dispatch(logout());
      }
    });

    // cleanup
    return unsubscribe;
  }, [dispatch]);

  return (
    <div className="app">
      <Router>
        {
          !user ? (
            <LoginScreen />
          ) : (
            <Routes>
              <Route exact path='/' element = {<HomeScreen />} />
              <Route path = '/profile' element = {<ProfileScreen />} />
            </Routes>
          )
        }
      </Router>
    </div>
  );
}

export default App;

This is the root component for our project. Let's break it down.

  1. It renders Login Screen if the user doesn't get signed in.
  2. If the user is logged in then it renders the main home screen and profile screen.
  3. The task of the useEffect is to dispatch the user details when signed in and reload the page when user gets signed out.

We're using the bueatiful react-render-dom here with the latest changes in the imports. No errors up to date.

userSlice.js

import { createSlice } from '@reduxjs/toolkit';

export const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
  },
  reducers: {
    login: (state, action) => {
      state.user = action.payload;
    },
    logout: (state) => {
      state.user = null;
    }
  },
});

export const { login, logout } = userSlice.actions;
export const selectUser = (state) => state.user.user;
export default userSlice.reducer;

This component is pretty much simple. Think of yourself going to a market and you find there fruits like mango in a row, apples in another row and bananas in another.

Just like that we have slices for users. The state gets updated whenever user sign-in and sign-out. That's what we all need to listen to events for auth, thanks to redux and firebase auth.

axios.js We need to install axios by using the command "npm i axios".

import axios from 'axios';

const instance = axios.create({
    baseURL: "https://api.themoviedb.org/3"
})

export default instance;

The baseURL given above is really helpful when we fetch movies and webseries data from the TMDB API.

Requests.js

const API_KEY = "your-API-key";

const requests = {
    fetchTrending: `/trending/all/week?api_key=${API_KEY}&language=en-US`,
    fetchNetflixOriginals: `/discover/tv?api_key=${API_KEY}&with_networks=213`,
    fetchTopRated: `/movie/top_rated?api_key=${API_KEY}&language=en-US`,
    fetchActionMovies: `/discover/movie?api_key=${API_KEY}&with_genres=28`,
    fetchComedyMovies: `/discover/movie?api_key=${API_KEY}&with_genres=35`,
    fetchHorrorMovies: `/discover/movie?api_key=${API_KEY}&with_genres=27`,
    fetchRomanceMovies: `/discover/movie?api_key=${API_KEY}&with_genres=10749`,
    fetchDocumentaries: `/discover/movie?api_key=${API_KEY}&with_genres=99`,
};

export default requests;

Save this file in src > components.

Banner.js

import axios from '../axios';
import { useEffect, useState } from 'react';
import requests from './Requests';

function Banner() {
    const [movie, setMovie] = useState([]);

    useEffect(() => {
        async function fetchData() {
            const request = await axios.get(requests.fetchNetflixOriginals);
            setMovie(
                request.data.results[
                    Math.floor(Math.random() * request.data.results.length - 1)
                ]
            );
            return request;
        }

        fetchData();
    }, []);

    const truncate = (str, n) => {
        return str?.length > n ? str.substr(0, n - 1) + '...' : str
    }

  return (
    <header 
        className='banner'
        style={{
            backgroundSize: "cover",
            backgroundImage: `url("https://image.tmdb.org/t/p/original/${movie?.backdrop_path}")`,
            backgroundPosition: "center center",
        }}
    >
        <div className='banner-content'>
            <h1 className='banner-title'>
                {movie?.title || movie?.name || movie?.original_name}
            </h1>
            <div className='banner-buttons'>
                <button className='banner-button'>Play</button>
                <button className='banner-button'>My List</button>
            </div>
            <h1 className='banner-description'>
                {
                    truncate(
                        movie.overview, 150
                    )
                }
            </h1>
        </div>

        <div className='banner-fadeBottom' />
    </header>
  )
}

export default Banner;

Banner is what we see when the Netflix page get's loaded. The big main one on the top.

1) We're displaying the banner with the banner content. 2) We've added two buttons just like in Netflix. 3) When the movie description is huge, we truncate it short.

netflix

Row.js

import axios from "../axios";
import { useEffect, useState } from "react";

const Row = ({ title, fetchURL, isLargeRow = false }) => {
   const [movies, setMovies] = useState([]);

   const BASE_URL = "https://image.tmdb.org/t/p/original/";

   useEffect(() => {
    async function fetchData() {
        const request = await axios.get(fetchURL);
        setMovies(request.data.results);
        return request;
    }

    fetchData();
   }, [fetchURL]);

  return (
    <div className='row'>
        <h2>{ title }</h2>

        <div className="row-posters">
            {movies.map((movie) => (
                (isLargeRow && movie.poster_path) ||
                (!isLargeRow && movie.backdrop_path)) && (
                    <img key={movie.id}
                        className = {`row-poster ${isLargeRow && 'row-posterL'}`}
                        src={`${BASE_URL}${
                            isLargeRow ? movie.poster_path : movie.backdrop_path
                        }`}
                        alt = {movie.name} 
                    />
                )
            )}
        </div>
    </div>
  )
}

export default Row;

Those beautiful rows you see down on the main screen, down the banner are fetched using the TMDB API whenever the page loads.

Nav.js

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom';

const Nav = () => {
    const [show, handleShow] = useState(false);
    const navigate = useNavigate();

    const transitionNavBar = () => {
       (window.scrollY > 100) ? handleShow(true) : handleShow(false);
    }

    useEffect(() => {
        window.addEventListener("scroll", transitionNavBar);
        // clean-up
        return () => window.removeEventListener("scroll", transitionNavBar);
    }, []);

  return (
    <div className={`nav ${show && 'nav-black'}`}>
        <div className='nav-content'>
            <img
                onClick={() => navigate('/')}
src='https://assets.stickpng.com/images/580b57fcd9996e24bc43c529.png'
                alt='netflix-logo'
                className='navbar-logo'
            />

            <img 
                onClick={() => navigate('/profile')}
                src='https://upload.wikimedia.org/wikipedia/commons/0/0b/Netflix-avatar.png'
                alt = "avatar"
                className='navbar-avatar'
            />
        </div>

    </div>
  )
}

export default Nav;

I want to give a brief about this one.

1) That cool Netflix logo and Avatar you see on Nav bar. 2) When you click on Netflix logo, you'll be navigated to Homepage of netflix. 3) When you click on your Avatar you'll see a profile screen. That's the power of redux.

profile

ProfileScreen.js

import React from 'react'
import { useSelector } from 'react-redux'
import { selectUser } from '../features/counter/userSlice'
import { auth } from '../firebase'
import Nav from './Nav'

const ProfileScreen = () => {
  const user = useSelector(selectUser);

  return (
    <div className='profileScreen'>
        <Nav />
        <div className='profile-body'>
            <h1>Your Profile</h1>
            <div className='profile-info'>
                <img 
                    src='https://upload.wikimedia.org/wikipedia/commons/0/0b/Netflix-avatar.png' 
                    alt='avatar' 
                />
                <div className='profile-details'>
                  <h2>{user.email}</h2>
                  <div className='profile-plan'>
                    <h3>Premium 4K + HDR</h3>
                    <button 
                      className='profile-signOut'
                      onClick={() => auth.signOut()}
                    >
                        Sign Out
                    </button>
                  </div>
                </div>
            </div>
        </div>
    </div>
  )
}

export default ProfileScreen;

We're fetching the user's email and displaying it with the plan details. Thanks to the useSelector.

LoginScreen.js

import { useState } from 'react'
import SignUpScreen from './SignUpScreen';
import logo from '../img/login-screen-logo.png'

const LoginScreen = () => {
    const [signIn, setSignIn] = useState(false);

  return (
    <div className='loginScreen'>
        <div className='login-bg'>
            <img 
                className='login-logo'
                src={logo}
                alt='login-screen-logo' 
            />
            <button 
                className='login-btn'
                onClick={() => setSignIn(true)}
            >
                Sign In
            </button>

            <div className='login-gradient' />
        </div>

        <div className='login-body'>
            {signIn ? (
                <SignUpScreen />
            ) : (
               <>
                <h1>Unlimited movies, TV shows and more.</h1>
                <h2>Watch anywhere. Cancel anytime.</h2>
                <h3>Ready to watch? Enter your email to create or restart your membership.</h3>

                <div className='login-input'>
                    <form>
                        <input 
                            type='email' 
                            placeholder='Email address'
                        />
                        <button 
                            className='login-getStarted'
                            onClick={() => setSignIn(true)}
                        >
                            {`Get Started >`} 
                        </button>
                    </form>
                </div>
               </>
            )}
        </div>
    </div>
  )
}

export default LoginScreen;

We build the login screen page for the user as it is shown in the original netflix.

1) If the user is new, sign up now will be an option. 2) If the user already exists, the user can sign in with their email and password.

SignUpScreen.js

import React, { useRef } from 'react'
import { auth } from '../firebase';

const SignUpScreen = () => {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const register = (e) => {
    e.preventDefault();

    auth.createUserWithEmailAndPassword(
      emailRef.current.value,
      passwordRef.current.value
    ).then((authUser) => {
      console.log(authUser)
    }).catch((err) => {
      alert(err.message);
    });
  };

  const signIn = (e) => {
    e.preventDefault();
    auth.signInWithEmailAndPassword(
      emailRef.current.value,
      passwordRef.current.value
    ).then((authUser) => {
      console.log(authUser)
    }).catch((err) => {
      alert(err.message)
    });
  };


  return (
    <div className='signUp'>
        <form>
          <h1>Sign In</h1>
          <input 
            type="email"
            placeholder='Email address'
            ref={emailRef}
          />
          <input 
            placeholder='Password'
            type="password"
            ref={passwordRef}
          />
          <button 
            type='submit'
            onClick={signIn}
          >
            Sign In
          </button>

          <h4>
            <span className='signUp-gray'>New to Netflix? </span>
            <span className='signUp-link' onClick={register}>Sign Up now.</span>
          </h4>
        </form>
    </div>
  )
}

export default SignUpScreen;

Here we use the firebase auth to provide the authentication to our users. When a new user is signed up, their data is stored for login availability.

signin

And the stiles

@import url('https://fonts.googleapis.com/css2?family=Readex+Pro:wght@300;400;500;600;700&display=swap');

* {
  margin: 0;
  font-family: 'Readex Pro';
}

/* NavBar */
.nav {
  position: fixed;
  top: 0;
  width: 100%;
  padding: 20px;
  height: 30px;
  z-index: 1;
  transition-timing-function: ease-in;
  transition: all 0.5s;
}



.nav-content {
  display: flex;
  justify-content: space-between;
}

.nav-black {
  background-color: #111;
}

.navbar-logo {
  position: fixed;
  top: 10px;
  left: 0px;
  width: 80px;
  object-fit: contain;
  padding-left: 20px;
  cursor: pointer;
}

.navbar-avatar {
  position: fixed;
  right: 20px;
  width: 30px;
  cursor: pointer;
}

/* Banner */
.banner {
  height: 448px;
  position: relative;
  object-fit: contain;
  color: white;
}

.banner-content {
  margin-left: 30px;
  padding-top: 140px;
  height: 190px;
}

.banner-title {
  font-size: 3rem;
  font-weight: 800;
  padding-bottom: 0.3rem;
}

.banner-description {
  width: 45rem;
  line-height: 1.3;
  padding-top: 1rem;
  font-size: 0.8rem;
  max-width: 360px;
  height: 80px;
}

.banner-fadeBottom {
  height: 7.4rem;
  background-image: linear-gradient(
    100deg,
    transparent,
    rgba(37, 37, 37, 0.61),
    #111
  );
}

.banner-button {
  cursor: pointer;
  color: #fff;
  outline: none;
  border: none;
  font-weight: 700;
  border-radius: 0.2vw;
  margin-right: 1rem;
  background-color: rgba(51, 51, 51, 0.5);
  padding: 0.5rem 2rem;
}

.banner-button:hover {
  color: #000;
  background-color: #e6e6e6;
  transition: all 0.2s;
}

.app {
  background-color: #111;
}

/* Row */
.row {
  color: #fff;
  margin-left: 20px;
}

.row-posters {
  display: flex;
  overflow-y: hidden;
  overflow-x: scroll;
  padding: 20px;
}

.row-posters::-webkit-scrollbar {
  display: none;
}

.row-poster {
  max-height: 100px;
  object-fit: contain;
  margin-right: 10px;
  width: 100%;
  transition: transform 450ms;
}

.row-poster:hover {
  transform: scale(1.08);
  opacity: 1;
}

.row-posterL {
  max-height: 250px;
}

/* LoginScreen */
.loginScreen {
  position: relative;
  height: 100%;
  background: url('https://assets.nflxext.com/ffe/siteui/vlv3/79fe83d4-7ef6-4181-9439-46db72599559/bd05b4ed-7e37-4be9-85c8-078f067bd150/IN-en-20221017-popsignuptwoweeks-perspective_alpha_website_medium.jpg') center no-repeat;
  background-size: cover;
}

.login-logo {
  position: fixed;
  left: 0;
  width: 150px;
  object-fit: contain;
  padding-left: 20px;
}

.login-btn {
  position: fixed;
  right: 20px;
  top: 20px;
  padding: 10px 20px;
  font-size: 1rem;
  color: #fff;
  background-color: #e50914;
  font-weight: 600;
  border: none;
  cursor: pointer;
}

.login-body {
  position: absolute;
  z-index: 1;
  top: 30%;
  color: #fff;
  padding: 20px;
  text-align: center;
  /* centering a absolute element */
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
}

.login-gradient {
  width: 100%;
  z-index: 1;
  height: 100vh;
  background: rgba(0, 0, 0, 0.4);
  background-image: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.8) 0,
    rgba(0, 0, 0, 0) 60%,
    rgba(0, 0, 0, 0.8) 100%
  );
}

.login-body > h1 {
  font-size: 3.125rem;
  margin-bottom: 20px;
}

.login-body > h2 {
  font-size: 2rem;
  font-weight: 400;
  margin-bottom: 30px;
}

.login-body > h3 {
  font-size: 1.3rem;
  font-weight: 400;
}

.login-input > form > input {
  padding: 10px;
  outline-width: 0;
  height: 30px;
  width: 30%;
  border: none;
  max-width: 600px;
}

.login-getStarted {
  padding: 15px 20px;
  font-size: 1rem;
  color: #fff;
  background-color: #e50914;
  border: none;
  font-weight: 600;
  cursor: pointer;
}

.login-input {
  margin: 20px;
}


/* Sign-Up Screen */
.signUp{
  max-width: 300px;
  padding: 70px;
  margin-left: auto;
  margin-right: auto;
  background: rgba(0, 0, 0, 0.85);
}

.signUp > form {
  display: grid;
}

.signUp > form > h1 {
  text-align: left;
  margin-bottom: 25px;
}

.signUp > form > input {
  padding: 5px 15px;
  outline-width: 0;
  height: 40px;
  margin-bottom: 14px;
  border-radius: 5px;
  border: none;
}

.signUp > form > button {
  padding: 16px 20px;
  font-size: 1rem;
  color: #fff;
  border-radius: 5px;
  background-color: #e50914;
  border: none;
  font-weight: 600;
  cursor: pointer;
  margin-top: 20px;
}


.signUp > form > h4 {
  text-align: left;
  margin-top: 30px;
}

.signUp-gray {
  color: gray;
}

.signUp-link:hover {
  cursor: pointer;
  text-decoration: underline;
}

/* Profile Screen */
.profileScreen {
  height: 100vh;
  color: #fff;
}

.profile-body {
  display: flex;
  flex-direction: column;
  width: 50%;
  margin-left: auto;
  margin-right: auto;
  padding-top: 8%;
  max-width: 800px;
}

.profile-body > h1 {
  font-size: 60px;
  font-weight: 400;
  border-bottom: 1px solid #282c2d;
  margin-bottom: 20px;
}

.profile-info {
  display: flex;
}

.profile-info > img {
  height: 100px;
}

.profile-details {
  color: #fff;
  margin-left: 25px;
  flex: 1;
}

.profile-details > h2 {
  background-color: gray;
  padding: 15px;
  font-size: 15px;
  padding-left: 20px;
}

.profile-signOut {
  padding: 10px 20px;
  font-size: 1rem;
  margin-top: 5%;
  width: 100%;
  color: #fff;
  background-color: #e50914;
  font-weight: 600;
  border: none;
  cursor: pointer;
}

.profile-plan {
  margin-top: 20px;
}

.profile-plan > h3 {
  border-bottom: 1px solid #282c2d;
  padding-bottom: 10px;
}

@media(max-width: 900px) {
  .profileScreen {
    height: 100vh;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .profile-body > h1 {
    font-size: 1.9rem;
    font-weight: 400;
    border-bottom: 1px solid #282c2d;
    margin-bottom: 20px;
  }

  .profile-info > img {
    height: 50px;
    display: none;
  }

  .login-body > h1 {
    font-size: 2.5rem;
    margin-bottom: 20px;
  }

  .login-body > h2 {
    font-size: 1.5rem;
    font-weight: 400;
    margin-bottom: 30px;
  }

  .login-body > h3 {
    font-size: 0.9rem;
    font-weight: 400;
  }

  .login-input > form {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
  }

  .login-input > form > input {
    width: 80%;
  }

  .login-getStarted{
    margin-top: 10px;
  }

}

The Final Project

final

That's pretty much up to it. Let's wrap it up with the deployment.

Deployment

We're going to host our webapp in firebase itself with a few commands.

npm i -g firebase-tools
npm run build
firebase login
firebase init
firebase deploy

The steps are to generate build first, then login using your google account to the firebase and init the firebase by choosing already existing project (here, netflix-clone) and deploy.

Choose "build" as your firebase public folder.

Code ⚡

Live 🚀