├── .eslintrc.json ├── public ├── favicon.ico ├── netfuix.jpg ├── images │ ├── hero.jpg │ ├── logo.png │ ├── default-red.png │ ├── default-blue.png │ ├── default-green.png │ ├── default-slate.png │ ├── plus.svg │ ├── download.svg │ ├── tv.svg │ ├── popcorn.svg │ └── crystalball.svg ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── site.webmanifest ├── screenshots ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png └── screenshot-5.png ├── postcss.config.js ├── lib ├── fetcher.ts ├── prismadb.ts └── serverAuth.ts ├── global.d.ts ├── next.config.js ├── hooks ├── useCurrentUser.ts ├── useBillboard.ts ├── useMovieList.ts ├── useFavourites.ts ├── useMovie.ts └── useInfoModal.ts ├── components ├── NavbarItem.tsx ├── svg │ ├── ExitIcon.tsx │ ├── SearchIcon.tsx │ ├── SpinnerIcon.tsx │ ├── PencilIcon.tsx │ ├── AccountIcon.tsx │ ├── BellIcon.tsx │ ├── HelpIcon.tsx │ ├── TransferIcon.tsx │ └── NetfuixLogo.tsx ├── VolumeButton.tsx ├── PlayButton.tsx ├── Input.tsx ├── MobileMenu.tsx ├── FavouriteButton.tsx ├── MovieList.tsx ├── AuthFooter.tsx ├── AccountMenu.tsx ├── Billboard.tsx ├── DisclaimerModal.tsx ├── Navbar.tsx ├── InfoModal.tsx └── MovieCard.tsx ├── .gitignore ├── pages ├── api │ ├── current.ts │ ├── movies │ │ ├── index.ts │ │ └── [movieId].ts │ ├── favourites.ts │ ├── random.ts │ ├── register.ts │ ├── favourite.ts │ └── auth │ │ └── [...nextauth].ts ├── watch │ └── [movieId].tsx ├── browse.tsx ├── profile.tsx ├── _app.tsx ├── auth.tsx └── index.tsx ├── tailwind.config.js ├── tsconfig.json ├── .env.example ├── package.json ├── README.md ├── prisma └── schema.prisma ├── styles └── globals.css └── movies.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/favicon.ico -------------------------------------------------------------------------------- /public/netfuix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/netfuix.jpg -------------------------------------------------------------------------------- /public/images/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/images/hero.jpg -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/images/logo.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/default-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/images/default-red.png -------------------------------------------------------------------------------- /screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /screenshots/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/screenshots/screenshot-2.png -------------------------------------------------------------------------------- /screenshots/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/screenshots/screenshot-3.png -------------------------------------------------------------------------------- /screenshots/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/screenshots/screenshot-4.png -------------------------------------------------------------------------------- /screenshots/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/screenshots/screenshot-5.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/default-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/images/default-blue.png -------------------------------------------------------------------------------- /public/images/default-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/images/default-green.png -------------------------------------------------------------------------------- /public/images/default-slate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/images/default-slate.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/netflix-clone/main/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /lib/fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const fetcher = (url: string) => axios.get(url).then((res) => res.data); 4 | 5 | export default fetcher; 6 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | declare global { 4 | namespace globalThis { 5 | var prismadb: PrismaClient; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const client = global.prismadb || new PrismaClient(); 4 | if (process.env.NODE_ENV == 'production') global.prismadb = client; 5 | 6 | export default client; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | domains: ['res.cloudinary.com'] 7 | } 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /public/images/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /hooks/useCurrentUser.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '../lib/fetcher'; 4 | 5 | const useCurrentUser = () => { 6 | const { data, error, isLoading, mutate } = useSWR('/api/current', fetcher); 7 | 8 | return { data, error, isLoading, mutate }; 9 | }; 10 | 11 | export default useCurrentUser; 12 | -------------------------------------------------------------------------------- /components/NavbarItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface NavbarItemProps { 4 | label: string; 5 | } 6 | 7 | const NavbarItem: React.FC = ({ label }) => { 8 | return ( 9 |
10 | {label} 11 |
12 | ); 13 | }; 14 | 15 | export default NavbarItem; 16 | -------------------------------------------------------------------------------- /hooks/useBillboard.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import fetcher from '../lib/fetcher'; 3 | 4 | const useBillboard = () => { 5 | const { data, error, isLoading } = useSWR('/api/random', fetcher, { 6 | revalidateIfStale: false, 7 | revalidateOnFocus: false, 8 | revalidateOnReconnect: false 9 | }); 10 | 11 | return { data, error, isLoading }; 12 | }; 13 | 14 | export default useBillboard; 15 | -------------------------------------------------------------------------------- /hooks/useMovieList.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import fetcher from '../lib/fetcher'; 3 | 4 | const useMovieList = () => { 5 | const { data, error, isLoading } = useSWR('/api/movies', fetcher, { 6 | revalidateIfStale: false, 7 | revalidateOnFocus: false, 8 | revalidateOnReconnect: false 9 | }); 10 | 11 | return { data, error, isLoading }; 12 | }; 13 | 14 | export default useMovieList; 15 | -------------------------------------------------------------------------------- /hooks/useFavourites.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import fetcher from '../lib/fetcher'; 3 | 4 | const useFavourites = () => { 5 | const { data, error, isLoading, mutate } = useSWR( 6 | '/api/favourites', 7 | fetcher, 8 | { 9 | revalidateIfStale: false, 10 | revalidateOnFocus: false, 11 | revalidateOnReconnect: false 12 | } 13 | ); 14 | 15 | return { data, error, isLoading, mutate }; 16 | }; 17 | 18 | export default useFavourites; 19 | -------------------------------------------------------------------------------- /hooks/useMovie.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import fetcher from '../lib/fetcher'; 3 | 4 | const useMovie = (id?: string) => { 5 | const { data, error, isLoading } = useSWR( 6 | id ? `/api/movies/${id}` : null, 7 | fetcher, 8 | { 9 | revalidateIfStale: false, 10 | revalidateOnFocus: false, 11 | revalidateOnReconnect: false 12 | } 13 | ); 14 | 15 | return { 16 | data, 17 | error, 18 | isLoading 19 | }; 20 | }; 21 | 22 | export default useMovie; 23 | -------------------------------------------------------------------------------- /hooks/useInfoModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export interface ModalStoreInterface { 4 | movieId?: string; 5 | isOpen: boolean; 6 | openModal: (movieId: string) => void; 7 | closeModal: () => void; 8 | } 9 | 10 | const useInfoModal = create((set) => ({ 11 | movieId: undefined, 12 | isOpen: false, 13 | openModal: (movieId: string) => set({ isOpen: true, movieId }), 14 | closeModal: () => set({ isOpen: false, movieId: undefined }) 15 | })); 16 | 17 | export default useInfoModal; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /pages/api/current.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import serverAuth from '../../lib/serverAuth'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method != 'GET') { 10 | return res.status(405).end(); 11 | } 12 | 13 | try { 14 | const { currentUser } = await serverAuth(req, res); 15 | 16 | return res.status(200).json(currentUser); 17 | } catch (error) { 18 | console.log(error); 19 | return res.status(500).end(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx,mdx}', 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}' 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | 'red-netfuix': 'rgba(229, 9, 20, 1)', 12 | 'red-netfuix-dark': 'rgba(193, 17, 25, 1)', 13 | 'dark-netfuix': 'rgb(20, 20, 20)' 14 | }, 15 | screens: { 16 | '3xl': '1920px' 17 | } 18 | } 19 | }, 20 | plugins: [] 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/movies/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import prismadb from '../../../lib/prismadb'; 3 | import serverAuth from '../../../lib/serverAuth'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method !== 'GET') { 10 | return res.status(405).end(); 11 | } 12 | 13 | try { 14 | await serverAuth(req, res); 15 | 16 | const movies = await prismadb.movie.findMany(); 17 | 18 | return res.status(200).json(movies); 19 | } catch (error) { 20 | console.log(error); 21 | return res.status(500).end(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /components/svg/ExitIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ExitIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default ExitIcon; 25 | -------------------------------------------------------------------------------- /lib/serverAuth.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getServerSession } from 'next-auth'; 3 | 4 | import prismadb from '../lib/prismadb'; 5 | import { authOptions } from '../pages/api/auth/[...nextauth]'; 6 | 7 | const serverAuth = async (req: NextApiRequest, res: NextApiResponse) => { 8 | const session = await getServerSession(req, res, authOptions); 9 | 10 | if (!session?.user?.email) { 11 | throw new Error('Not sign in'); 12 | } 13 | 14 | const currentUser = await prismadb.user.findUnique({ 15 | where: { 16 | email: session.user.email 17 | } 18 | }); 19 | 20 | if (!currentUser) { 21 | throw new Error('Not sign in'); 22 | } 23 | 24 | return { currentUser }; 25 | }; 26 | 27 | export default serverAuth; 28 | -------------------------------------------------------------------------------- /pages/api/favourites.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import prismadb from '../../lib/prismadb'; 3 | import serverAuth from '../../lib/serverAuth'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method !== 'GET') { 10 | return res.status(405).end(); 11 | } 12 | 13 | try { 14 | const { currentUser } = await serverAuth(req, res); 15 | 16 | const favouriteMovies = await prismadb.movie.findMany({ 17 | where: { 18 | id: { 19 | in: currentUser?.favouriteIds 20 | } 21 | } 22 | }); 23 | 24 | return res.status(200).json(favouriteMovies); 25 | } catch (error) { 26 | console.log(error); 27 | return res.status(500).end(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pages/api/random.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import prismadb from '../../lib/prismadb'; 3 | import serverAuth from '../../lib/serverAuth'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method !== 'GET') { 10 | return res.status(405).end(); 11 | } 12 | 13 | try { 14 | await serverAuth(req, res); 15 | 16 | const movieCount = await prismadb.movie.count(); 17 | const randomIndex = Math.floor(Math.random() * movieCount); 18 | 19 | const randomMovies = await prismadb.movie.findMany({ 20 | take: 1, 21 | skip: randomIndex 22 | }); 23 | 24 | return res.status(200).json(randomMovies[0]); 25 | } catch (error) { 26 | console.log(error); 27 | return res.status(500).end(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/VolumeButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FiVolume2, FiVolumeX } from 'react-icons/fi'; 3 | 4 | interface VolumeButtonProps { 5 | isMute: boolean; 6 | handleMuteVideo: any; 7 | } 8 | 9 | const VolumeButton: React.FC = ({ 10 | isMute, 11 | handleMuteVideo 12 | }) => { 13 | return ( 14 |
18 | {isMute && } 19 | {!isMute && } 20 |
21 | ); 22 | }; 23 | 24 | export default VolumeButton; 25 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="Add your MongoDB URL here" 8 | 9 | NEXTAUTH_JWT_SECRET="Add your NextAuth JWT secret here" 10 | NEXTAUTH_SECRET="Add your NextAuth secret here" 11 | NEXTAUTH_URL=http://localhost:3000 12 | 13 | GITHUB_ID="Add your GitHub ID here" 14 | GITHUB_SECRET="Add your GitHub Secret here" 15 | 16 | GOOGLE_CLIENT_ID="Add your Google Client ID here" 17 | GOOGLE_CLIENT_SECRET="Add your Google Client Secret here" -------------------------------------------------------------------------------- /components/svg/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SearchIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default SearchIcon; 25 | -------------------------------------------------------------------------------- /components/svg/SpinnerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ClassProps { 4 | classes?: string; 5 | } 6 | 7 | const SpinnerIcon: React.FC = ({ classes }) => { 8 | return ( 9 | 15 | 23 | 28 | 29 | ); 30 | }; 31 | 32 | export default SpinnerIcon; 33 | -------------------------------------------------------------------------------- /components/svg/PencilIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PencilIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default PencilIcon; 25 | -------------------------------------------------------------------------------- /components/PlayButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillPlayFill } from 'react-icons/bs'; 3 | import { useRouter } from 'next/router'; 4 | 5 | interface PlayButtonProps { 6 | movieId: string; 7 | isModal?: boolean; 8 | } 9 | 10 | const PlayButton: React.FC = ({ movieId, isModal }) => { 11 | const router = useRouter(); 12 | 13 | return ( 14 | 29 | ); 30 | }; 31 | 32 | export default PlayButton; 33 | -------------------------------------------------------------------------------- /pages/api/movies/[movieId].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import prismadb from '../../../lib/prismadb'; 3 | import serverAuth from '../../../lib/serverAuth'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method !== 'GET') { 10 | return res.status(405).end(); 11 | } 12 | 13 | try { 14 | await serverAuth(req, res); 15 | 16 | const { movieId } = req.query; 17 | 18 | if (typeof movieId !== 'string') { 19 | throw new Error('Invalid ID'); 20 | } 21 | 22 | if (!movieId) { 23 | throw new Error('Missing ID'); 24 | } 25 | 26 | const movie = await prismadb.movie.findUnique({ 27 | where: { 28 | id: movieId 29 | } 30 | }); 31 | 32 | if (!movie) { 33 | throw new Error('Cannot find movie'); 34 | } 35 | 36 | return res.status(200).json(movie); 37 | } catch (error) { 38 | console.log(error); 39 | return res.status(500).end(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface InputProps { 4 | id: string; 5 | onChange: any; 6 | value: string; 7 | label: string; 8 | type?: string; 9 | } 10 | 11 | const Input: React.FC = ({ id, onChange, value, label, type }) => { 12 | return ( 13 |
14 | 22 | 28 |
29 | ); 30 | }; 31 | 32 | export default Input; 33 | -------------------------------------------------------------------------------- /components/svg/AccountIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AccountIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default AccountIcon; 25 | -------------------------------------------------------------------------------- /pages/api/register.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import prismadb from '../../lib/prismadb'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method != 'POST') { 10 | return res.status(405).end(); 11 | } 12 | 13 | try { 14 | const { email, name, password } = req.body; 15 | 16 | const existingUser = await prismadb.user.findUnique({ 17 | where: { 18 | email 19 | } 20 | }); 21 | 22 | if (existingUser) { 23 | return res.status(422).json({ error: 'Email taken' }); 24 | } 25 | 26 | const hashedPassword = await bcrypt.hash(password, 12); 27 | 28 | const user = await prismadb.user.create({ 29 | data: { 30 | email, 31 | name, 32 | hashedPassword, 33 | image: '', 34 | emailVerified: new Date() 35 | } 36 | }); 37 | 38 | return res.status(200).json(user); 39 | } catch (error) { 40 | console.log(error); 41 | return res.status(400).end({ error: `Something went wrong: ${error}` }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pages/watch/[movieId].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import useMovie from '../../hooks/useMovie'; 4 | import { HiOutlineArrowLeft } from 'react-icons/hi'; 5 | 6 | const Watch = () => { 7 | const router = useRouter(); 8 | const { movieId } = router.query; 9 | 10 | const { data } = useMovie(movieId as string); 11 | 12 | return ( 13 |
14 | 23 | 30 |
31 | ); 32 | }; 33 | 34 | export default Watch; 35 | -------------------------------------------------------------------------------- /components/svg/BellIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const BellIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default BellIcon; 25 | -------------------------------------------------------------------------------- /components/svg/HelpIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HelpIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default HelpIcon; 25 | -------------------------------------------------------------------------------- /components/svg/TransferIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TransferIcon = () => { 4 | return ( 5 | 13 | 19 | 20 | ); 21 | }; 22 | 23 | export default TransferIcon; 24 | -------------------------------------------------------------------------------- /components/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AiFillCaretUp } from 'react-icons/ai'; 3 | 4 | interface MobileMenuProps { 5 | visible?: boolean; 6 | } 7 | 8 | const MobileMenu: React.FC = ({ visible }) => { 9 | if (!visible) { 10 | return null; 11 | } 12 | 13 | return ( 14 | <> 15 | 16 |
17 |
18 |
19 | Home 20 |
21 |
22 | TV Shows 23 |
24 |
25 | Movies 26 |
27 |
28 | New & Popular 29 |
30 |
31 | My List 32 |
33 |
34 | Browse by Languages 35 |
36 |
37 |
38 | 39 | ); 40 | }; 41 | 42 | export default MobileMenu; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netflix-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "migrate:dev": "npx dotenv -e .env.local -- prisma migrate dev", 11 | "db:push": "npx dotenv -e .env.local -- prisma db push", 12 | "migrate:reset": "npx dotenv -e .env.local -- prisma migrate reset", 13 | "db:seed": "npx dotenv -e .env.local -- prisma db seed", 14 | "prisma:generate": "npx dotenv -e .env.local -- prisma generate", 15 | "prisma:studio": "npx dotenv -e .env.local -- prisma studio", 16 | "production:build": "npx prisma generate && npx prisma migrate deploy && next build" 17 | }, 18 | "dependencies": { 19 | "@headlessui/react": "^1.7.14", 20 | "@next-auth/prisma-adapter": "^1.0.6", 21 | "@prisma/client": "^4.14.0", 22 | "axios": "^1.12.0", 23 | "bcrypt": "^5.1.0", 24 | "lodash": "^4.17.21", 25 | "next": "14.2.32", 26 | "next-auth": "^4.24.10", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "react-icons": "^4.8.0", 30 | "swiper": "^9.3.2", 31 | "swr": "^2.1.5", 32 | "zustand": "^4.3.8" 33 | }, 34 | "devDependencies": { 35 | "@types/bcrypt": "^5.0.0", 36 | "@types/lodash": "^4.14.194", 37 | "@types/node": "20.1.2", 38 | "@types/react": "18.2.6", 39 | "@types/react-dom": "18.2.4", 40 | "autoprefixer": "^10.4.14", 41 | "eslint": "8.40.0", 42 | "eslint-config-next": "13.4.1", 43 | "postcss": "^8.4.23", 44 | "prisma": "^4.14.0", 45 | "tailwindcss": "^3.3.2", 46 | "typescript": "5.0.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Netfuix](https://netfuix.salimi.my) · [![Author Salimi](https://img.shields.io/badge/Author-Salimi-%3C%3E)](https://www.linkedin.com/in/mohamad-salimi/) 2 | 3 | This is a Netflix clone app created using Next.js for educational purposes. User can sign up and create an account or sign in using Google or GitHub account. The app includes features of movie list slider, movie streaming, save favourite to My List collection and more. 4 | 5 | ## Exact copy of Netflix UI 6 | 7 | - This is a clone of Netflix app 8 | - Login using NextAuth.js through Google or GitHub 9 | - MongoDB & Prisma for database 10 | - Zustand for state management 11 | - useSWR hook for data fetching 12 | - Hosted in Vercel 13 | 14 | ## Tech/framework used 15 | 16 | - Next.js 17 | - NextAuth.js 18 | - Tailwind CSS 19 | - TypeScript 20 | - MongoDB 21 | - Prisma 22 | - Zustand 23 | - Vercel 24 | 25 | ## Starting the project 26 | 27 | Open the [.env.local.example](/.env.local.example) and fill in your MongoDB URL & NextAuth Configurations then save it as .env.local the run the following command: 28 | 29 | ```bash 30 | npm install 31 | npm run dev 32 | # or 33 | yarn install 34 | yarn run dev 35 | ``` 36 | 37 | ## Demo 38 | 39 | The app is hosted on Vercel. [Click here](https://netfuix.salimi.my) to visit. 40 |
41 | Direct link: `https://netfuix.salimi.my` 42 | 43 | ## Screenshots 44 | 45 | #### Landing 46 | 47 | ![Landing](/screenshots/screenshot-1.png) 48 | 49 | #### Auth 50 | 51 | ![Auth](/screenshots/screenshot-2.png) 52 | 53 | #### Browse 54 | 55 | ![Browse](/screenshots/screenshot-3.png) 56 | 57 | #### Modal 58 | 59 | ![Modal](/screenshots/screenshot-4.png) 60 | 61 | #### Watch 62 | 63 | ![Watch](/screenshots/screenshot-5.png) 64 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | name String 16 | image String? 17 | email String? @unique 18 | emailVerified DateTime? 19 | hashedPassword String? 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @updatedAt 22 | favouriteIds String[] @db.ObjectId 23 | sessions Session[] 24 | accounts Account[] 25 | } 26 | 27 | model Account { 28 | id String @id @default(auto()) @map("_id") @db.ObjectId 29 | userId String @db.ObjectId 30 | type String 31 | provider String 32 | providerAccountId String 33 | refresh_token String? @db.String 34 | access_token String? @db.String 35 | expires_at Int? 36 | token_type String? 37 | scope String? 38 | id_token String? @db.String 39 | session_state String? 40 | 41 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 42 | 43 | @@unique([provider, providerAccountId]) 44 | } 45 | 46 | model Session { 47 | id String @id @default(auto()) @map("_id") @db.ObjectId 48 | sessionToken String @unique 49 | userId String @db.ObjectId 50 | expires DateTime 51 | 52 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 53 | } 54 | 55 | model VerificationToken { 56 | id String @id @default(auto()) @map("_id") @db.ObjectId 57 | identifier String 58 | token String @unique 59 | expires DateTime 60 | 61 | @@unique([identifier, token]) 62 | } 63 | 64 | model Movie { 65 | id String @id @default(auto()) @map("_id") @db.ObjectId 66 | title String 67 | description String 68 | videoUrl String 69 | thumbnailUrl String 70 | genre String[] 71 | duration String 72 | } -------------------------------------------------------------------------------- /pages/browse.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage, NextPageContext } from 'next'; 2 | import { getSession } from 'next-auth/react'; 3 | import Navbar from '../components/Navbar'; 4 | import Billboard from '../components/Billboard'; 5 | import MovieList from '../components/MovieList'; 6 | import useMovieList from '../hooks/useMovieList'; 7 | import useFavourites from '../hooks/useFavourites'; 8 | import InfoModal from '../components/InfoModal'; 9 | import useInfoModal from '../hooks/useInfoModal'; 10 | 11 | const Browse: NextPage = () => { 12 | const { data: movies = [] } = useMovieList(); 13 | const { data: favourites = [] } = useFavourites(); 14 | const { isOpen, closeModal } = useInfoModal(); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 | 26 | 0} 30 | /> 31 | 32 | 33 | 34 |
35 | 36 | ); 37 | }; 38 | 39 | export default Browse; 40 | 41 | export async function getServerSideProps(context: NextPageContext) { 42 | const session = await getSession(context); 43 | 44 | if (!session) { 45 | return { 46 | redirect: { 47 | destination: '/auth', 48 | permanent: false 49 | } 50 | }; 51 | } 52 | 53 | return { 54 | props: {} 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage, NextPageContext } from 'next'; 2 | import { getSession } from 'next-auth/react'; 3 | import Image from 'next/image'; 4 | import React from 'react'; 5 | import useCurrentUser from '../hooks/useCurrentUser'; 6 | import { useRouter } from 'next/router'; 7 | 8 | const Profile: NextPage = () => { 9 | const router = useRouter(); 10 | const { data: user } = useCurrentUser(); 11 | 12 | return ( 13 |
14 |
15 |

16 | Who's watching? 17 |

18 |
19 |
{ 21 | router.push('/browse'); 22 | }} 23 | > 24 |
25 |
26 | Profile 32 |
33 |
34 | {user?.name} 35 |
36 |
37 |
38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Profile; 45 | 46 | export async function getServerSideProps(context: NextPageContext) { 47 | const session = await getSession(context); 48 | 49 | if (!session) { 50 | return { 51 | redirect: { 52 | destination: '/auth', 53 | permanent: false 54 | } 55 | }; 56 | } 57 | 58 | return { 59 | props: {} 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /pages/api/favourite.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { without } from 'lodash'; 3 | 4 | import prismadb from '../../lib/prismadb'; 5 | import serverAuth from '../../lib/serverAuth'; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | try { 12 | if (req.method === 'POST') { 13 | const { currentUser } = await serverAuth(req, res); 14 | 15 | const { movieId } = req.body; 16 | 17 | const existingMovie = await prismadb.movie.findUnique({ 18 | where: { 19 | id: movieId 20 | } 21 | }); 22 | 23 | if (!existingMovie) { 24 | throw new Error('Invalid ID'); 25 | } 26 | 27 | const user = await prismadb.user.update({ 28 | where: { 29 | email: currentUser.email || '' 30 | }, 31 | data: { 32 | favouriteIds: { 33 | push: movieId 34 | } 35 | } 36 | }); 37 | 38 | return res.status(200).json(user); 39 | } 40 | 41 | if (req.method === 'DELETE') { 42 | const { currentUser } = await serverAuth(req, res); 43 | 44 | const { movieId } = req.query as { movieId: string }; 45 | 46 | const existingMovie = await prismadb.movie.findUnique({ 47 | where: { 48 | id: movieId 49 | } 50 | }); 51 | 52 | if (!existingMovie) { 53 | throw new Error('Invalid ID'); 54 | } 55 | 56 | const updatedFavouriteIds = without(currentUser.favouriteIds, movieId); 57 | 58 | const updatedUser = await prismadb.user.update({ 59 | where: { 60 | email: currentUser.email || '' 61 | }, 62 | data: { 63 | favouriteIds: updatedFavouriteIds 64 | } 65 | }); 66 | 67 | return res.status(200).json(updatedUser); 68 | } 69 | 70 | return res.status(405).end(); 71 | } catch (error) { 72 | console.log(error); 73 | return res.status(500).end(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /components/svg/NetfuixLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ClassProps { 4 | classes: string; 5 | } 6 | 7 | const NetfuixLogo: React.FC = ({ classes }) => { 8 | return ( 9 | 19 | ); 20 | }; 21 | 22 | export default NetfuixLogo; 23 | -------------------------------------------------------------------------------- /components/FavouriteButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from 'react'; 2 | import axios from 'axios'; 3 | 4 | import useCurrentUser from '../hooks/useCurrentUser'; 5 | import useFavourites from '../hooks/useFavourites'; 6 | 7 | import { AiOutlinePlus } from 'react-icons/ai'; 8 | import { AiOutlineCheck } from 'react-icons/ai'; 9 | import SpinnerIcon from './svg/SpinnerIcon'; 10 | 11 | interface FavouriteButtonProps { 12 | movieId: string; 13 | isModal?: boolean; 14 | } 15 | 16 | const FavouriteButton: React.FC = ({ 17 | movieId, 18 | isModal 19 | }) => { 20 | const { mutate: mutateFavourites } = useFavourites(); 21 | const { data: currentUser, mutate } = useCurrentUser(); 22 | 23 | const [loading, setLoading] = useState(false); 24 | 25 | const isFavourite = useMemo(() => { 26 | const list = currentUser?.favouriteIds || []; 27 | 28 | return list.includes(movieId); 29 | }, [currentUser, movieId]); 30 | 31 | const toggleFavourites = useCallback(async () => { 32 | setLoading(true); 33 | let response; 34 | 35 | if (isFavourite) { 36 | response = await axios.delete(`/api/favourite?movieId=${movieId}`); 37 | } else { 38 | response = await axios.post('/api/favourite', { movieId }); 39 | } 40 | 41 | const updatedFavouriteIds = response?.data?.favouriteIds; 42 | 43 | mutate({ 44 | ...currentUser, 45 | favouriteIds: updatedFavouriteIds 46 | }); 47 | mutateFavourites(); 48 | setLoading(false); 49 | }, [movieId, isFavourite, currentUser, mutate, mutateFavourites, setLoading]); 50 | 51 | const Icon = isFavourite ? AiOutlineCheck : AiOutlinePlus; 52 | 53 | return ( 54 | <> 55 | {loading && } 56 | {!loading && ( 57 |
65 | 66 |
67 | )} 68 | 69 | ); 70 | }; 71 | 72 | export default FavouriteButton; 73 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | import Head from 'next/head'; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | Netfuix — Watch Movies, TV Shows Online 10 | 11 | 15 | 16 | 17 | 18 | 22 | 26 | 27 | 28 | 29 | 30 | 34 | 38 | 39 | 40 | 45 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default MyApp; 66 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { AuthOptions } from 'next-auth'; 2 | import CredentialsProvider from 'next-auth/providers/credentials'; 3 | import { compare } from 'bcrypt'; 4 | 5 | import GoogleProvider from 'next-auth/providers/google'; 6 | import GithubProvider from 'next-auth/providers/github'; 7 | 8 | import { PrismaAdapter } from '@next-auth/prisma-adapter'; 9 | 10 | import prismadb from '../../../lib/prismadb'; 11 | 12 | export const authOptions: AuthOptions = { 13 | providers: [ 14 | GoogleProvider({ 15 | clientId: process.env.GOOGLE_CLIENT_ID || '', 16 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', 17 | allowDangerousEmailAccountLinking: true 18 | }), 19 | GithubProvider({ 20 | clientId: process.env.GITHUB_ID || '', 21 | clientSecret: process.env.GITHUB_SECRET || '', 22 | allowDangerousEmailAccountLinking: true 23 | }), 24 | CredentialsProvider({ 25 | id: 'credentials', 26 | name: 'Credentials', 27 | credentials: { 28 | username: { 29 | label: 'Email', 30 | type: 'text' 31 | }, 32 | password: { 33 | label: 'Password', 34 | type: 'password' 35 | } 36 | }, 37 | async authorize(credentials: any) { 38 | if (!credentials?.email || !credentials?.password) { 39 | throw new Error('Email and password are required.'); 40 | } 41 | 42 | const user = await prismadb.user.findUnique({ 43 | where: { 44 | email: credentials.email 45 | } 46 | }); 47 | 48 | if (!user || !user.hashedPassword) { 49 | throw new Error('Incorrect email or password.'); 50 | } 51 | 52 | const isCorrectPassword = await compare( 53 | credentials.password, 54 | user.hashedPassword 55 | ); 56 | 57 | if (!isCorrectPassword) { 58 | throw new Error('Incorrect email or password.'); 59 | } 60 | 61 | return user; 62 | } 63 | }) 64 | ], 65 | pages: { 66 | signIn: '/auth', 67 | error: '/auth' 68 | }, 69 | debug: process.env.NODE_ENV == 'development', 70 | adapter: PrismaAdapter(prismadb), 71 | session: { 72 | strategy: 'jwt' 73 | }, 74 | jwt: { 75 | secret: process.env.NEXTAUTH_JWT_SECRET 76 | }, 77 | secret: process.env.NEXTAUTH_SECRET 78 | }; 79 | 80 | export default NextAuth(authOptions); 81 | -------------------------------------------------------------------------------- /components/MovieList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isEmpty } from 'lodash'; 3 | import MovieCard from './MovieCard'; 4 | import { Swiper, SwiperSlide } from 'swiper/react'; 5 | import { Navigation } from 'swiper'; 6 | import 'swiper/css'; 7 | import 'swiper/css/navigation'; 8 | 9 | interface MovieListProps { 10 | data: Record[]; 11 | title: string; 12 | common?: boolean; 13 | } 14 | 15 | const MovieList: React.FC = ({ data, title, common }) => { 16 | if (isEmpty(data)) { 17 | return null; 18 | } 19 | 20 | console.log(typeof data); 21 | 22 | return ( 23 | //
24 | //
25 | //

26 | // {title} 27 | //

28 | //
29 | // {data.map((movie) => ( 30 | // 31 | // ))} 32 | //
33 | //
34 | //
35 |
40 |
41 |

42 | {title} 43 |

44 | 12} 48 | // loopedSlides={6} 49 | slidesPerView={2} 50 | // slidesPerGroup={2} 51 | breakpoints={{ 52 | 768: { 53 | slidesPerView: 3 54 | // slidesPerGroup: 3 55 | }, 56 | 1024: { 57 | slidesPerView: 4 58 | // slidesPerGroup: 4 59 | }, 60 | 1440: { 61 | slidesPerView: 6 62 | // slidesPerGroup: 6 63 | } 64 | }} 65 | navigation={true} 66 | cssMode={true} 67 | allowTouchMove={false} 68 | className='w-full -mt-5 lg:-mt-10 xl:-mt-20 2xl:-mt-28' 69 | // className='w-full' 70 | > 71 | {data 72 | .sort((a, b) => Math.random() - 0.5) 73 | .map((movie) => ( 74 | 75 | 76 | 77 | ))} 78 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default MovieList; 85 | -------------------------------------------------------------------------------- /components/AuthFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsGlobe, BsChevronDown } from 'react-icons/bs'; 3 | import { HiOutlineChevronDown } from 'react-icons/hi'; 4 | 5 | const AuthFooter = () => { 6 | return ( 7 |
8 |
9 |

10 | Questions? Email me at{' '} 11 | 15 | contact@salimi.my 16 | 17 |

18 |
    19 |
  • 20 |

    FAQ

    21 |
  • 22 |
  • 23 |

    Help Center

    24 |
  • 25 |
  • 26 |

    Terms of Use

    27 |
  • 28 |
  • 29 |

    Privacy

    30 |
  • 31 |
  • 32 |

    Cookie Preferences

    33 |
  • 34 |
  • 35 |

    36 | Corporate Information 37 |

    38 |
  • 39 |
40 |
41 |
42 |
43 | 44 |

English

45 |
46 | 47 |
48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default AuthFooter; 55 | -------------------------------------------------------------------------------- /components/AccountMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { signOut } from 'next-auth/react'; 3 | import Image from 'next/image'; 4 | import { AiFillCaretUp } from 'react-icons/ai'; 5 | import PencilIcon from './svg/PencilIcon'; 6 | import ExitIcon from './svg/ExitIcon'; 7 | import TransferIcon from './svg/TransferIcon'; 8 | import AccountIcon from './svg/AccountIcon'; 9 | import HelpIcon from './svg/HelpIcon'; 10 | import useCurrentUser from '../hooks/useCurrentUser'; 11 | 12 | interface AccountMenuProps { 13 | visible?: boolean; 14 | } 15 | 16 | const AccountMenu: React.FC = ({ visible }) => { 17 | const { data } = useCurrentUser(); 18 | 19 | if (!visible) { 20 | return null; 21 | } 22 | 23 | return ( 24 | <> 25 | 26 |
27 |
28 |
29 | Profile 36 |

37 | {data?.name} 38 |

39 |
40 |
41 | 42 |

43 | Manage Profiles 44 |

45 |
46 |
47 | 48 |

49 | Exit Profile 50 |

51 |
52 |
53 | 54 |

55 | Transfer Profile 56 |

57 |
58 |
59 | 60 |

61 | Account 62 |

63 |
64 |
65 | 66 |

67 | Help Center 68 |

69 |
70 |
71 |
signOut()} 73 | className='px-3 text-center text-white text-sm hover:underline' 74 | > 75 | Sign out of Netfuix 76 |
77 |
78 |
79 | 80 | ); 81 | }; 82 | 83 | export default AccountMenu; 84 | -------------------------------------------------------------------------------- /components/Billboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useCallback, useState } from 'react'; 2 | import useBillboard from '../hooks/useBillboard'; 3 | import { AiOutlineInfoCircle } from 'react-icons/ai'; 4 | import PlayButton from './PlayButton'; 5 | import useInfoModal from '../hooks/useInfoModal'; 6 | import VolumeButton from './VolumeButton'; 7 | 8 | const Billboard = () => { 9 | const { data } = useBillboard(); 10 | const videoRef = useRef(null); 11 | const { openModal } = useInfoModal(); 12 | 13 | const [isMute, setIsMute] = useState(true); 14 | 15 | const handleOpenModal = useCallback(() => { 16 | openModal(data?.id); 17 | }, [openModal, data?.id]); 18 | 19 | useEffect(() => { 20 | const timer = setTimeout(() => { 21 | if (videoRef != null) { 22 | videoRef.current?.play(); 23 | } 24 | }, 3000); 25 | 26 | return () => clearTimeout(timer); 27 | }, []); 28 | 29 | const handleMuteVideo = () => { 30 | setIsMute(!isMute); 31 | }; 32 | 33 | return ( 34 |
35 |
36 | 44 |
45 |
46 |
47 |
48 |

49 | {data?.title} 50 |

51 |

52 | {data?.description} 53 |

54 |
55 | 56 | 63 |
64 | 65 |
66 | 13+ 67 |
68 |
69 |
70 |
71 |
72 | ); 73 | }; 74 | 75 | export default Billboard; 76 | -------------------------------------------------------------------------------- /components/DisclaimerModal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from '@headlessui/react'; 2 | import { Fragment, useState } from 'react'; 3 | 4 | export default function MyModal() { 5 | let [isOpen, setIsOpen] = useState(true); 6 | 7 | function closeModal() { 8 | setIsOpen(false); 9 | } 10 | 11 | function openModal() { 12 | setIsOpen(true); 13 | } 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 28 |
29 | 30 | 31 |
32 |
33 | 42 | 43 | 47 | This is NOT REAL Netflix 48 | 49 |
50 |

51 | This site is not the real Netflix. I created this 52 | site for educational purposes only. Under no circumstance 53 | shall I have any liability to you for any loss or damage 54 | of any kind incurred as a result of the use of the site or 55 | reliance on any information provided on the site. Your use 56 | of the site and your reliance on any information on the 57 | site is solely at your own risk. 58 |

59 |
60 | 61 |
62 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | ); 78 | } 79 | 80 | function Alert() { 81 | return ( 82 | 90 | 95 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import NetfuixLogo from './svg/NetfuixLogo'; 3 | import NavbarItem from './NavbarItem'; 4 | import MobileMenu from './MobileMenu'; 5 | import { AiFillCaretDown } from 'react-icons/ai'; 6 | import SearchIcon from './svg/SearchIcon'; 7 | import BellIcon from './svg/BellIcon'; 8 | import Image from 'next/image'; 9 | import AccountMenu from './AccountMenu'; 10 | 11 | const TOP_OFFSET = 66; 12 | 13 | const Navbar = () => { 14 | const [showMobileMenu, setShowMobileMenu] = useState(false); 15 | const [showAccountMenu, setShowAccountMenu] = useState(false); 16 | const [showBackground, setShowBackground] = useState(false); 17 | 18 | useEffect(() => { 19 | const handleScroll = () => { 20 | if (window.scrollY >= TOP_OFFSET) { 21 | setShowBackground(true); 22 | } else { 23 | setShowBackground(false); 24 | } 25 | }; 26 | 27 | window.addEventListener('scroll', handleScroll); 28 | 29 | return () => { 30 | window.removeEventListener('scroll', handleScroll); 31 | }; 32 | }, []); 33 | 34 | const toggleMobileMenu = useCallback(() => { 35 | setShowMobileMenu((current) => !current); 36 | }, []); 37 | 38 | const toggleAccountMenu = useCallback(() => { 39 | setShowAccountMenu((current) => !current); 40 | }, []); 41 | 42 | return ( 43 | 103 | ); 104 | }; 105 | 106 | export default Navbar; 107 | -------------------------------------------------------------------------------- /components/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import { AiOutlineClose } from 'react-icons/ai'; 3 | 4 | import PlayButton from './PlayButton'; 5 | import FavouriteButton from './FavouriteButton'; 6 | import useInfoModal from '../hooks/useInfoModal'; 7 | import useMovie from '../hooks/useMovie'; 8 | 9 | interface InfoModalProps { 10 | visible?: boolean; 11 | onClose: any; 12 | } 13 | 14 | const InfoModal: React.FC = ({ visible, onClose }) => { 15 | const [isVisible, setIsVisible] = useState(!!visible); 16 | 17 | const { movieId } = useInfoModal(); 18 | const { data = {} } = useMovie(movieId); 19 | 20 | useEffect(() => { 21 | setIsVisible(!!visible); 22 | }, [visible]); 23 | 24 | const handleClose = useCallback(() => { 25 | setIsVisible(false); 26 | setTimeout(() => { 27 | onClose(); 28 | }, 300); 29 | }, [onClose]); 30 | 31 | if (!visible) { 32 | return null; 33 | } 34 | 35 | return ( 36 |
37 |
38 |
43 |
44 |
69 | 70 |
71 |
72 |

New

73 |
74 | 13+ 75 |
76 |

{data?.duration}

77 |
78 | HD 79 |
80 |
81 |
82 |
83 | {data?.genre?.map((genre: any, index: any, genres: any) => { 84 | if (index + 1 === genres.length) { 85 | return ( 86 |
90 | {genre} 91 |
92 | ); 93 | } else { 94 | return ( 95 |
99 | {genre} 100 | 101 | • 102 | 103 |
104 | ); 105 | } 106 | })} 107 |
108 |
109 |

{data?.description}

110 |
111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default InfoModal; 118 | -------------------------------------------------------------------------------- /components/MovieCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import React from 'react'; 3 | import { BsFillPlayFill, BsChevronDown } from 'react-icons/bs'; 4 | import FavouriteButton from './FavouriteButton'; 5 | import { useRouter } from 'next/router'; 6 | import useInfoModal from '../hooks/useInfoModal'; 7 | 8 | interface MovieCardProps { 9 | data: Record; 10 | } 11 | 12 | const MovieCard: React.FC = ({ data }) => { 13 | const router = useRouter(); 14 | const { openModal } = useInfoModal(); 15 | 16 | return ( 17 |
18 | Movie 26 |
27 |
router.push(`/watch/${data?.id}`)} 29 | className='relative cursor-pointer object-cover transition duration shadow-md rounded-t-md w-full h-[79px] md:h-[101px] lg:h-[100px] xl:h-[91px] 2xl:h-[135px] overflow-hidden' 30 | > 31 | Movie 39 |
40 |
41 |
42 |
router.push(`/watch/${data?.id}`)} 44 | className='cursor-pointer w-6 h-6 lg:w-7 lg:h-7 bg-white rounded-full flex justify-center items-center transition hover:bg-neutral-300' 45 | > 46 | 47 |
48 | 49 |
openModal(data?.id)} 51 | className='cursor-pointer group/item w-6 h-6 lg:w-7 lg:h-7 bg-[rgba(42,42,42,.6)] border-[hsla(0,0%,100%,.5)] border-[0.12rem] rounded-full flex justify-center items-center transition hover:border-white ml-auto' 52 | > 53 | 54 |
55 |
56 |
57 |

New

58 |
59 | 13+ 60 |
61 |

{data?.duration}

62 |
63 | HD 64 |
65 |
66 |
67 |
68 | {data?.genre.map((genre: any, index: any, genres: any) => { 69 | if (index + 1 === genres.length) { 70 | return ( 71 |
75 | {genre} 76 |
77 | ); 78 | } else { 79 | return ( 80 |
84 | {genre} 85 | 86 | • 87 | 88 |
89 | ); 90 | } 91 | })} 92 |
93 |
94 |
95 |
96 |
97 | ); 98 | }; 99 | 100 | export default MovieCard; 101 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .bottom-vignette { 6 | background-color: transparent; 7 | background-image: linear-gradient( 8 | 180deg, 9 | hsla(0, 0%, 8%, 0) 0, 10 | hsla(0, 0%, 8%, 0.15) 15%, 11 | hsla(0, 0%, 8%, 0.35) 29%, 12 | hsla(0, 0%, 8%, 0.58) 44%, 13 | #141414 68%, 14 | #141414 15 | ); 16 | background-position: 0 top; 17 | background-repeat: repeat-x; 18 | background-size: 100% 100%; 19 | bottom: -1px; 20 | height: 14.7vw; 21 | opacity: 1; 22 | top: auto; 23 | width: 100%; 24 | } 25 | 26 | .left-vignette { 27 | background: linear-gradient(77deg, rgba(0, 0, 0, 0.6), transparent 85%); 28 | bottom: 0; 29 | left: 0; 30 | opacity: 1; 31 | position: absolute; 32 | right: 26.09%; 33 | top: 0; 34 | transition: opacity 0.5s; 35 | } 36 | 37 | .modal-vignette { 38 | height: 100%; 39 | position: absolute; 40 | top: 0; 41 | width: 100%; 42 | background: linear-gradient(0deg, #181818, transparent 50%); 43 | } 44 | 45 | body { 46 | @apply bg-dark-netfuix h-full; 47 | } 48 | 49 | #__next { 50 | @apply h-full; 51 | } 52 | 53 | html { 54 | @apply h-full; 55 | } 56 | 57 | /* .swiper-slide { 58 | width: 100%; 59 | height: 100%; 60 | } */ 61 | 62 | /* .swiper-slide:hover { 63 | width: 230px !important; 64 | } */ 65 | 66 | .swiper-button-next { 67 | right: 0 !important; 68 | } 69 | 70 | .swiper-button-prev { 71 | left: 0 !important; 72 | } 73 | 74 | .swiper-button-prev:after, 75 | .swiper-button-next:after { 76 | font-size: 28px !important; 77 | color: #ffffff; 78 | text-shadow: 3px 4px 7px rgba(81, 67, 21, 0.8); 79 | } 80 | 81 | .swiper-button-next:hover { 82 | opacity: 1; 83 | background-image: linear-gradient( 84 | to right, 85 | rgba(0, 0, 0, 0.6), 86 | rgba(0, 0, 0, 0.6) 87 | ); 88 | } 89 | 90 | .swiper-button-prev:hover { 91 | background-image: linear-gradient( 92 | to right, 93 | rgba(0, 0, 0, 0.6), 94 | rgba(0, 0, 0, 0.6) 95 | ); 96 | } 97 | 98 | .swiper-wrapper { 99 | padding: 30px 0; 100 | } 101 | 102 | .swiper-button-prev, 103 | .swiper-button-next { 104 | font-weight: bolder; 105 | width: 40px !important; 106 | height: 109px !important; 107 | top: 52px !important; 108 | } 109 | 110 | @media only screen and (min-width: 768px) { 111 | .swiper-wrapper { 112 | padding: 40px 0; 113 | } 114 | 115 | .swiper-button-prev, 116 | .swiper-button-next { 117 | font-weight: bolder; 118 | width: 60px !important; 119 | height: 131px !important; 120 | top: 62px !important; 121 | } 122 | } 123 | 124 | @media only screen and (min-width: 1024px) { 125 | .swiper-wrapper { 126 | padding: 60px 0; 127 | } 128 | 129 | .swiper-button-prev, 130 | .swiper-button-next { 131 | font-weight: bolder; 132 | width: 60px !important; 133 | height: 130px !important; 134 | top: 82px !important; 135 | } 136 | } 137 | 138 | @media only screen and (min-width: 1440px) { 139 | .swiper-wrapper { 140 | padding: 90px 0; 141 | } 142 | 143 | .swiper-button-prev, 144 | .swiper-button-next { 145 | font-weight: bolder; 146 | width: 60px !important; 147 | height: 122px !important; 148 | top: 112px !important; 149 | } 150 | } 151 | 152 | @media only screen and (min-width: 1536px) { 153 | .swiper-wrapper { 154 | padding: 129px 0; 155 | } 156 | 157 | .swiper-button-prev, 158 | .swiper-button-next { 159 | font-weight: bolder; 160 | width: 60px !important; 161 | height: 165px !important; 162 | top: 151px !important; 163 | } 164 | } 165 | 166 | .landing-bg { 167 | min-height: 30rem; 168 | background-image: linear-gradient( 169 | 103.24deg, 170 | rgba(0, 8, 29, 0.9) 23.83%, 171 | rgba(0, 8, 29, 0.3) 96.1% 172 | ), 173 | url('/images/hero.jpg'); 174 | padding: 0 1.5rem; 175 | } 176 | 177 | @media only screen and (min-width: 600px) { 178 | .landing-bg { 179 | min-height: 32rem; 180 | padding: 0 2rem; 181 | } 182 | } 183 | 184 | @media only screen and (min-width: 960px) { 185 | .landing-bg { 186 | min-height: 32rem; 187 | padding: 0 2rem; 188 | } 189 | } 190 | 191 | @media only screen and (min-width: 1280px) { 192 | .landing-bg { 193 | min-height: 43.75rem; 194 | padding: 0 3rem; 195 | } 196 | } 197 | 198 | .landing-vignette { 199 | display: -webkit-box; 200 | display: -webkit-flex; 201 | display: -ms-flexbox; 202 | display: flex; 203 | box-sizing: inherit; 204 | padding: 0px; 205 | margin-top: 0px; 206 | margin-left: 0px; 207 | position: absolute; 208 | bottom: 0; 209 | left: 0; 210 | height: 26.25rem; 211 | width: 100%; 212 | background: linear-gradient(180deg, rgba(0, 8, 29, 0) 0%, #00081d 93.46%); 213 | } 214 | 215 | .gradient-box { 216 | background: linear-gradient(151.6deg, #e40913 -46.64%, #181049 48.45%); 217 | } 218 | 219 | @media only screen and (min-width: 600px) { 220 | .gradient-box { 221 | background: linear-gradient(151.6deg, #e40913 -46.64%, #181049 48.45%); 222 | } 223 | } 224 | 225 | @media only screen and (min-width: 960px) { 226 | .gradient-box { 227 | background: linear-gradient(152.34deg, #730a2f -1.4%, #170f48 60.78%); 228 | } 229 | } 230 | 231 | @media only screen and (min-width: 1280px) { 232 | .gradient-box { 233 | background: linear-gradient(153.61deg, #e40913 -192.17%, #170f48 80.89%); 234 | } 235 | } 236 | 237 | @media only screen and (min-width: 1920px) { 238 | .gradient-box { 239 | background: linear-gradient(154.01deg, #e40913 -78.21%, #181049 75.33%); 240 | } 241 | } 242 | 243 | details summary { 244 | transition-duration: 250ms; 245 | transition-property: background-color; 246 | transition-timing-function: cubic-bezier(0.9, 0, 0.51, 1); 247 | } 248 | 249 | details summary::-webkit-details-marker { 250 | display: none; 251 | } 252 | 253 | details summary:hover { 254 | transition-timing-function: cubic-bezier(0.5, 0, 0.1, 1); 255 | background-color: rgba(34, 51, 98, 1); 256 | } 257 | 258 | details[open] summary { 259 | background: rgba(19, 33, 68, 1); 260 | color: white; 261 | } 262 | 263 | details[open] summary::after { 264 | transform: rotate(-45deg); 265 | } 266 | 267 | details[open] summary ~ * { 268 | animation: slideDown 0.3s ease-in-out; 269 | } 270 | 271 | details[open] summary p { 272 | opacity: 0; 273 | animation-name: showContent; 274 | animation-duration: 0.6s; 275 | animation-delay: 0.2s; 276 | animation-fill-mode: forwards; 277 | margin: 0; 278 | } 279 | 280 | @keyframes showContent { 281 | from { 282 | opacity: 0; 283 | height: 0; 284 | } 285 | to { 286 | opacity: 1; 287 | height: auto; 288 | } 289 | } 290 | @keyframes slideDown { 291 | from { 292 | opacity: 0; 293 | height: 0; 294 | padding: 0; 295 | } 296 | 297 | to { 298 | opacity: 1; 299 | height: auto; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /pages/auth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import NetfuixLogo from '../components/svg/NetfuixLogo'; 3 | import Input from '../components/Input'; 4 | import axios from 'axios'; 5 | import { signIn } from 'next-auth/react'; 6 | 7 | import { FcGoogle } from 'react-icons/fc'; 8 | import { FaGithub } from 'react-icons/fa'; 9 | import AuthFooter from '../components/AuthFooter'; 10 | import Link from 'next/link'; 11 | import DisclaimerModal from '../components/DisclaimerModal'; 12 | import SpinnerIcon from '../components/svg/SpinnerIcon'; 13 | import { useRouter } from 'next/router'; 14 | 15 | const Auth = () => { 16 | const [email, setEmail] = useState(''); 17 | const [name, setName] = useState(''); 18 | const [password, setPassword] = useState(''); 19 | 20 | const [variant, setVariant] = useState('login'); 21 | const [loggingIn, setLoggingIn] = useState(false); 22 | 23 | const { error } = useRouter().query; 24 | 25 | const toggleVariant = useCallback(() => { 26 | setVariant((currentVariant) => 27 | currentVariant == 'login' ? 'register' : 'login' 28 | ); 29 | }, []); 30 | 31 | const login = useCallback(async () => { 32 | setLoggingIn(true); 33 | try { 34 | await signIn('credentials', { 35 | email, 36 | password, 37 | callbackUrl: '/profile' 38 | }); 39 | } catch (error) { 40 | console.log(error); 41 | } 42 | }, [email, password]); 43 | 44 | const register = useCallback(async () => { 45 | setLoggingIn(true); 46 | try { 47 | await axios.post('/api/register', { 48 | email, 49 | name, 50 | password 51 | }); 52 | 53 | login(); 54 | } catch (error) { 55 | console.log(error); 56 | } 57 | }, [email, name, password, login]); 58 | 59 | return ( 60 | <> 61 |
62 |
63 | 68 |
69 |
70 |

71 | {variant == 'login' ? 'Sign In' : 'Sign Up'} 72 |

73 | {error && ( 74 |

75 | {error} 76 |

77 | )} 78 |
79 | {variant == 'register' && ( 80 | }; 84 | }) => setName(e.target.value)} 85 | id='name' 86 | value={name} 87 | /> 88 | )} 89 | }; 93 | }) => setEmail(e.target.value)} 94 | id='email' 95 | type='email' 96 | value={email} 97 | /> 98 | }; 102 | }) => setPassword(e.target.value)} 103 | id='password' 104 | type='password' 105 | value={password} 106 | /> 107 |
108 | 121 | 122 |
123 |

