DEV Community

Cover image for Building Netflix Clone with NextJs 13.4: Part 2
Abhirup Kumar Bhowmick
Abhirup Kumar Bhowmick

Posted on

Building Netflix Clone with NextJs 13.4: Part 2

Netflix Clone

Github Link: https://github.com/abhirupkumar/Netflix-Clone

Project Link: https://netflix-akb.vercel.app

Create a hooks folder in the root directory. The hooks folder will have two files, useAuth.tsx and useList.tsx. useAuth.tsx is for checking whether the user is authenticated and subscribed or not.

  import {
        createUserWithEmailAndPassword,
        onAuthStateChanged,
        signInWithEmailAndPassword,
        signOut,
        User,
      } from 'firebase/auth';
      import { useRouter } from 'next/navigation';
      import { createContext, useContext, useEffect, useMemo, useState } from 'react';
      import { auth, db } from '../firebase';
    import { doc, getDoc } from 'firebase/firestore';

    interface Props{
      id: string;
      name: string;
      amount: number;
      expiry: Date;
    }

      interface IAuth {
        user: User | null
        signUp: (email: string, password: string) => Promise<void>
        signIn: (email: string, password: string) => Promise<void>
        logout: () => Promise<void>
        planDetails: Props | null
        error: string | null
        loading: boolean
      }

      const AuthContext = createContext<IAuth>({
        user: null,
        signUp: async () => {},
        signIn: async () => {},
        logout: async () => {},
        planDetails: null,
        error: null,
        loading: false,
      })

      interface AuthProviderProps {
        children: React.ReactNode
      }

      export const AuthProvider = ({ children }: AuthProviderProps) => {
        const [loading, setLoading] = useState(false)
        const [user, setUser] = useState<User | null>(null)
        const [error, setError] = useState(null)
        const [planDetails, setPlanDetails] = useState<Props | null>(null)
        const [initialLoading, setInitialLoading] = useState(true)
        const router = useRouter()

        // Persisting the user
        useEffect(
          () =>
            onAuthStateChanged(auth, (user) => {
              if (user) {
                // Logged in...
                setUser(user)
                fetchSubs(user);
                setLoading(false)
              } else {
                // Not logged in...
                setUser(null)
                setLoading(false)
                router.push('/login')
              }

              setInitialLoading(false)
            }),
          [auth])

          useEffect(() => {
            if(error!=null){
              setLoading(false)
              setError(null);
            }
          }, [error])

        const fetchSubs = async (userD: User) : Promise<void> => {
          const isSub = await isSubscribed(userD);
        }

        const signUp = async (email: string, password: string) => {
          setLoading(true)

          await createUserWithEmailAndPassword(auth, email, password)
            .then((userCredential) => {
              setUser(userCredential.user)
              router.push('/')
              setLoading(false)
            })
            .catch((err) => {
              alert(err.message)
              setError(err.message)
            })
            .finally(() => {
              setLoading(false)})
        }

        const signIn = async (email: string, password: string) => {
          setLoading(true)

          await signInWithEmailAndPassword(auth, email, password)
            .then((userCredential) => {
              setUser(userCredential.user)
              router.push('/')
              setLoading(false)
            })
            .catch((err) => {
              alert(err.message)
              setError(err.message)
            })
            .finally(() => {
              setLoading(false)})
        }

        const isSubscribed = async (user: User | null) : Promise<boolean | null> => {
          if(user == null) return null;
          const userRef =  doc(db, "subscriptions", user.uid);
          const docSnap = await getDoc(userRef);
          if(!docSnap.exists()) return false;
          else{
            const data = docSnap.data();
            const newdate = new Date().toISOString()
            const expiryDate = data.subscriptionExpiryDate
            if(newdate > expiryDate){
              return false;
            }
            else{
              setPlanDetails({id: data.subscriptionPlanId, name: data.subscriptionPlan, amount: data.subscriptionAmount, expiry: expiryDate})
              return true;
            }
          }
        }

        const logout = async () => {
          setLoading(true)

          signOut(auth)
            .then(() => {
              setUser(null)
            })
            .catch((err) => {
              alert(err.message)
              setError(err.message)
            })
            .finally(() => {
              setLoading(false)})
        }

        const memoedValue = useMemo(
          () => ({
            user,
            signUp,
            signIn,
            logout,
            planDetails,
            loading,
            error,
          }),
          [user, loading]
        )

        return (
          <AuthContext.Provider value={memoedValue}>
            {!initialLoading && children}
          </AuthContext.Provider>
        )
      }

      export default function useAuth() {
        return useContext(AuthContext)
      }
Enter fullscreen mode Exit fullscreen mode

In useList.tsx, we add all the favourite movies to the user’s list.

    import { collection, DocumentData, onSnapshot } from 'firebase/firestore'
    import { useEffect, useState } from 'react'
    import { db } from '../firebase'
    import { Movie } from '../typings'

    function useList(uid: string | undefined) {
      const [list, setList] = useState<Movie[] | DocumentData[]>([])

      useEffect(() => {
        if (!uid) return

        return onSnapshot(
          collection(db, 'customers', uid, 'myList'),
          (snapshot) => {
            setList(
              snapshot.docs.map((doc) => ({
                id: doc.id,
                ...doc.data(),
              }))
            )
          }
        )
      }, [db, uid])

      return list
    }

    export default useList
Enter fullscreen mode Exit fullscreen mode

Now change the App.tsx page as we need to AuthContext for authentication.

    "use client";

    import { AuthProvider } from '@/hooks/useAuth';
    import React from 'react'
    import { RecoilRoot } from 'recoil'

    const App = ({
        children,
      }: {
        children: React.ReactNode
      }) => {
      return (
        <html lang="en">
          <body>
            <RecoilRoot>
            <AuthProvider>
              {children}
              </AuthProvider>
            </RecoilRoot>
          </body>
        </html>
      )
    }

    export default App
Enter fullscreen mode Exit fullscreen mode

