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)
}
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
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
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' });
}
};
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' });
}
};
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
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>
)
}
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
Create Loader.tsx file in components folder.
function Loader() {
return (
<div className="lds-ripple">
<div></div>
<div></div>
</div>
)
}
export default Loader;
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;
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
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
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;
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
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
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
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
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,}} />
);
}
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>
)
}
// loading.tsx
import Loader from '@/components/Loader'
import React from 'react'
const loading = () => {
return (
<div className='pt-36'>
<Loader />
</div>
)
}
export default loading
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>
)
}
// loading.tsx
import Loader from '@/components/Loader'
import React from 'react'
const loading = () => {
return (
<div className='pt-36'>
<Loader />
</div>
)
}
export default loading
// 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
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: [],
}
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)