124 | Or continue with 125 |

126 |
127 | 128 |
129 | 142 | 158 |
159 | 160 |

161 | {variant == 'login' 162 | ? 'New to Netfuix?' 163 | : 'Already have an account?'} 164 | 168 | {variant == 'login' ? 'Sign up now' : 'Sign in here'} 169 | 170 | . 171 |

172 |
173 |
174 |
175 |
176 | 177 |
178 |
179 | 180 | 181 | ); 182 | }; 183 | 184 | export default Auth; 185 | -------------------------------------------------------------------------------- /movies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": { "$oid": "test_id_1" }, 4 | "title": "Big Buck Bunny", 5 | "description": "Three rodents amuse themselves by harassing creatures of the forest. However, when they mess with a bunny, he decides to teach them a lesson.", 6 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684221792/netflix-clone-video/big-buck-bunny_pyiwx7.mp4", 7 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218092/video-poster/big-buck-bunny_jutuum.jpg", 8 | "genre": ["Comedy", "Animation", "Short"], 9 | "duration": "0h 10m" 10 | }, 11 | { 12 | "_id": { "$oid": "test_id_2" }, 13 | "title": "Sintel", 14 | "description": "A lonely young woman, Sintel, helps and befriends a dragon, whom she calls Scales. But when he is kidnapped by an adult dragon, Sintel decides to embark on a dangerous quest to find her lost friend Scales.", 15 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684225261/netflix-clone-video/sintel_mxhyer.mp4", 16 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218091/video-poster/sintel_uixa0t.jpg", 17 | "genre": ["Fantasy", "Animation", "Short"], 18 | "duration": "0h 15m" 19 | }, 20 | { 21 | "_id": { "$oid": "test_id_3" }, 22 | "title": "Tears of Steel", 23 | "description": "In an apocalyptic future, a group of soldiers and scientists takes refuge in Amsterdam to try to stop an army of robots that threatens the planet.", 24 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684225002/netflix-clone-video/tears-of-steel_hkqisy.mp4", 25 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218088/video-poster/tears-of-steel_jw42td.jpg", 26 | "genre": ["Sci-Fi", "Short"], 27 | "duration": "0h 12m" 28 | }, 29 | { 30 | "_id": { "$oid": "test_id_4" }, 31 | "title": "Elephants Dream", 32 | "description": "Friends Proog and Emo journey inside the folds of a seemingly infinite Machine, exploring the dark and twisted complex of wires, gears, and cogs, until a moment of conflict negates all their assumptions.", 33 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684223216/netflix-clone-video/elephants-dream_rpmcc2.mp4", 34 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218092/video-poster/elephants-dream_ikowjk.jpg", 35 | "genre": ["Sci-Fi", "Animation", "Short"], 36 | "duration": "0h 15m" 37 | }, 38 | { 39 | "_id": { "$oid": "test_id_5" }, 40 | "title": "Spring", 41 | "description": "Spring is the story of a shepherd girl and her dog, who face ancient spirits in order to continue the cycle of life. This poetic and visually stunning short film was written and directed by Andy Goralczyk, inspired by his childhood in the mountains of Germany.", 42 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684221803/netflix-clone-video/spring_osiq4o.mp4", 43 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218090/video-poster/spring_vnewrb.jpg", 44 | "genre": ["Adventure", "Animation", "Short"], 45 | "duration": "0h 8m" 46 | }, 47 | { 48 | "_id": { "$oid": "test_id_6" }, 49 | "title": "Cosmos Laundromat", 50 | "description": "On a desolate island, suicidal sheep Franck meets his fate in a quirky salesman, who offers him the gift of a lifetime. Little does he know that he can only handle this much 'lifetime'.", 51 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684224756/netflix-clone-video/cosmos-laundromat-first-cycle_k0b2db.mp4", 52 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218095/video-poster/cosmos-laundromat-first-cycle_vwmmik.jpg", 53 | "genre": ["Fantasy", "Animation", "Short"], 54 | "duration": "0h 12m" 55 | }, 56 | { 57 | "_id": { "$oid": "test_id_7" }, 58 | "title": "Sprite Fright", 59 | "description": "When a group of rowdy teenagers trek into an isolated forest, they discover peaceful mushroom creatures that turn out to be an unexpected force of nature.", 60 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684224514/netflix-clone-video/sprite-fright_d827yg.mp4", 61 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684218089/video-poster/sprite-fright_flxxw9.jpg", 62 | "genre": ["Horror", "Comedy", "Short"], 63 | "duration": "0h 10m" 64 | }, 65 | { 66 | "_id": { "$oid": "test_id_8" }, 67 | "title": "Charge", 68 | "description": "In an energy-scarce dystopia, an old destitute man breaks into a battery factory but soon finds himself confronted by a deadly security droid and no way out.", 69 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684379273/netflix-clone-video/charge_dsjn1a.mp4", 70 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684379388/video-poster/charge_vj4f3n.jpg", 71 | "genre": ["Action", "Sci-Fi", "Short"], 72 | "duration": "0h 5m" 73 | }, 74 | { 75 | "_id": { "$oid": "test_id_9" }, 76 | "title": "Coffee Run", 77 | "description": "Fueled by caffeine, a young woman runs through the bittersweet memories of her past relationship.", 78 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684379268/netflix-clone-video/coffee-run_d7nzcv.mp4", 79 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684379388/video-poster/coffee-run_h1ayag.jpg", 80 | "genre": ["Animation", "Short"], 81 | "duration": "0h 3m" 82 | }, 83 | { 84 | "_id": { "$oid": "test_id_10" }, 85 | "title": "Hero", 86 | "description": "Hero is a showcase for the upcoming Grease Pencil in Blender 2.8. Grease Pencil means 2D animation tools within a full 3D pipeline.", 87 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684379279/netflix-clone-video/hero_krl0fp.mp4", 88 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684379387/video-poster/hero_wakye1.jpg", 89 | "genre": ["Animation", "Action", "Short"], 90 | "duration": "0h 4m" 91 | }, 92 | { 93 | "_id": { "$oid": "test_id_11" }, 94 | "title": "Agent 327", 95 | "description": "Agent 327 is investigating a clue that leads him to a shady barbershop in Amsterdam. Little does he know that he is being tailed by mercenary Boris Kloris.", 96 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684379258/netflix-clone-video/agent-327_b7dqhy.mp4", 97 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684379388/video-poster/agent-327_vupg4z.jpg", 98 | "genre": ["Animation", "Short"], 99 | "duration": "0h 4m" 100 | }, 101 | { 102 | "_id": { "$oid": "test_id_12" }, 103 | "title": "Caminandes: Gran Dillama", 104 | "description": "A young llama named Koro discovers that the grass is always greener on the other side (of the fence).", 105 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684379278/netflix-clone-video/caminandes-gran-dillama_hyyjjv.mp4", 106 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684379388/video-poster/caminandes-gran-dillama_w8ldhs.jpg", 107 | "genre": ["Animation", "Comedy", "Short"], 108 | "duration": "0h 3m" 109 | }, 110 | { 111 | "_id": { "$oid": "test_id_13" }, 112 | "title": "Caminandes: Llamigos", 113 | "description": "It's winter in Patagonia, food is getting scarce. Koro the Llama engages with Oti the pesky penguin in an epic fight over that last tasty berry.", 114 | "videoUrl": "https://res.cloudinary.com/salimi/video/upload/v1684379277/netflix-clone-video/caminandes-llamigos_xe1kgg.mp4", 115 | "thumbnailUrl": "https://res.cloudinary.com/salimi/image/upload/v1684379388/video-poster/caminandes-llamigos_sgtzpf.jpg", 116 | "genre": ["Adventure", "Comedy", "Short"], 117 | "duration": "0h 2m" 118 | } 119 | ] 120 | -------------------------------------------------------------------------------- /public/images/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /public/images/tv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /public/images/popcorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /public/images/crystalball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Link from 'next/link'; 3 | import NetfuixLogo from '../components/svg/NetfuixLogo'; 4 | import { SlGlobe } from 'react-icons/sl'; 5 | import { AiFillCaretDown } from 'react-icons/ai'; 6 | import { FiChevronRight } from 'react-icons/fi'; 7 | import Image from 'next/image'; 8 | import { useRouter } from 'next/router'; 9 | import DisclaimerModal from '../components/DisclaimerModal'; 10 | 11 | const Home: NextPage = () => { 12 | const router = useRouter(); 13 | 14 | return ( 15 | <> 16 |
17 |
18 | 40 |
41 |
42 |