Lets create all the Apis required in this project. So, create a api folder in app folder. Within the api folder create create-razorpay-order folder and within it create route.ts file.

    import { NextRequest, NextResponse } from 'next/server';
    import razorpay from 'razorpay';

    export async function POST(req: NextRequest) {
        try {
            const { amount } = await req.json();
            const razorpayInstance = new razorpay({
              key_id: <string>process.env.RAZORPAY_KEY,
              key_secret: <string>process.env.RAZORPAY_SECRET,
            });

            const payment_capture = 1;
            const options = {
              amount: (amount * 100).toString(), // Amount in paise or smallest currency unit
              currency: 'INR',
              receipt: 'order_receipt',
              payment_capture,
            };

            const order = await razorpayInstance.orders.create(options);
            return NextResponse.json({ id: order.id });
        } catch (error) {
            return NextResponse.json({ error: 'Failed to create Razorpay order' });
        }
    };
Enter fullscreen mode Exit fullscreen mode

Agin within the api folder create create-razorpay-subscription folder and within it create route.ts file.

    import { NextRequest, NextResponse } from 'next/server';
    import razorpay from 'razorpay';

    export async function POST(req: NextRequest) {
        try {
          const { orderId, planType, planId, email, userId, amount } = await req.json();
            const razorpayInstance = new razorpay({
              key_id: <string>process.env.RAZORPAY_KEY,
              key_secret: <string>process.env.RAZORPAY_SECRET,
            });

            razorpayInstance.subscriptions.create({
              plan_id: planId,
              total_count: 1,
              quantity: 1,
              customer_notify: 1,
              addons: [
                {
                  item: {
                    name: userId,
                    amount: amount*100,
                    currency: "INR"
                  }
                }
              ],
              notes: {
                planType,
                orderId,
              },
              notify_info: {
                notify_email: email,
              }
            })

            return NextResponse.json({ success: true });
        } catch (error) {
            return NextResponse.json({ success: false, error: 'Payment verification failed' });
        }
    };
Enter fullscreen mode Exit fullscreen mode

Create a folder components in root directory. Create Banner.tsx file in components folder.

    "use client";

    import { modalState, movieState } from '@/atoms/modalAtom';
    import { baseUrl } from '@/constants/movie';
    import { Movie } from '@/typings'
    import Image from 'next/image'
    import React, { useEffect, useState } from 'react'
    import { FaPlay } from 'react-icons/fa'
    import { HiOutlineInformationCircle } from 'react-icons/hi'
    import { useRecoilState } from 'recoil';

    interface Props {
      netflixOriginals: Movie[]
    }

    const Banner = ({ netflixOriginals } : Props) => {
      const [movie, setMovie] = useState<Movie | null>(null)
      const [showModal, setShowModal] = useRecoilState(modalState)
      const [currentMovie, setCurrentMovie] = useRecoilState(movieState)

      const truncate = (str: string) => {
        return str.length > 250 ? str.substring(0, 250) + "..." : str;
      }

      useEffect(() => {
        setMovie(
          netflixOriginals[Math.floor(Math.random() * netflixOriginals.length)]
        );
      }, [netflixOriginals])

      return (
        <div className="flex flex-col space-y-2 pt-16 md:space-y-4 lg:h-[65vh] lg:justify-end">
          <div className="absolute top-0 left-0 -z-10 h-[95vh] w-screen">
            {(movie?.backdrop_path || movie?.poster_path) && <Image
              src={`${baseUrl}${movie?.backdrop_path || movie?.poster_path}`}
              fill={true}
              object-fit="cover"
              alt="banner-image"
              priority={true}
            />}
          </div>

          <h1 className="text-2xl font-bold md:text-4xl lg:text-6xl">
            {movie?.title || movie?.name || movie?.original_name}
          </h1>
          <p className="max-w-xs text-xs text-shadow-md md:max-w-lg md:text-lg lg:max-w-xl lg:text-lg">
            {movie?.overview && truncate(movie?.overview)}
          </p>

          <div className="flex space-x-3">
            <button className="bannerButton bg-white text-black" onClick={() => {
                setCurrentMovie(movie)
                setShowModal(true)
              }}>
              <FaPlay className="h-4 w-4 text-black md:h-7 md:w-7" onClick={() => {
                setCurrentMovie(movie)
                setShowModal(true)
              }} /> Play
            </button>
            <button
              className="bannerButton bg-[gray]/70"
              onClick={() => {
                setCurrentMovie(movie)
                setShowModal(true)
              }}
            >
              More Info <HiOutlineInformationCircle className="h-5 w-5 md:h-8 md:w-8" />
            </button>
          </div>
        </div>
      )
    }

    export default Banner
Enter fullscreen mode Exit fullscreen mode

Create BasicMenu.tsx file in components folder.

    "use client";

    import Button from '@mui/material/Button'
    import Menu from '@mui/material/Menu'
    import MenuItem from '@mui/material/MenuItem'
    import { useState } from 'react'

    export default function BasicMenu() {
      const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
      const open = Boolean(anchorEl)

      const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
        setAnchorEl(event.currentTarget)
      }

      const handleClose = () => {
        setAnchorEl(null)
      }

      return (
        <div className="md:!hidden">
          <Button
            id="basic-button"
            aria-controls={open ? 'basic-menu' : undefined}
            aria-haspopup="true"
            aria-expanded={open ? 'true' : undefined}
            onClick={handleClick}
            className="!capitalize !text-white"
          >
            Browse
          </Button>
          <Menu
            id="basic-menu"
            anchorEl={anchorEl}
            open={open}
            onClose={handleClose}
            className="menu"
            MenuListProps={{
              'aria-labelledby': 'basic-button',
            }}
          >
            <MenuItem onClick={handleClose}>Home</MenuItem>
            <MenuItem onClick={handleClose}>TV Shows</MenuItem>
            <MenuItem onClick={handleClose}>Movies</MenuItem>
            <MenuItem onClick={handleClose}>New & Popular</MenuItem>
            <MenuItem onClick={handleClose}>My List</MenuItem>
          </Menu>
        </div>
      )
    }
Enter fullscreen mode Exit fullscreen mode