43 | Unlimited movies, TV shows, and more 44 |

45 |
46 |
47 |

48 | Watch anywhere. Cancel anytime. 49 |

50 |
51 |
52 |
53 |
{ 55 | e.preventDefault(); 56 | router.push('/auth'); 57 | }} 58 | className='flex flex-col' 59 | > 60 |

61 | Ready to watch? Enter your email to create or restart your 62 | membership. 63 |

64 |
65 |
66 | 72 | 78 |
79 | 87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | TV 105 |

106 | Enjoy on your TV 107 |

108 |

109 | Watch on Smart TVs, Playstation, Xbox, Chromecast, Apple TV, 110 | Blu-ray players, and more. 111 |

112 |
113 |
114 |
115 |
116 | Popcorn 122 |

123 | Watch everywhere 124 |

125 |

126 | Stream unlimited movies and TV shows on your phone, tablet, 127 | laptop, and TV. 128 |

129 |
130 |
131 |
132 |
133 | Crystal ball 139 |

140 | Create profiles for kids 141 |

142 |

143 | Send kids on adventures with their favorite characters in a 144 | space made just for them—free with your membership. 145 |

146 |
147 |
148 |
149 |
150 | Download 156 |

157 | Download your shows to watch offline 158 |

159 |

160 | Save your favorites easily and always have something to 161 | watch. 162 |

163 |
164 |
165 |
166 |
167 |
168 |

169 | Frequently Asked Questions 170 |

171 |
172 |
173 |
174 | 175 | What is Netfuix? 176 | 177 |

178 | Netfuix is a streaming service that offers a wide 179 | variety of award-winning TV shows, movies, anime, 180 | documentaries, and more on thousands of 181 | internet-connected devices. 182 |

183 |

184 | You can watch as much as you want, whenever you want 185 | without a single commercial - all for one low monthly 186 | price. There's always something new to discover and 187 | new TV shows and movies are added every week! 188 |

189 |
190 |
191 | 192 | How much does Netfuix cost? 193 | 194 |

195 | Watch Netfuix on your smartphone, tablet, Smart TV, 196 | laptop, or streaming device, all for one fixed monthly 197 | fee. Plans range from RM17 to RM55 a month. No extra 198 | costs, no contracts. 199 |

200 |
201 |
202 | 203 | Where can I watch? 204 | 205 |

206 | Watch anywhere, anytime. Sign in with your Netfuix 207 | account to watch instantly on the web at netfuix.com 208 | from your personal computer or on any internet-connected 209 | device that offers the Netfuix app, including smart TVs, 210 | smartphones, tablets, streaming media players and game 211 | consoles. 212 |