Now lets create Header.tsx file in components folder.

    "use client";

    import Link from 'next/link'
    import { useEffect, useState } from 'react'
    import useAuth from '@/hooks/useAuth'
    import { BiBell, BiSearch } from 'react-icons/bi'
    import BasicMenu from './BasicMenu';

    function Header() {
      const [isScrolled, setIsScrolled] = useState(false)
      const { logout } = useAuth()

      useEffect(() => {
        const handleScroll = () => {
          if (window.scrollY > 0) {
            setIsScrolled(true)
          } else {
            setIsScrolled(false)
          }
        }

        window.addEventListener('scroll', handleScroll)

        return () => {
          window.removeEventListener('scroll', handleScroll)
        }
      }, [])

      return (
        <header className={`${isScrolled && 'bg-[#141414]'}`}>
          <div className="flex items-center space-x-2 md:space-x-10">
            <img
              src="https://rb.gy/ulxxee"
              width={100}
              height={100}
              className="cursor-pointer object-contain"
            />

            <BasicMenu />

            <ul className="hidden space-x-4 md:flex">
              <li className="headerLink">Home</li>
              <li className="headerLink">TV Shows</li>
              <li className="headerLink">Movies</li>
              <li className="headerLink">New & Popular</li>
              <li className="headerLink">My List</li>
            </ul>
          </div>

          <div className="flex items-center space-x-4 text-sm font-light">
            <BiSearch className="hidden h-6 w-6 sm:inline" />
            <p className="hidden lg:inline">Kids</p>
            <BiBell className="h-6 w-6" />
            <Link href="/account">
              <img
                src="https://rb.gy/g1pwyx"
                alt="image"
                className="cursor-pointer rounded"
              />
            </Link>
          </div>
        </header>
      )
    }

    export default Header
Enter fullscreen mode Exit fullscreen mode

Create Loader.tsx file in components folder.

    function Loader() {
        return (
          <div className="lds-ripple">
            <div></div>
            <div></div>
          </div>
        )
    }

    export default Loader;
Enter fullscreen mode Exit fullscreen mode

Create Membership.tsx file in components folder.

    "use client";

    import React, { useState } from 'react'
    import useAuth from '../hooks/useAuth'
    import Loader from './Loader'
    import { useRouter } from 'next/navigation';
    import { deleteDoc, doc } from 'firebase/firestore';
    import { db } from '@/firebase';

    function Membership () {
        const { user } = useAuth();
        const [loading, setLoading] = useState<boolean>(false)
        const router = useRouter()

        const handleClick = async () : Promise<void> => {
            setLoading(true);
            if(user == null){
                setLoading(false);
                return;
            }
            const docRef = doc(db, "subscriptions", user.uid);
            await deleteDoc(docRef);
            router.push('/');
        }

        return (
            <div className="mt-6 grid grid-cols-1 gap-x-4 border px-4 md:grid-cols-4 md:border-x-0 md:border-t md:border-b-0 md:px-0">
                <div className="space-y-2 py-4">
                    <h4 className="text-lg text-[gray]">Membership & Billing</h4>
                    <button
                      disabled={loading}
                    className="h-10 w-3/5 whitespace-nowrap bg-gray-300 py-2 text-sm font-medium text-black shadow-md hover:bg-gray-200 md:w-4/5"
                      onClick={handleClick}
                    >
                    {loading ? (
                        <Loader />
                    ) : (
                        'Cancel Membership'
                    )}
                    </button>
                </div>

                <div className="col-span-3">
                    <div className="flex flex-col justify-between border-b border-white/10 py-4 md:flex-row">
                    <div>
                        <p className="font-medium">{user?.email}</p>
                    </div>
                    <div className="md:text-right">
                        <p className="membershipLink">Change email</p>
                        <p className="membershipLink">Change password</p>
                    </div>
                    </div>

                    <div className="flex flex-col justify-between pt-4 pb-4 md:flex-row md:pb-0">
                        <div className="flex flex-1 flex-col md:text-right">
                            <p className="membershipLink">Manage payment info</p>
                            <p className="membershipLink">Add backup payment method</p>
                            <p className="membershipLink">Billing Details</p>
                            <p className="membershipLink">Change billing day</p>
                        </div>
                    </div>
                </div>
          </div>
        );
    }

    export default Membership;
Enter fullscreen mode Exit fullscreen mode

Create Model.tsx file in components folder.

    "use client";

      import MuiModal from '@mui/material/Modal'
      import {
        collection,
        deleteDoc,
        doc,
        DocumentData,
        onSnapshot,
        setDoc,
      } from 'firebase/firestore'
      import { useEffect, useState } from 'react'
      import toast, { Toaster } from 'react-hot-toast'
      import { FaPlay } from 'react-icons/fa'
      import ReactPlayer from 'react-player/lazy'
      import { useRecoilState } from 'recoil'
      import { modalState, movieState } from '../atoms/modalAtom'
      import { db } from '../firebase'
      import useAuth from '../hooks/useAuth'
      import { Element, Genre, Movie } from '../typings'
    import { AiOutlineCheck, AiOutlinePlus } from 'react-icons/ai';
    import { BsHandThumbsUp } from 'react-icons/bs';
    import { HiOutlineVolumeOff, HiOutlineVolumeUp } from 'react-icons/hi';
    import { RxCross1 } from 'react-icons/rx';

      function Modal() {
        const [showModal, setShowModal] = useRecoilState(modalState)
        const [movie, setMovie] = useRecoilState(movieState)
        const [trailer, setTrailer] = useState('')
        const [genres, setGenres] = useState<Genre[]>([])
        const [muted, setMuted] = useState(true)
        const { user } = useAuth()
        const [addedToList, setAddedToList] = useState(false)
        const [movies, setMovies] = useState<DocumentData[] | Movie[]>([])

        const toastStyle = {
          background: 'white',
          color: 'black',
          fontWeight: 'bold',
          fontSize: '16px',
          padding: '15px',
          borderRadius: '9999px',
          maxWidth: '1000px',
        }

        useEffect(() => {
          if (!movie) return

          async function fetchMovie() {
            const data = await fetch(
              `https://api.themoviedb.org/3/${
                movie?.media_type === 'tv' ? 'tv' : 'movie'
              }/${movie?.id}?api_key=${
                process.env.NEXT_PUBLIC_API_KEY
              }&language=en-IN&append_to_response=videos`
            )
              .then((response) => response.json())
              .catch((err) => console.log(err.message))

            if (data?.videos) {
              const index = data.videos.results.findIndex(
                (element: Element) => element.type === 'Trailer'
              )
              setTrailer(data.videos?.results[index]?.key)
            }
            if (data?.genres) {
              setGenres(data.genres)
            }
          }

          fetchMovie()
        }, [movie])

        // Find all the movies in the user's list
        useEffect(() => {
          if (user) {
            return onSnapshot(
              collection(db, 'customers', user.uid, 'myList'),
              (snapshot) => setMovies(snapshot.docs)
            )
          }
        }, [db, movie?.id])

        // Check if the movie is already in the user's list
        useEffect(
          () =>
            setAddedToList(
              movies.findIndex((result) => result.data().id === movie?.id) !== -1
            ),
          [movies]
        )

        const handleList = async () => {
          if (addedToList) {
            await deleteDoc(
              doc(db, 'customers', user!.uid, 'myList', movie?.id.toString()!)
            )

            toast(
              `${movie?.title || movie?.original_name} has been removed from My List`,
              {
                duration: 8000,
                style: toastStyle,
              }
            )
          } else {
            await setDoc(
              doc(db, 'customers', user!.uid, 'myList', movie?.id.toString()!),
              { ...movie }
            )

            toast(
              `${movie?.title || movie?.original_name} has been added to My List`,
              {
                duration: 8000,
                style: toastStyle,
              }
            )
          }
        }

        const handleClose = () => {
          setShowModal(false)
        }

        console.log(trailer)

        return (
          <MuiModal
            open={showModal}
            onClose={handleClose}
            className="fixex !top-7 left-0 right-0 z-50 mx-auto w-full max-w-5xl overflow-hidden overflow-y-scroll rounded-md scrollbar-hide"
          >
            <>
              <Toaster position="bottom-center" />
              <button
                onClick={handleClose}
                className="modalButton absolute right-5 top-5 !z-40 h-9 w-9 border-none bg-[#181818] hover:bg-[#181818]"
              >
                <RxCross1 className="h-6 w-6" />
              </button>

              <div className="relative pt-[56.25%]">
                <ReactPlayer
                  url={`https://www.youtube.com/watch?v=${trailer}`}
                  width="100%"
                  height="100%"
                  style={{ position: 'absolute', top: '0', left: '0' }}
                  playing
                  muted={muted}
                />
                <div className="absolute bottom-10 flex w-full items-center justify-between px-10">
                  <div className="flex space-x-2">
                    <button className="flex items-center gap-x-2 rounded bg-white px-8 text-xl font-bold text-black transition hover:bg-[#e6e6e6]">
                      <FaPlay className="h-7 w-7 text-black" />
                      Play
                    </button>

                    <button className="modalButton" onClick={handleList}>
                      {addedToList ? (
                        <AiOutlineCheck className="h-7 w-7" />
                      ) : (
                        <AiOutlinePlus className="h-7 w-7" />
                      )}
                    </button>

                    <button className="modalButton">
                      <BsHandThumbsUp className="h-7 w-7" />
                    </button>
                  </div>
                  <button className="modalButton" onClick={() => setMuted(!muted)}>
                    {muted ? (
                      <HiOutlineVolumeOff className="h-6 w-6" />
                    ) : (
                      <HiOutlineVolumeUp className="h-6 w-6" />
                    )}
                  </button>
                </div>
              </div>

              <div className="flex space-x-16 rounded-b-md bg-[#181818] px-10 py-8">
                <div className="space-y-6 text-lg">
                  <div className="flex items-center space-x-2 text-sm">
                    <p className="font-semibold text-green-400">
                      {movie!.vote_average * 10}% Match
                    </p>
                    <p className="font-light">
                      {movie?.release_date || movie?.first_air_date}
                    </p>
                    <div className="flex h-4 items-center justify-center rounded border border-white/40 px-1.5 text-xs">
                      HD
                    </div>
                  </div>

                  <div className="flex flex-col gap-x-10 gap-y-4 font-light md:flex-row">
                    <p className="w-5/6">{movie?.overview}</p>
                    <div className="flex flex-col space-y-3 text-sm">
                      <div>
                        <span className="text-[gray]">Genres: </span>
                        {genres.map((genre) => genre.name).join(', ')}
                      </div>

                      <div>
                        <span className="text-[gray]">Original language: </span>
                        {movie?.original_language}
                      </div>

                      <div>
                        <span className="text-[gray]">Total votes: </span>
                        {movie?.vote_count}
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </>
          </MuiModal>
        )
      }

      export default Modal
Enter fullscreen mode Exit fullscreen mode

Create Page.tsx file in components folder. This will be our home page whcih will contain all the movies data.

    "use client";

    import React, { useEffect, useState } from 'react'
    import Header from './Header'
    import Banner from './Banner'
    import Row from './Row'
    import { Movie } from '@/typings'
    import useAuth from '@/hooks/useAuth'
    import { useRecoilValue } from 'recoil';
    import { modalState, movieState } from '@/atoms/modalAtom';
    import Modal from './Modal';
    import Plans from './Plan';
    import Loader from './Loader';
    import useList from '@/hooks/useList';
    import { doc, getDoc } from 'firebase/firestore';
    import { db } from '@/firebase';
    import { User } from 'firebase/auth';

    interface Props {
        netflixOriginals: Movie[]
        trendingNow: Movie[]
        topRated: Movie[]
        actionMovies: Movie[]
        comedyMovies: Movie[]
        horrorMovies: Movie[]
        romanceMovies: Movie[]
        documentaries: Movie[]
      }

    const Page = ({netflixOriginals,
        actionMovies,
        comedyMovies,
        documentaries,
        horrorMovies,
        romanceMovies,
        topRated,
        trendingNow,
    } : Props) => {

        const { loading, user } = useAuth();
        const [subscription, setSubscription] = useState<boolean | null>(null)
        const showModal = useRecoilValue(modalState);
        const movie = useRecoilValue(movieState);
        const list = useList(user?.uid)
        useEffect(() => {
          if(user != null)
            fetchSubscription()
        }, [user])

        const fetchSubscription = async () : Promise<void> => {
          const isSub = await isSubscribed(user);
          setSubscription(isSub)

          console.log(subscription)
        }

        const isSubscribed = async (user: User | null) : Promise<boolean | null> => {
          if(user == null) return null;
          const userRef =  doc(db, "subscriptions", user.uid);
          const docSnap = await getDoc(userRef);
          if(!docSnap.exists()) return false;
          else{
            const data = docSnap.data();
            const newdate = new Date().toISOString()
            const expiryDate = data.subscriptionExpiryDate
            if(newdate > expiryDate){
              return false
            }
            else{
              return true;
            }
          }
        }

        if(user == null) return <Loader />;
        if (loading || subscription === null) return <Loader />;

        if (!subscription) return <Plans />;
        console.log("subscription: ", subscription)

      return (
        <div className={`relative h-screen bg-gradient-to-b lg:h-[140vh] ${
            showModal && '!h-screen overflow-hidden'
          }`}>
             <Header />
             <main className="relative pl-4 pb-24 lg:space-y-24 lg:pl-16">
              <Banner netflixOriginals={netflixOriginals} />
              <section className="md:space-y-24">
              <Row title="Trending Now" movies={trendingNow} />
              <Row title="Top Rated" movies={topRated} />
              <Row title="Action Thrillers" movies={actionMovies} />
              {list.length > 0 && <Row title="My List" movies={list} />}
              <Row title="Comedies" movies={comedyMovies} />
              <Row title="Scary Movies" movies={horrorMovies} />
              <Row title="Romance Movies" movies={romanceMovies} />
              <Row title="Documentaries" movies={documentaries} />
              </section>
             </main>
             {showModal && <Modal />}
          </div>
      )
    }

    export default Page