213 |

214 | You can also download your favorite shows with the iOS, 215 | Android, or Windows 10 app. Use downloads to watch while 216 | you're on the go and without an internet 217 | connection. Take Netfuix with you anywhere. 218 |

219 |
220 |
221 | 222 | How do I cancel? 223 | 224 |

225 | Netfuix is flexible. There are no pesky contracts and no 226 | commitments. You can easily cancel your account online 227 | in two clicks. There are no cancellation fees - start or 228 | stop your account anytime. 229 |

230 |
231 |
232 | 233 | What can I watch on Netfuix? 234 | 235 |

236 | Netfuix has an extensive library of feature films, 237 | documentaries, TV shows, anime, award-winning Netfuix 238 | originals, and more. Watch as much as you want, anytime 239 | you want. 240 |

241 |
242 |
243 | 244 | Is Netfuix good for kids? 245 | 246 |

247 | The Netfuix Kids experience is included in your 248 | membership to give parents control while kids enjoy 249 | family-friendly TV shows and movies in their own space. 250 |

251 |

252 | Kids profiles come with PIN-protected parental controls 253 | that let you restrict the maturity rating of content 254 | kids can watch and block specific titles you don’t want 255 | kids to see. 256 |

257 |
258 |
259 |
260 |
261 |
262 |
{ 264 | e.preventDefault(); 265 | router.push('/auth'); 266 | }} 267 | className='flex flex-col' 268 | > 269 |