Enter fullscreen mode Exit fullscreen mode

Now create PaymentForm.tsx in components folder. This folder is very important for for payment and subcription.

    'use client';

    import React, { useEffect, useState } from 'react';
    import { Plan } from '@/typings';
    import { User } from 'firebase/auth';
    import { addDoc, collection, doc, getDoc, getDocs, query, setDoc, updateDoc } from 'firebase/firestore';
    import { db } from '@/firebase';
    import Razorpay from 'razorpay';
    import Loader from './Loader';
    import { useRouter } from 'next/navigation';
    import useAuth from '@/hooks/useAuth';

    declare global {
        interface Window {
          Razorpay: any;
        }
      }

    interface Props {
      selectedPlan: Plan;
      previousPlan: Plan | null;
      user: User | null,
      handleClose: () => void;
    }

    const initializeRazorpay = async (): Promise<any> => {
        return new Promise((resolve) => {
          const script = document.createElement("script");
          script.src = "https://checkout.razorpay.com/v1/checkout.js";

          script.onload = () => {
            resolve(true);
          };
          script.onerror = () => {
            resolve(false);
          };

          document.body.appendChild(script);
        });
      }

    const PaymentForm: React.FC<Props> = ({ selectedPlan, previousPlan, user, handleClose }) => {
      const [paymentCompleted, setPaymentCompleted] = useState(false);
      const [amount, setAmount] = useState(selectedPlan.amount);
      const [loading, setLoading] = useState(false);
      const router = useRouter()

      useEffect(() => {
        if(previousPlan != null) setAmount(selectedPlan.amount - previousPlan.amount);
      }, [])

      const handlePayment = async (e: any): Promise<void> => {
        setLoading(true)
        e.preventDefault();
        const currentUser = user;
        if(currentUser == null) return;
        const orderResponse = await createRazorpayOrder(amount);
        const orderId = orderResponse;

          try {
            const res = await initializeRazorpay();
            if (!res) {
                alert("Your are offline.... Razorpay SDK Failed to load");
                return;
              }

              const options = {
                key: process.env.RAZORPAY_KEY,
                amount: amount, // Amount in paise or smallest currency unit
                currency: 'INR',
                name: 'Netflix',
                description: 'Subscription Payment',
                order_id: orderId,
                image: "@/app/favicon.ico",
                handler: async function (response: any) {
                  // Verify the payment with Razorpay API
                    // Payment successful, update user's subscription status in Firestore
                  const expiryDate = calculateExpiryDate();

                  await updateSubscriptionStatus(currentUser.uid, selectedPlan, orderId, expiryDate)
                  setPaymentCompleted(true);
                  location.reload();
                },
                prefill: {
                  email: currentUser.email,
                },
                notes: {
                  planName: selectedPlan.name,
                  planId: selectedPlan.id,
                  userId: currentUser.uid,
                },
                theme: {
                  color: '#F37254',
                },
              }

            const razorpay = new window.Razorpay(options);
            razorpay.open();
          }
        catch (error) {
            // Handle errors during payment process
            console.log('Payment error:', error);
            handleClose(); // Close the payment form
          }
          setLoading(false);
      };

      const createRazorpayOrder = async (amount: number): Promise<any> => {
        const response = await fetch('/api/create-razorpay-order', {
            method: 'POST',
            body: JSON.stringify({ amount }),
            headers: {
              'Content-Type': 'application/json',
            },
          });
          const data = await response.json();
          return data.id;
      };

      const calculateExpiryDate = (): string => {
        const currentDate = new Date();
        const expiryDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
        return expiryDate.toISOString();
      };

      const updateSubscriptionStatus = async (
        userId: string,
        plan: Plan,
        subscriptionId: string,
        expiryDate: string
      ): Promise<void> => {
        try {
        //   const userRef = doc(db, 'subscriptions', userId);
          const userRef =  doc(db, "subscriptions", userId);
          const docSnap = await getDoc(userRef);
          //find userId in firestore and if present then update doc otherwise addDoc


          if(docSnap.exists()){
            await updateDoc(doc(db, 'subscriptions', userId), {
                subscriptionPlan: plan.name,
                subscriptionPlanId: plan.id,
                subscriptionAmount: plan.amount,
                subscriptionExpiryDate: expiryDate,
                subscriptionId,
              });
          }
          else{
            await setDoc(userRef, {
                subscriptionPlan: plan.name,
                subscriptionPlanId: plan.id,
                subscriptionAmount: plan.amount,
                subscriptionExpiryDate: expiryDate,
                subscriptionId,
              })
          }
          console.log('Subscription status updated successfully');
          // Add any further logic or redirection after successful subscription update
        } catch (error) {
          // Handle errors during subscription status update
          console.log('Subscription status update error:', error);
        }
      };

      return (
        <div className="bg-gray-900 text-white p-8 rounded-lg shadow-lg pt-28">
          {paymentCompleted ? (
            <div>
              <h2 className="text-3xl mb-4">Payment Completed!</h2>
              <p className="text-xl">Thank you for your subscription.</p>
              <p className="text-xl">Just wait your would be redirected to home.</p>
            </div>
          ) : (
            <div>
              <h2 className="text-3xl mb-4">Payment Form</h2>
              <p className="text-xl mb-4">Selected Plan: {selectedPlan.name}</p>
              <p className="text-xl mb-4">Video Quality: {selectedPlan.videoQuality}</p>
              <p className="text-xl mb-4">Resolution: {selectedPlan.resolution}</p>
              <p className="text-xl mb-4">Description: {selectedPlan.description}</p>
              <p className="text-xl mb-4">Price: INR {selectedPlan.amount}.00</p>
              <p className="text-xl mb-4">Amount Payable: INR {amount}.00</p>
              {!loading ? <>
                <button
                className="bg-red-600 hover:bg-red-700 text-white px-6 py-3 rounded-lg mr-4"
                onClick={handlePayment}
              >
                Proceed to Pay
              </button>
              <button
                className="bg-gray-800 hover:bg-gray-900 text-white px-6 py-3 rounded-lg"
                onClick={handleClose}
              >
                Cancel
              </button>
              </> : <Loader />}
            </div>
          )}
          </div>
      );
    };

    export default PaymentForm;
Enter fullscreen mode Exit fullscreen mode

Create Plan.tsx file in components folder.

    "use client";

    import Link from 'next/link'
    import { useEffect, useState } from 'react'
    import useAuth from '../hooks/useAuth'
    import Loader from './Loader'
    import { AiOutlineCheck } from 'react-icons/ai'
    import Table from './Table';
    import { Plan } from '@/typings';
    import PaymentForm from './PaymentForm';
    import { doc, getDoc } from 'firebase/firestore';
    import { db } from '@/firebase';
    import plans from '@/utils/data'

    function Plans() {
      const { logout, user, planDetails } = useAuth()
      const [selectedPlan, setSelectedPlan] = useState<Plan>(plans[2])
      const [isBillingLoading, setBillingLoading] = useState(false)
      const [showPaymentForm, setShowPaymentForm] = useState(false);
      const [previousPlan, setPreviousPlan] = useState<Plan | null>(null)
      const [loading, setLoading] = useState<boolean>(false)

      useEffect(() => {
        setLoading(true)
        fetchSubscribes()
        setLoading(false)
      }, [planDetails])

      const fetchSubscribes = async (): Promise<void> => {
        if (!user) return;
        const userRef =  doc(db, "subscriptions", user.uid);
        const docSnap = await getDoc(userRef);
        if(docSnap.exists()){
          if(!docSnap.data().subscriptionPlanId) return;
            for(let i=0; i<plans.length; i++){
              if(plans[i].id == docSnap.data().subscriptionPlanId){
                setPreviousPlan(plans[i])
                setSelectedPlan(plans[i])
                break;
              }
            }
        }
      }

      const subscribeToPlan = async () => {
        if (!user) return

        setBillingLoading(true)
        setShowPaymentForm(true);
        setBillingLoading(false)
      }

      const handlePaymentFormClose = () => {
        setShowPaymentForm(false);
      };

      if(loading) return <Loader />

      return (
        <div>
          <header className="border-b border-white/10 bg-[#141414]">
            <Link href="/">
              <img
                src="https://rb.gy/ulxxee"
                alt="Netflix"
                width={150}
                height={90}
                className="cursor-pointer object-contain"
              />
            </Link>
            <button
              className="text-lg font-medium hover:underline"
              onClick={logout}
            >
              Sign Out
            </button>
          </header>
          {!showPaymentForm ?
          <main className="mx-auto max-w-5xl px-5 pt-28 pb-12 transition-all md:px-10">
            <h1 className="mb-3 text-3xl font-medium">
              Choose the plan that's right for you
            </h1>
            <ul>
              <li className="flex items-center gap-x-2 text-lg">
                <AiOutlineCheck className="h-7 w-7 text-[#E50914]" /> Watch all you want.
                Ad-free.
              </li>
              <li className="flex items-center gap-x-2 text-lg">
                <AiOutlineCheck className="h-7 w-7 text-[#E50914]" /> Recommendations
                just for you.
              </li>
              <li className="flex items-center gap-x-2 text-lg">
                <AiOutlineCheck className="h-7 w-7 text-[#E50914]" /> Upgrade or cancel
                your plan anytime.
              </li>
            </ul>

            <div className="mt-4 flex flex-col space-y-5">
              <div className="flex w-full items-center justify-center self-end md:w-3/5">
                {plans.map((item) => (
                    <button
                    key={item.id}
                    className={`planBox cursor-pointer ${
                        selectedPlan?.id === item.id ? 'opacity-100' : 'opacity-60'
                      }`}
                      onClick={() => setSelectedPlan(item)}
                  >
                    {item.name}
                    </button>
                ))}
              </div>

              <Table plans={plans} selectedPlan={selectedPlan}  />

              {(previousPlan ==null || previousPlan.amount < selectedPlan.amount) && <button
                disabled={!selectedPlan || isBillingLoading}
                className={`mx-auto w-11/12 rounded bg-[#E50914] py-4 text-xl shadow hover:bg-[#f6121d] md:w-[420px] ${
                  isBillingLoading && 'opacity-60'
                }`}
                onClick={subscribeToPlan}
              >
                {isBillingLoading ? (
                  <Loader />
                ) : (
                  'Subscribe'
                )}
              </button>}
            </div>
          </main>
          :
            <PaymentForm selectedPlan={selectedPlan} previousPlan={previousPlan} user={user} handleClose={handlePaymentFormClose} />
          }
        </div>
      )
    }

    export default Plans
Enter fullscreen mode Exit fullscreen mode

Create Row.tsx in components folder.

    "use client";

    import { AiOutlineLeft, AiOutlineRight } from 'react-icons/ai';
    import { DocumentData } from 'firebase/firestore'
    import { useRef, useState } from 'react'
    import { Movie } from '../typings'
    import Thumbnail from './Thumbnail'

    interface Props {
      title: string
      movies: Movie[] | DocumentData[]
    }

    function Row({ title, movies }: Props) {
      const rowRef = useRef<HTMLDivElement>(null)
      const [isMoved, setIsMoved] = useState(false)

      const handleClick = (direction: string) => {
        setIsMoved(true)

        if (rowRef.current) {
          const { scrollLeft, clientWidth } = rowRef.current;

          const scrollTo = direction === 'left' ? scrollLeft - clientWidth : scrollLeft + clientWidth;

          rowRef.current.scrollTo({ left: scrollTo, behavior: 'smooth' });
        }
      }

      return (
        <div className="h-40 space-y-0.5 md:space-y-2">
          <h2 className="w-56 cursor-pointer text-sm font-semibold text-[#e5e5e5] transition duration-200 hover:text-white md:text-2xl">
            {title}
          </h2>
          <div className="group relative md:-ml-2">
            <AiOutlineLeft
              className={`absolute top-0 bottom-0 left-2 z-40 m-auto h-9 w-9 cursor-pointer opacity-0 transition hover:scale-125 group-hover:opacity-100 ${
                !isMoved && 'hidden'
              }`}
              onClick={() => handleClick('left')}
            />

            <div
              ref={rowRef}
              className="flex items-center space-x-0.5 overflow-x-scroll no-scrollbar md:space-x-2.5 md:p-2"
            >
              {movies.map((movie) => (
                <Thumbnail key={movie.id} movie={movie} />
              ))}
            </div>

            <AiOutlineRight
              className={`absolute top-0 bottom-0 right-2 z-40 m-auto h-9 w-9 cursor-pointer opacity-0 transition hover:scale-125 group-hover:opacity-100`}
              onClick={() => handleClick('right')}
            />
          </div>
        </div>
      )
    }

    export default Row