270 | Ready to watch? Enter your email to create or restart 271 | your membership. 272 |

273 |
274 |
275 | 281 | 287 |
288 | 296 |
297 |
298 |
299 |
300 | 301 |
302 |
303 |

304 | Questions? Email me at{' '} 305 | 309 | contact@salimi.my 310 | 311 |

312 |
313 |
    314 |
  • 315 | FAQ 316 |
  • 317 |
  • 318 | Help Center 319 |
  • 320 |
  • 321 | Account 322 |
  • 323 |
  • 324 | Media Center 325 |
  • 326 |
  • 327 | Investor Relations 328 |
  • 329 |
  • 330 | Jobs 331 |
  • 332 |
  • 333 | Redeem Gift Cards 334 |
  • 335 |
  • 336 | Buy Gift Cards 337 |
  • 338 |
  • 339 | Ways to Watch 340 |
  • 341 |
  • 342 | Terms of Use 343 |
  • 344 |
  • 345 | Privacy 346 |
  • 347 |
  • 348 | Cookie Preferences 349 |
  • 350 |
  • 351 | Corporate Information 352 |
  • 353 |
  • 354 | Contact Us 355 |
  • 356 |
  • 357 | Speed Test 358 |
  • 359 |
  • 360 | Legal Notices 361 |
  • 362 |
  • 363 | Only on Netflix 364 |
  • 365 |
366 |
367 |
368 |
369 |
370 |
371 | 372 |

English

373 |
374 | 375 |
376 |
377 |

378 | Netfuix. Created by{' '} 379 | 380 | Salimi 381 | 382 |

383 |
384 |
385 |
386 |
387 |
388 |
389 | 390 | 391 | ); 392 | }; 393 | 394 | export default Home; 395 | --------------------------------------------------------------------------------