Enter fullscreen mode Exit fullscreen mode

Create Table.tsx in components folder.

    import { Plan } from '@/typings'
    import { AiOutlineCheck } from 'react-icons/ai'

    interface Props {
        plans: Plan[]
        selectedPlan: Plan | null
    }

    function Table({ plans, selectedPlan }: Props) {
      return (
        <table>
          <tbody className="divide-y divide-[gray]">
            <tr className="tableRow">
              <td className="tableDataTitle">Monthly price</td>

              {plans.map((item) => (
                <td
                  key={item.id}
                  className={`tableDataFeature ${
                    selectedPlan?.id === item.id
                      ? 'text-[#e50914]'
                      : 'text-[gray]'
                  }`}
                >
                  INR {item.amount}
                </td>
              ))}
            </tr>

            <tr className="tableRow">
              <td className="tableDataTitle">Video quality</td>

              {plans.map((item) => (
                <td
                  key={item.id}
                  className={`tableDataFeature ${
                    selectedPlan?.id === item.id
                      ? 'text-[#e50914]'
                      : 'text-[gray]'
                  }`}
                >
                  {item.videoQuality}
                </td>
              ))}
            </tr>

            <tr className="tableRow">
              <td className="tableDataTitle">Resolution</td>
              {plans.map((item) => (
                <td
                  key={item.id}
                  className={`tableDataFeature ${
                    selectedPlan?.id === item.id
                      ? 'text-[#e50914]'
                      : 'text-[gray]'
                  }`}
                >
                  {item.resolution}
                </td>
              ))}
            </tr>

            <tr className="tableRow">
              <td className="tableDataTitle">
                Availability
              </td>
              {plans.map((item) => (
                <td
                  key={item.id}
                  className={`tableDataFeature ${
                    selectedPlan?.id === item.id
                      ? 'text-[#e50914]'
                      : 'text-[gray]'
                  }`}
                >
                  {item.description}
                </td>
              ))}
            </tr>
          </tbody>
        </table>
      )
    }

    export default Table
Enter fullscreen mode Exit fullscreen mode

Create Thumbnail.tsx file in components folder.

    "use client";

    import React from 'react';
    import { Movie } from '../typings';
    import Image from 'next/image';
    import { DocumentData } from 'firebase/firestore';
    import { useRecoilState } from 'recoil';
    import { modalState, movieState } from '@/atoms/modalAtom';

    interface Props {
        movie: Movie | DocumentData
      }

    const Thumbnail = ({ movie }: Props) => {

      const [showModal, setShowModal] = useRecoilState(modalState)
      const [currentMovie, setCurrentMovie] = useRecoilState(movieState)

      return (
        <div
          className="relative h-28 min-w-[180px] cursor-pointer transition duration-200 ease-out md:h-36 md:min-w-[260px] md:hover:scale-105"
          onClick={() => {
            setCurrentMovie(movie)
            setShowModal(true)
          }}
        >
          <Image
            src={`https://image.tmdb.org/t/p/w500${
              movie.backdrop_path || movie.poster_path
            }`}
            className="rounded-sm object-cover md:rounded"
            fill={true}
            alt={`thumbnail-${movie.id}`}
          />
        </div>
      )
    }

    export default Thumbnail
Enter fullscreen mode Exit fullscreen mode

Now, lets code the page.tsx file. In page.tsx we will just fetch the movies data and sent it to the Page component.

    import { fetchAllData } from '@/actions/actions'
    import { Movie } from '../typings'
    import Page from '@/components/Page';

    interface Props {
      netflixOriginals: Movie[]
      trendingNow: Movie[]
      topRated: Movie[]
      actionMovies: Movie[]
      comedyMovies: Movie[]
      horrorMovies: Movie[]
      romanceMovies: Movie[]
      documentaries: Movie[]
    }

    export default async function Home() {
      const {netflixOriginals,
        actionMovies,
        comedyMovies,
        documentaries,
        horrorMovies,
        romanceMovies,
        topRated,
        trendingNow} : Props = await fetchAllData();

        return (
          <Page {...{netflixOriginals,
            actionMovies,
            comedyMovies,
            documentaries,
            horrorMovies,
            romanceMovies,
            topRated,
            trendingNow,}} />
        );

    }
Enter fullscreen mode Exit fullscreen mode

Now, lets create the login page. Create a login folder in app directory. Within the folder create layout.tsx, loading.tsx and *page.tsx *files. Below are the code for all the files.

    // page.tsx
    import { fetchAllData } from '@/actions/actions'
    import { Movie } from '../typings'
    import Page from '@/components/Page';

    interface Props {
      netflixOriginals: Movie[]
      trendingNow: Movie[]
      topRated: Movie[]
      actionMovies: Movie[]
      comedyMovies: Movie[]
      horrorMovies: Movie[]
      romanceMovies: Movie[]
      documentaries: Movie[]
    }

    export default async function Home() {
      const {netflixOriginals,
        actionMovies,
        comedyMovies,
        documentaries,
        horrorMovies,
        romanceMovies,
        topRated,
        trendingNow} : Props = await fetchAllData();

        return (
          <Page {...{netflixOriginals,
            actionMovies,
            comedyMovies,
            documentaries,
            horrorMovies,
            romanceMovies,
            topRated,
            trendingNow,}} />
        );

    }

    // layout.tsx
    import type { Metadata } from 'next'

    export const metadata: Metadata = {
      title: 'NETFLIX - Login',
      description: 'Watch your favorite movies and TV shows on Netflix.',
    }

    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
          <section className="flex flex-col items-center">
              {children}
          </section>
      )
    }
Enter fullscreen mode Exit fullscreen mode
    // loading.tsx
    import Loader from '@/components/Loader'
    import React from 'react'

    const loading = () => {
      return (
        <div className='pt-36'>
        <Loader />
        </div>
      )
    }

    export default loading
Enter fullscreen mode Exit fullscreen mode

Finally create a account folder in app directory. Within the folder create layout.tsx, loading.tsx and *page.tsx *files. Below are the code for all the files.

    // layout.tsx
    import type { Metadata } from 'next'

    export const metadata: Metadata = {
      title: 'Account Settings - Netflix',
      description: 'Watch your favorite movies and TV shows on Netflix.',
    }

    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
          <section className="flex flex-col items-center">
              {children}
          </section>
      )
    }
Enter fullscreen mode Exit fullscreen mode
    // loading.tsx
    import Loader from '@/components/Loader'
    import React from 'react'

    const loading = () => {
      return (
        <div className='pt-36'>
        <Loader />
        </div>
      )
    }

    export default loading
Enter fullscreen mode Exit fullscreen mode
    // page.tsx
    "use client";

    import React, { useEffect, useState } from 'react';
    import Link from 'next/link';
    import useAuth from '@/hooks/useAuth';
    import Membership from '@/components/Membership';
    import { useRouter } from 'next/navigation';
    import Loader from '@/components/Loader';
    import Plans from '@/components/Plan';
    import { doc, getDoc } from 'firebase/firestore';
    import { db } from '@/firebase';
    import { User } from 'firebase/auth';

    interface Props{
      id: string;
      name: string;
      amount: number;
      expiry: Date;
    }

    const Page = () => {
        const { user, logout } = useAuth()
        const [showPlan, setShowPlan] = useState<boolean>(false)
        const [plan, setPlan] = useState<Props | null>(null)

        const router = useRouter()
        const [loading, setLoading] = useState<boolean>(false);

        useEffect(() => {
          isSubscribed(user);
        }, [])

          const isSubscribed = async (user: User | null) : Promise<void> => {
            setLoading(true);
            if(user == null){
              setLoading(false);
              return;
            }
            const userRef =  doc(db, "subscriptions", user.uid);
            const docSnap = await getDoc(userRef);
            if(!docSnap.exists()) return;
            else{
              const data = docSnap.data();
              const newdate = new Date().toISOString()
              const expiryDate = data.subscriptionExpiryDate
              if(newdate > expiryDate){
                setPlan(null)
              }
              else{
                setPlan({id: data.subscriptionPlanId, name: data.subscriptionPlan, amount: data.subscriptionAmount, expiry: expiryDate})
              }
            }
            setLoading(false);
          }

        if(showPlan) return <div className='pt-32 space-y-2'>
          <button onClick={() => setShowPlan(false)} className='cursor-pointer hover:underline -mb-28 font-bold text-xl'>Cancel</button>
            <Plans />
          </div>

        if(loading) return <div className="flex pt-32 items-center"><Loader /></div>

      return (
        <div>
          <header className={`bg-[#141414]`}>
            <Link href="/">
              <img
                src="https://rb.gy/ulxxee"
                width={120}
                height={120}
                className="cursor-pointer object-contain"
                alt="image"
              />
            </Link>
            <Link href="/account">
              <img
                src="https://rb.gy/g1pwyx"
                alt="new-image"
                className="cursor-pointer rounded"
              />
            </Link>
          </header>

          <main className="mx-auto max-w-6xl px-5 pt-24 pb-12 transition-all md:px-10">
            <div className="flex flex-col gap-x-4 md:flex-row md:items-center">
              <h1 className="text-3xl md:text-4xl">Account</h1>
              <div className="-ml-0.5 flex items-center gap-x-1.5">
                <img src="https://rb.gy/4vfk4r" alt="" className="h-7 w-7" />
                <p className="text-xs font-semibold text-[#555]">
                  {plan!=null ? `Membership will expire on ${(new Date(plan.expiry)).toDateString()}` : 'Membership Expired!'}
                </p>
              </div>
            </div>

            {plan!=null && <Membership />}

            <div className="mt-6 grid grid-cols-1 gap-x-4 border px-4 py-4 md:grid-cols-4 md:border-x-0 md:border-t md:border-b-0 md:px-0 md:pb-0 items-center">
              {plan!=null && <h4 className="text-lg text-[gray]">Plan Name:</h4>}
              {/* Find the current plan */}
              {plan!=null && <div className="col-span-2 font-medium">
                {plan.name}
              </div>}
              <button onClick={() => setShowPlan(true)} className="cursor-pointer flex flex-1 text-blue-500 hover:underline md:text-right">
                Change plan
              </button>
            </div>

            <div className="mt-6 grid grid-cols-1 gap-x-4 border px-4 py-4 md:grid-cols-4 md:border-x-0 md:border-t md:border-b-0 md:px-0">
              <h4 className="text-lg text-[gray]">Settings</h4>
              <p
                className="col-span-3 cursor-pointer text-blue-500 hover:underline"
                onClick={logout}
              >
                Sign out
              </p>
            </div>
          </main>
        </div>
      )
    }

    export default Page
Enter fullscreen mode Exit fullscreen mode

One thing I missed in my last article was that the tailwind.config.js file needed to be changed.

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      content: [
        './pages/**/*.{js,ts,jsx,tsx,mdx}',
        './components/**/*.{js,ts,jsx,tsx,mdx}',
        './app/**/*.{js,ts,jsx,tsx,mdx}',
      ],
      theme: {
        extend: {
          backgroundImage: {
            'gradient-to-b':
              'linear-gradient(to bottom,rgba(20,20,20,0) 0,rgba(20,20,20,.15) 15%,rgba(20,20,20,.35) 29%,rgba(20,20,20,.58) 44%,#141414 68%,#141414 100%);',
          },
        },
      },
      plugins: [],
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

You may now host your website using free services like vercel or commercial ones like Hostinger, Digitalocean, and more. The subscription is completely working, and you can get subscriptions using razorpay test cards.

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. And if you want more articles like this, follow me on dev.to . If you missed anything or want to check out the full code here. Here is the part 1 of the blog.Thanks for reading!

Top comments (0)