├── app ├── favicon.ico ├── (root) │ ├── (home) │ │ └── page.tsx │ ├── tv │ │ └── page.tsx │ ├── movies │ │ └── page.tsx │ ├── browse │ │ └── page.tsx │ ├── search │ │ └── [query] │ │ │ └── page.tsx │ └── mylist │ │ └── page.tsx ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── account │ │ ├── login │ │ │ └── route.ts │ │ └── route.ts │ └── favourite │ │ └── route.ts ├── layout.tsx └── globals.css ├── postcss.config.js ├── .idea ├── .gitignore ├── vcs.xml ├── modules.xml ├── netflix.iml └── inspectionProfiles │ └── Project_Default.xml ├── components ├── shared │ ├── loader.tsx │ ├── custom-image.tsx │ ├── common.tsx │ ├── movie │ │ ├── movie-row.tsx │ │ ├── movie-item.tsx │ │ └── movie-popup.tsx │ ├── login.tsx │ ├── navbar │ │ ├── search-bar.tsx │ │ └── index.tsx │ ├── banner.tsx │ └── manage-account.tsx ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── popover.tsx │ ├── button.tsx │ ├── dialog.tsx │ ├── use-toast.ts │ ├── form.tsx │ └── toast.tsx └── form │ ├── login-account-form.tsx │ └── create-account-form.tsx ├── lib ├── utils.ts ├── validation.ts ├── mongoose.ts └── api.ts ├── next.config.js ├── database ├── account.ts └── favourite.ts ├── components.json ├── constants └── index.ts ├── provider └── index.tsx ├── .env ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── context └── index.tsx ├── README.md ├── package.json ├── tailwind.config.ts └── types └── index.d.ts /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samarbadriddin0v/netflix-web/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /components/shared/loader.tsx: -------------------------------------------------------------------------------- 1 | export default function Loader(){ 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /app/(root)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {redirect} from "next/navigation"; 3 | 4 | const Page = () => { 5 | return redirect('/browse') 6 | }; 7 | 8 | export default Page; -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /lib/validation.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const createAccountSchema = z.object({ 4 | name: z.string().min(2).max(8), 5 | pin: z.string().min(4).max(4) 6 | }) 7 | 8 | export const loginSchema = z.object({ 9 | pin: z.string().min(4).max(4) 10 | }) -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | {protocol: "https", hostname: "*"}, 6 | {protocol: "http", hostname: "*"} 7 | ] 8 | } 9 | } 10 | 11 | module.exports = nextConfig 12 | -------------------------------------------------------------------------------- /database/account.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const accountSchema = new mongoose.Schema({ 4 | uid: String, 5 | name: String, 6 | pin: String, 7 | }, {timestamps: true}); 8 | 9 | const Account = mongoose.models.Account || mongoose.model("Account", accountSchema); 10 | export default Account; 11 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /database/favourite.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const favouriteSchema = new mongoose.Schema({ 4 | uid: String, 5 | accountId: String, 6 | backdrop_path: String, 7 | poster_path: String, 8 | movieId: String, 9 | type: String, 10 | title: String, 11 | overview: String, 12 | }, {timestamps: true}) 13 | 14 | const Favourite = mongoose.models.Favourite || mongoose.model('Favourite', favouriteSchema); 15 | export default Favourite; -------------------------------------------------------------------------------- /constants/index.ts: -------------------------------------------------------------------------------- 1 | import {MenuItemProps} from "@/types"; 2 | 3 | export const menuItems: MenuItemProps[] = [ 4 | { 5 | id: 'home', 6 | title: 'Home', 7 | path: '/browse' 8 | }, 9 | { 10 | id: 'tv', 11 | title: 'TV Shows', 12 | path: '/tv' 13 | }, 14 | { 15 | id: 'movies', 16 | title: 'Movies', 17 | path: '/movies' 18 | }, 19 | { 20 | id: 'my-list', 21 | title: 'My List', 22 | path: '/mylist' 23 | }, 24 | ] -------------------------------------------------------------------------------- /provider/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | import {SessionProvider} from "next-auth/react"; 7 | 8 | export function Provider({ children, ...props }: ThemeProviderProps) { 9 | return 10 | 11 | {children} 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.idea/netflix.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # NEXT AUTH 2 | GITHUB_CLIENT_ID=5be13b189880cc43616e 3 | GITHUB_CLIENT_SECRET=aa5ebfbceb819151d6d1c20956f400780f52941b 4 | NEXTAUTH_URL=http://localhost:3000 5 | SECRET_KEY=secret 6 | 7 | # DATABASE 8 | MONGODB_URL=mongodb+srv://info:xnY7b6kqcFuc5FwH@cluster0.lr2o4ih.mongodb.net/?retryWrites=true&w=majority 9 | 10 | # THE MOVIE DB API KEY 11 | NEXT_PUBLIC_TMDB_API_KEY=7db5efb642c5d47759bd2c2b413fbea8 12 | NEXT_PUBLIC_TMDB_BASE_URL=https://api.themoviedb.org/3 13 | NEXT_PUBLIC_TMDB_IMAGE_BASE_URL=https://image.tmdb.org/t/p/original 14 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.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 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /lib/mongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose, {ConnectOptions} from "mongoose"; 2 | 3 | let isConnected: boolean = false; 4 | 5 | export const connectToDatabase = async () => { 6 | mongoose.set("strictQuery", true); 7 | 8 | if(!process.env.MONGODB_URL) { 9 | throw new Error("MONGODB_URL not found"); 10 | } 11 | 12 | if(isConnected) { 13 | return; 14 | } 15 | 16 | try { 17 | const options: ConnectOptions = { 18 | dbName: "netflix", 19 | autoCreate: true, 20 | } 21 | 22 | await mongoose.connect(process.env.MONGODB_URL, options); 23 | 24 | isConnected = true; 25 | console.log("MongoDB connected"); 26 | }catch (e) { 27 | console.log("MongoDB connection error. Please make sure MongoDB is running"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, {NextAuthOptions} from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | 4 | const authOptions: NextAuthOptions = { 5 | providers: [ 6 | GithubProvider({ 7 | clientId: process.env.GITHUB_CLIENT_ID as string, 8 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string 9 | }) 10 | ], 11 | callbacks: { 12 | async session({session, token}: any) { 13 | session.user.username = session?.user?.name.split(" ").join("").toLowerCase(); 14 | session.user.uid = token.sub; 15 | return session 16 | } 17 | }, 18 | secret: process.env.SECRET_KEY, 19 | } 20 | 21 | const handler = NextAuth(authOptions); 22 | export {handler as GET, handler as POST}; 23 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/shared/custom-image.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {useState} from "react"; 4 | import Image from "next/image"; 5 | import {cn} from "@/lib/utils"; 6 | 7 | interface Props { 8 | image: string; 9 | alt: string; 10 | className?: string; 11 | onClick?: () => void; 12 | } 13 | 14 | 15 | const CustomImage = ({image, alt, className, onClick}: Props) => { 16 | const [isLoading, setIsLoading] = useState(true) 17 | 18 | return ( 19 | {alt} setIsLoading(false)} 29 | onClick={onClick} 30 | /> 31 | ); 32 | }; 33 | 34 | export default CustomImage; -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | import type { Metadata } from 'next' 4 | import { Inter } from 'next/font/google' 5 | import {Provider} from "@/provider"; 6 | import GlobalContext from "@/context"; 7 | import {ReactNode} from "react"; 8 | import {Toaster} from "@/components/ui/toaster"; 9 | 10 | const inter = Inter({ subsets: ['latin'] }) 11 | 12 | export const metadata: Metadata = { 13 | title: 'Netflix Clone', 14 | description: 'Netflix Clone built with Next.js', 15 | } 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: { 20 | children: ReactNode 21 | }) { 22 | return ( 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/shared/common.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Navbar from "@/components/shared/navbar"; 4 | import {MovieDataProps} from "@/types"; 5 | import Banner from "@/components/shared/banner"; 6 | import MovieRow from "@/components/shared/movie/movie-row"; 7 | 8 | interface Props { 9 | moviesData: MovieDataProps[] 10 | } 11 | 12 | const Common = ({moviesData}: Props) => { 13 | return ( 14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | {moviesData && moviesData.map((movie => ( 22 | 27 | )))} 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Common; -------------------------------------------------------------------------------- /app/api/account/login/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from "next/server"; 2 | import {connectToDatabase} from "@/lib/mongoose"; 3 | import Account from "@/database/account"; 4 | import {compare} from "bcryptjs"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export async function POST(req: Request) { 9 | try { 10 | await connectToDatabase() 11 | // @ts-ignore 12 | const {pin, uid, accountId} = await req.json(); 13 | 14 | const currentAccount = await Account.findOne({_id: accountId, uid}) 15 | 16 | if(!currentAccount) { 17 | return NextResponse.json({success: false, message: "Account not found"}) 18 | } 19 | 20 | const isMatch = await compare(pin, currentAccount.pin) 21 | 22 | if(isMatch) { 23 | return NextResponse.json({success: true, data: currentAccount}) 24 | }else { 25 | return NextResponse.json({success: false, message: "Incorrect pin"}) 26 | } 27 | 28 | }catch (e) { 29 | return NextResponse.json({success: false, message: "Something went wrong"}) 30 | } 31 | } -------------------------------------------------------------------------------- /components/shared/movie/movie-row.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {MovieProps} from "@/types"; 4 | import MovieItem from "@/components/shared/movie/movie-item"; 5 | 6 | interface Props { 7 | title: string 8 | data: MovieProps[] 9 | } 10 | 11 | const MovieRow = ({title, data}: Props) => { 12 | return ( 13 |
14 |

15 | {title} 16 |

17 | 18 |
19 |
20 | {data && data.filter(item => item.backdrop_path !== null && item.poster_path !== null).map((movie => ( 21 | 25 | )))} 26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default MovieRow; -------------------------------------------------------------------------------- /components/shared/login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from "next/image"; 3 | import {Button} from "@/components/ui/button"; 4 | import {AiFillGithub} from "react-icons/ai"; 5 | import {signIn} from "next-auth/react"; 6 | 7 | const Login = () => { 8 | return ( 9 |
10 |
11 | {"bg"} 16 |
17 |
18 |
19 | 26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Login; -------------------------------------------------------------------------------- /context/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {createContext, useContext, useEffect, useState} from "react"; 4 | import {AccountProps, ChildProps, ContextType, MovieProps} from "@/types"; 5 | 6 | export const Context = createContext(null) 7 | 8 | const GlobalContext = ({children}: ChildProps) => { 9 | const [account, setAccount] = useState(null) 10 | const [pageLoader, setPageLoader] = useState(true) 11 | const [open, setOpen] = useState(false) 12 | const [movie, setMovie] = useState(null) 13 | 14 | useEffect(() => { 15 | setAccount(JSON.parse(sessionStorage.getItem("account")!)) 16 | }, []) 17 | 18 | return ( 19 | 30 | {children} 31 | 32 | ); 33 | }; 34 | 35 | export default GlobalContext; 36 | 37 | export const useGlobalContext = () => { 38 | const context = useContext(Context) 39 | if (context === null) { 40 | throw new Error('useGlobalContext must be used within a GlobalContext') 41 | } 42 | return context 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Discover Seamless Streaming: Netflix Clone with Next.js Node.js MongoDB & Tailwind CSS

2 | 3 |

project-image

4 | 5 |

Indulge in our Netflix clone: ultra-fast streaming tailored recommendations and sleek design. Dive into movies and shows effortlessly—your gateway to entertainment perfection!

6 | 7 |

🚀 Demo

8 | 9 | [https://sammi-netflix.vercel.app](https://sammi-netflix.vercel.app) 10 | 11 |

🧐 Features

12 | 13 | Here're some of the project's best features: 14 | 15 | * User Authentication 16 | * Content Management 17 | * Search and Filters 18 | * User Interaction: 19 | * Social Integration 20 | 21 |

🛠️ Installation Steps:

22 | 23 |

1. Install all dependencies

24 | 25 | ``` 26 | npm install | yarn install 27 | ``` 28 | 29 |

2. Run application

30 | 31 | ``` 32 | npm run dev | yarn dev 33 | ``` 34 | 35 |

3. Write your own environment values

36 | 37 | ``` 38 | .env 39 | ``` 40 | 41 | 42 | 43 |

💻 Built with

44 | 45 | Technologies used in the project: 46 | 47 | * NextJS 14v 48 | * NodeJS 49 | * ExpressJS 50 | * MongoDB 51 | * Mongoose 52 | * TailwindCSS 53 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 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 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.3.2", 13 | "@radix-ui/react-dialog": "^1.0.5", 14 | "@radix-ui/react-label": "^2.0.2", 15 | "@radix-ui/react-popover": "^1.0.7", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "@radix-ui/react-toast": "^1.1.5", 18 | "@tailwindcss/line-clamp": "^0.4.4", 19 | "axios": "^1.6.0", 20 | "bcryptjs": "^2.4.3", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.0.0", 23 | "framer-motion": "^10.16.4", 24 | "lucide-react": "^0.292.0", 25 | "mongoose": "^8.0.0", 26 | "next": "14.0.1", 27 | "next-auth": "^4.24.4", 28 | "next-themes": "^0.2.1", 29 | "react": "^18", 30 | "react-dom": "^18", 31 | "react-hook-form": "^7.48.2", 32 | "react-icons": "^4.11.0", 33 | "react-pin-input": "^1.3.1", 34 | "react-player": "^2.13.0", 35 | "react-stars": "^2.2.5", 36 | "tailwind-merge": "^2.0.0", 37 | "tailwind-scrollbar": "^3.0.5", 38 | "tailwind-scrollbar-hide": "^1.1.7", 39 | "tailwindcss-animate": "^1.0.7", 40 | "zod": "^3.22.4" 41 | }, 42 | "devDependencies": { 43 | "@types/bcryptjs": "^2.4.5", 44 | "@types/node": "^20", 45 | "@types/react": "^18", 46 | "@types/react-dom": "^18", 47 | "@types/react-stars": "^2.2.3", 48 | "autoprefixer": "^10.0.1", 49 | "eslint": "^8", 50 | "eslint-config-next": "14.0.1", 51 | "postcss": "^8", 52 | "tailwindcss": "^3.3.0", 53 | "typescript": "^5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/api/favourite/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from "next/server"; 2 | import {connectToDatabase} from "@/lib/mongoose"; 3 | import {FavouriteProps} from "@/types"; 4 | import Favourite from "@/database/favourite"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export async function POST(req: Request) { 9 | try { 10 | await connectToDatabase(); 11 | 12 | const body: FavouriteProps = await req.json(); 13 | 14 | const {uid, accountId, movieId} = body; 15 | 16 | const isExist = await Favourite.findOne({uid, movieId, accountId}); 17 | 18 | if(isExist) { 19 | return NextResponse.json({success: false, message: "Already added to favourites"}) 20 | } 21 | 22 | const favourite = await Favourite.create(body); 23 | 24 | return NextResponse.json({success: true, data: favourite}) 25 | }catch (e) { 26 | return NextResponse.json({success: false, message: "Something went wrong"}) 27 | } 28 | } 29 | 30 | export async function GET(req: Request) { 31 | try{ 32 | await connectToDatabase() 33 | 34 | const {searchParams} = new URL(req.url) 35 | const uid = searchParams.get("uid") 36 | const accountId = searchParams.get("accountId") 37 | 38 | const favourites = await Favourite.find({uid, accountId}) 39 | 40 | return NextResponse.json({success: true, data: favourites}) 41 | }catch (e) { 42 | return NextResponse.json({success: false, message: "Something went wrong"}) 43 | } 44 | } 45 | 46 | export async function DELETE(req: Request) { 47 | try{ 48 | await connectToDatabase() 49 | 50 | const {searchParams} = new URL(req.url) 51 | const id = searchParams.get("id") 52 | 53 | await Favourite.findByIdAndDelete(id) 54 | 55 | return NextResponse.json({success: true, data: "Successfully deleted"}) 56 | }catch (e) { 57 | return NextResponse.json({success: false, message: "Something went wrong"}) 58 | } 59 | } 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /components/shared/navbar/search-bar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, {Dispatch, SetStateAction, useState, KeyboardEvent} from 'react'; 4 | import {AiOutlineSearch} from "react-icons/ai"; 5 | import {usePathname, useRouter} from "next/navigation"; 6 | import {useGlobalContext} from "@/context"; 7 | 8 | interface Props{ 9 | setShowSearchBar: Dispatch> 10 | } 11 | const SearchBar = ({setShowSearchBar}: Props) => { 12 | const [query, setQuery] = useState("") 13 | 14 | const router = useRouter() 15 | const pathname = usePathname() 16 | const {setPageLoader} = useGlobalContext() 17 | 18 | const handleKeySubmit = (e: KeyboardEvent) => { 19 | if(e.key === "Enter" && query && query.trim() !== "") { 20 | setPageLoader(true) 21 | if(pathname !== "/search") { 22 | router.replace(`/search/${query}`) 23 | }else{ 24 | router.push(`/search/${query}`) 25 | } 26 | } 27 | } 28 | 29 | return ( 30 |
31 |
32 |
33 | setQuery(e.target.value)} 38 | onKeyUp={handleKeySubmit} 39 | /> 40 |
41 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default SearchBar; -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /app/(root)/tv/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, {useEffect, useState} from 'react'; 4 | import {MovieDataProps, MovieProps} from "@/types"; 5 | import {useGlobalContext} from "@/context"; 6 | import {useSession} from "next-auth/react"; 7 | import Login from "@/components/shared/login"; 8 | import ManageAccount from "@/components/shared/manage-account"; 9 | import Loader from "@/components/shared/loader"; 10 | import {getMoviesByGenre} from "@/lib/api"; 11 | import Common from "@/components/shared/common"; 12 | 13 | const Page = () => { 14 | const [moviesData, setMoviesData] = useState([]) 15 | 16 | const {account, pageLoader, setPageLoader} = useGlobalContext(); 17 | const {data: session} = useSession() 18 | 19 | useEffect(() => { 20 | const getAllMovies = async () => { 21 | try { 22 | const [action, animation, comedy, crime, documentary, drama, family, war] = await Promise.all([ 23 | getMoviesByGenre("tv", 10759), 24 | getMoviesByGenre("tv", 16), 25 | getMoviesByGenre("tv", 35), 26 | getMoviesByGenre("tv", 80), 27 | getMoviesByGenre("tv", 99), 28 | getMoviesByGenre("tv", 18), 29 | getMoviesByGenre("tv", 10751), 30 | getMoviesByGenre("tv", 10768), 31 | ]) 32 | 33 | const allResult: MovieDataProps[] = [ 34 | {title: "Action", data: action}, 35 | {title: "Animation", data: animation}, 36 | {title: "Comedy", data: comedy}, 37 | {title: "Crime", data: crime}, 38 | {title: "Documentary", data: documentary}, 39 | {title: "Drama", data: drama}, 40 | {title: "Family", data: family}, 41 | {title: "War", data: war}, 42 | ].map(item => ({...item, data: item.data.map((movie: MovieProps) => ({...movie, type: "tv", addedToFavorites: false}))})) 43 | 44 | setMoviesData(allResult) 45 | }catch (e) { 46 | console.log(e) 47 | }finally { 48 | setPageLoader(false) 49 | } 50 | } 51 | 52 | getAllMovies() 53 | }, []); 54 | 55 | if(session === null) return 56 | if(account === null) return 57 | if(pageLoader) return 58 | 59 | return 60 | }; 61 | 62 | export default Page; -------------------------------------------------------------------------------- /app/(root)/movies/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, {useEffect, useState} from 'react'; 4 | import {MovieDataProps, MovieProps} from "@/types"; 5 | import {useGlobalContext} from "@/context"; 6 | import {useSession} from "next-auth/react"; 7 | import Login from "@/components/shared/login"; 8 | import ManageAccount from "@/components/shared/manage-account"; 9 | import Loader from "@/components/shared/loader"; 10 | import {getMoviesByGenre} from "@/lib/api"; 11 | import Common from "@/components/shared/common"; 12 | 13 | const Page = () => { 14 | const [moviesData, setMoviesData] = useState([]) 15 | 16 | const {account, pageLoader, setPageLoader} = useGlobalContext(); 17 | const {data: session} = useSession() 18 | 19 | useEffect(() => { 20 | const getAllMovies = async () => { 21 | try { 22 | const [action, animation, comedy, crime, documentary, drama, family, war] = await Promise.all([ 23 | getMoviesByGenre("movie", 28), 24 | getMoviesByGenre("movie", 16), 25 | getMoviesByGenre("movie", 35), 26 | getMoviesByGenre("movie", 80), 27 | getMoviesByGenre("movie", 99), 28 | getMoviesByGenre("movie", 18), 29 | getMoviesByGenre("movie", 10752), 30 | getMoviesByGenre("movie", 10768), 31 | ]) 32 | 33 | const allResult: MovieDataProps[] = [ 34 | {title: "Action", data: action}, 35 | {title: "Animation", data: animation}, 36 | {title: "Comedy", data: comedy}, 37 | {title: "Crime", data: crime}, 38 | {title: "Documentary", data: documentary}, 39 | {title: "Drama", data: drama}, 40 | {title: "Family", data: family}, 41 | {title: "War", data: war}, 42 | ].map(item => ({...item, data: item.data.map((movie: MovieProps) => ({...movie, type: "movie", addedToFavorites: false}))})) 43 | 44 | setMoviesData(allResult) 45 | } catch (e) { 46 | console.log(e) 47 | } finally { 48 | setPageLoader(false) 49 | } 50 | } 51 | 52 | getAllMovies() 53 | }, []); 54 | 55 | if (session === null) return 56 | if (account === null) return 57 | if (pageLoader) return 58 | 59 | return 60 | }; 61 | 62 | export default Page; -------------------------------------------------------------------------------- /app/api/account/route.ts: -------------------------------------------------------------------------------- 1 | import {connectToDatabase} from "@/lib/mongoose"; 2 | import {NextResponse} from "next/server"; 3 | import Account from "@/database/account"; 4 | import {hash} from "bcryptjs" 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | // Create a new account 9 | export async function POST(req: Request) { 10 | try { 11 | await connectToDatabase(); 12 | const {name, pin, uid} = await req.json(); 13 | 14 | const isExist = await Account.findOne({name}); 15 | const allAccounts = await Account.find({uid}); 16 | 17 | if(isExist) { 18 | return NextResponse.json({success: false, message: "You already have an account"}) 19 | } 20 | 21 | if(allAccounts && allAccounts.length === 4) { 22 | return NextResponse.json({success: false, message: "You can only have 4 accounts"}) 23 | } 24 | 25 | const hashPin = await hash(pin, 10); 26 | 27 | const account = await Account.create({name, pin: hashPin, uid}); 28 | 29 | return NextResponse.json({success: true, data: account}) 30 | }catch (e) { 31 | return NextResponse.json({success: false, message: "Something went wrong"}) 32 | } 33 | } 34 | 35 | // Get all accounts 36 | export async function GET(req: Request) { 37 | try{ 38 | await connectToDatabase(); 39 | 40 | const {searchParams} = new URL(req.url); 41 | const uid = searchParams.get("uid"); 42 | 43 | if(!uid) { 44 | return NextResponse.json({success: false, message: "Account id is mandatory"}) 45 | } 46 | 47 | const accounts = await Account.find({uid}); 48 | 49 | return NextResponse.json({success: true, data: accounts}) 50 | }catch (e) { 51 | return NextResponse.json({success: false, message: "Something went wrong"}) 52 | } 53 | } 54 | 55 | // Delete an account 56 | export async function DELETE(req: Request) { 57 | try { 58 | await connectToDatabase(); 59 | 60 | const {searchParams} = new URL(req.url); 61 | const id = searchParams.get("id"); 62 | 63 | if(!id) { 64 | return NextResponse.json({success: false, message: "Account id is mandatory"}) 65 | } 66 | 67 | await Account.findByIdAndDelete(id); 68 | 69 | return NextResponse.json({success: true, message: "Account deleted successfully"}) 70 | }catch (e) { 71 | return NextResponse.json({success: false, message: "Something went wrong"}) 72 | } 73 | } 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /lib/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const API_KEY = process.env.NEXT_PUBLIC_TMDB_API_KEY 4 | const BASE_URL = process.env.NEXT_PUBLIC_TMDB_BASE_URL 5 | 6 | export const getTrendingMovies = async (type: string) => { 7 | try { 8 | const {data} = await axios.get(`${BASE_URL}/trending/${type}/day?api_key=${API_KEY}&language=en-US`) 9 | return data && data.results; 10 | }catch (e) { 11 | console.log(e) 12 | } 13 | } 14 | 15 | export const getTopratedMovies = async (type: string) => { 16 | try { 17 | const {data} = await axios.get(`${BASE_URL}/${type}/top_rated?api_key=${API_KEY}&language=en-US`) 18 | return data && data.results; 19 | }catch (e) { 20 | console.log(e) 21 | } 22 | } 23 | 24 | export const getPopularMovies = async (type: string) => { 25 | try { 26 | const {data} = await axios.get(`${BASE_URL}/${type}/popular?api_key=${API_KEY}&language=en-US`) 27 | return data && data.results; 28 | }catch (e) { 29 | console.log(e) 30 | } 31 | } 32 | 33 | export const getMoviesByGenre = async(type: string, id: number) => { 34 | try { 35 | const {data} = await axios.get(`${BASE_URL}/discover/${type}?api_key=${API_KEY}&language=en-US&include_adult=false&sort_by=popularity.desc&with_genres=${id}`) 36 | return data && data.results; 37 | }catch (e) { 38 | console.log(e) 39 | } 40 | } 41 | 42 | export const getMovieDetails = async (type?: string, id?: number) => { 43 | try{ 44 | const {data} = await axios.get(`${BASE_URL}/${type}/${id}?api_key=${API_KEY}&language=en-US&append_to_response=videos`) 45 | return {data, type}; 46 | }catch (e) { 47 | console.log(e) 48 | } 49 | } 50 | 51 | 52 | export const getSimilarMovies = async (type?: string, id?: number) => { 53 | try { 54 | const {data} = await axios.get(`${BASE_URL}/${type}/${id}/similar?api_key=${API_KEY}&language=en-US`) 55 | return data && data.results; 56 | }catch (e) { 57 | console.log(e) 58 | } 59 | } 60 | 61 | export const getSearchResults = async (type: string, query: string) => { 62 | try { 63 | const {data} = await axios.get(`${BASE_URL}/search/${type}?api_key=${API_KEY}&include_adult=false&language=en-US&query=${query}`) 64 | return data && data.results; 65 | }catch (e) { 66 | console.log(e) 67 | } 68 | } 69 | 70 | export const getFavourites = async(uid?: string, accountId?: string) => { 71 | try{ 72 | const {data} = await axios.get(`/api/favourite?uid=${uid}&accountId=${accountId}`) 73 | return data 74 | }catch (e) { 75 | console.log(e) 76 | } 77 | } -------------------------------------------------------------------------------- /app/(root)/browse/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {useGlobalContext} from "@/context"; 4 | import Login from "@/components/shared/login"; 5 | import {useSession} from "next-auth/react"; 6 | import ManageAccount from "@/components/shared/manage-account"; 7 | import Loader from "@/components/shared/loader"; 8 | import {useEffect, useState} from "react"; 9 | import Common from "@/components/shared/common"; 10 | import {getPopularMovies, getTopratedMovies, getTrendingMovies} from "@/lib/api"; 11 | import {MovieDataProps, MovieProps} from "@/types"; 12 | 13 | const Page = () => { 14 | const [moviesData, setMoviesData] = useState([]) 15 | 16 | const {account, pageLoader, setPageLoader} = useGlobalContext(); 17 | const {data: session} = useSession() 18 | 19 | useEffect(() => { 20 | const getAllMovies = async () => { 21 | try { 22 | const [trendingTv, topRatedTv, popularTv, trendingMovie, topRatedMovie, popularMovie] = await Promise.all([ 23 | getTrendingMovies("tv"), 24 | getTopratedMovies("tv"), 25 | getPopularMovies("tv"), 26 | 27 | getTrendingMovies("movie"), 28 | getTopratedMovies("movie"), 29 | getPopularMovies("movie"), 30 | ]) 31 | 32 | const tvShows: MovieDataProps[] = [ 33 | {title: "Trending TV Shows", data: trendingTv}, 34 | {title: "Top Rated TV Shows", data: topRatedTv}, 35 | {title: "Popular TV Shows", data: popularTv}, 36 | ].map(item => ({ 37 | ...item, 38 | data: item.data.map((movie: MovieProps) => ({...movie, type: "tv", addedToFavorites: false})) 39 | })) 40 | 41 | const moviesShows: MovieDataProps[] = [ 42 | {title: "Trending Movies", data: trendingMovie}, 43 | {title: "Top Rated Movies", data: topRatedMovie}, 44 | {title: "Popular Movies", data: popularMovie}, 45 | ].map(item => ({ 46 | ...item, 47 | data: item.data.map((movie: MovieProps) => ({...movie, type: "movie", addedToFavorites: false})) 48 | })) 49 | 50 | const allMovies = [...moviesShows, ...tvShows] 51 | setMoviesData(allMovies) 52 | } catch (e) { 53 | console.log(e) 54 | } finally { 55 | setPageLoader(false) 56 | } 57 | } 58 | 59 | getAllMovies() 60 | }, []); 61 | 62 | if (session === null) return 63 | if (account === null) return 64 | if (pageLoader) return 65 | 66 | return 67 | }; 68 | 69 | export default Page; -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [ 76 | require("tailwindcss-animate"), 77 | require("tailwind-scrollbar-hide"), 78 | require("tailwind-scrollbar"), 79 | require("@tailwindcss/line-clamp"), 80 | ], 81 | } -------------------------------------------------------------------------------- /components/shared/banner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import {MovieProps} from "@/types"; 4 | import {useEffect, useState} from "react"; 5 | import Image from "next/image"; 6 | import {AiFillPlayCircle} from "react-icons/ai"; 7 | import {IoMdInformationCircleOutline} from "react-icons/io"; 8 | import {useGlobalContext} from "@/context"; 9 | 10 | interface Props { 11 | movies: MovieProps[]; 12 | } 13 | 14 | const Banner = ({movies}: Props) => { 15 | const [randomMovie, setRandomMovie] = useState(null) 16 | 17 | const {setOpen, setMovie, account} = useGlobalContext() 18 | 19 | useEffect(() => { 20 | const movie = movies[Math.floor(Math.random() * movies.length)] 21 | setRandomMovie(movie) 22 | }, []) 23 | 24 | const onHandlerPopup = () => { 25 | setMovie(randomMovie) 26 | setOpen(true) 27 | } 28 | 29 | return ( 30 |
31 |
32 | {"Banner"} 38 |
39 |
40 |
41 | 42 |

43 | {randomMovie?.title || randomMovie?.name || randomMovie?.original_name} 44 |

45 |

46 | {randomMovie?.overview} 47 |

48 |
49 | 56 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default Banner; -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import {Dispatch, ReactNode, SetStateAction} from "react"; 2 | 3 | export interface ContextType { 4 | account: AccountProps | null 5 | setAccount: Dispatch> 6 | pageLoader: boolean 7 | setPageLoader: Dispatch> 8 | open: boolean 9 | setOpen: Dispatch> 10 | movie: MovieProps | null 11 | setMovie: Dispatch> 12 | } 13 | 14 | export interface AccountProps { 15 | _id: string 16 | uid: string 17 | name: string 18 | pin: string 19 | } 20 | 21 | export interface ChildProps { 22 | children: ReactNode 23 | } 24 | 25 | export interface AxiosResponse { 26 | success: boolean 27 | message?: string 28 | } 29 | 30 | export interface AccountResponse extends AxiosResponse { 31 | data: AccountProps[] | AccountProps 32 | } 33 | 34 | export interface MenuItemProps { 35 | id: string 36 | title: string 37 | path: string 38 | } 39 | 40 | export interface MovieDataProps { 41 | title: string, 42 | data: MovieProps[] 43 | } 44 | 45 | export interface MovieProps { 46 | adult: boolean; 47 | backdrop_path: string; 48 | first_air_date: string; 49 | genre_ids: number[]; 50 | id: number; 51 | media_type: string; 52 | name: string; 53 | origin_country: string[]; 54 | original_language: string; 55 | original_name: string; 56 | overview: string; 57 | popularity: number; 58 | poster_path: string; 59 | type: string; 60 | vote_average: number; 61 | vote_count: number; 62 | title: string; 63 | addedToFavorites: boolean; 64 | movieID: number; 65 | } 66 | 67 | export interface MovieDetailsProps { 68 | adult: boolean; 69 | backdrop_path: string; 70 | belongs_to_collection: any; 71 | budget: number; 72 | genres: Array; 73 | homepage: string; 74 | id: number; 75 | imdb_id: string; 76 | original_language: string; 77 | original_title: string; 78 | overview: string; 79 | popularity: number; 80 | poster_path: string; 81 | production_companies: Array; 82 | production_countries: Array; 83 | release_date: string; 84 | revenue: number; 85 | runtime: number; 86 | spoken_languages: Array; 87 | status: string; 88 | tagline: string; 89 | title: string; 90 | video: boolean; 91 | videos: { 92 | results: VideoProps[] 93 | }; 94 | vote_average: number; 95 | vote_count: number; 96 | } 97 | 98 | export interface VideoProps { 99 | id: string; 100 | iso_639_1: string; 101 | iso_3166_1: string; 102 | key: string; 103 | name: string; 104 | official: boolean; 105 | published_at: string; 106 | site: string; 107 | size: number; 108 | type: string; 109 | } 110 | 111 | export interface FavouriteProps { 112 | uid: string 113 | accountId: string 114 | backdrop_path: string 115 | poster_path: string 116 | movieId: string 117 | type: string 118 | title: string 119 | overview: string 120 | _id?: string 121 | } -------------------------------------------------------------------------------- /app/(root)/search/[query]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {useParams} from "next/navigation"; 4 | import Login from "@/components/shared/login"; 5 | import ManageAccount from "@/components/shared/manage-account"; 6 | import Loader from "@/components/shared/loader"; 7 | import {useGlobalContext} from "@/context"; 8 | import {useSession} from "next-auth/react"; 9 | import {motion} from "framer-motion"; 10 | import Navbar from "@/components/shared/navbar"; 11 | import {useEffect, useState} from "react"; 12 | import {toast} from "@/components/ui/use-toast"; 13 | import {getSearchResults} from "@/lib/api"; 14 | import {MovieProps} from "@/types"; 15 | import MovieItem from "@/components/shared/movie/movie-item"; 16 | 17 | const Page = () => { 18 | const [movies, setMovies] = useState([]) 19 | 20 | const {data: session}: any = useSession() 21 | const {account, pageLoader, setPageLoader} = useGlobalContext() 22 | const params = useParams(); 23 | 24 | useEffect(() => { 25 | const getData = async () => { 26 | try { 27 | const [tv, movies] = await Promise.all([ 28 | getSearchResults('tv', params.query as string), 29 | getSearchResults('movie', params.query as string) 30 | ]) 31 | const tvShows = tv 32 | .filter((item: MovieProps) => item.backdrop_path !== null && item.poster_path !== null) 33 | .map((movie: MovieProps) => ({...movie, type: 'tv'})) 34 | 35 | const moviesShow = movies 36 | .filter((item: MovieProps) => item.backdrop_path !== null && item.poster_path !== null) 37 | .map((movie: MovieProps) => ({...movie, type: 'movie'})) 38 | 39 | setMovies([...tvShows, ...moviesShow]) 40 | } catch (e) { 41 | return toast({ 42 | title: 'Error', 43 | description: 'Something went wrong', 44 | variant: "destructive" 45 | }) 46 | } finally { 47 | setPageLoader(false) 48 | 49 | } 50 | } 51 | 52 | getData() 53 | }, []); 54 | 55 | if (session === null) return 56 | if (account === null) return 57 | if (pageLoader) return 58 | 59 | return ( 60 | 65 | 66 |
67 |

69 | Showing Results for {decodeURI(params.query as string)} 70 |

71 | 72 |
73 | {movies && movies.length 74 | ? movies.map((movie) => ( 75 | 79 | )) 80 | : null} 81 |
82 |
83 |
84 | ); 85 | }; 86 | 87 | export default Page; 88 | -------------------------------------------------------------------------------- /components/form/login-account-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, {useState} from 'react'; 4 | import PinInput from "react-pin-input"; 5 | import {Loader2} from "lucide-react"; 6 | import {AccountProps, AccountResponse} from "@/types"; 7 | import {toast} from "@/components/ui/use-toast"; 8 | import axios from "axios"; 9 | import {useGlobalContext} from "@/context"; 10 | import {usePathname, useRouter} from "next/navigation"; 11 | 12 | interface Props{ 13 | currentAccount: AccountProps | null 14 | } 15 | const LoginAccountForm = ({currentAccount}: Props) => { 16 | const [error, setError] = useState(false) 17 | const [pin, setPin] = useState("") 18 | const [isLoading, setIsLoading] = useState(false) 19 | 20 | const {setAccount} = useGlobalContext() 21 | const pathname = usePathname() 22 | const router = useRouter() 23 | 24 | const onSubmit = async (value: string) => { 25 | setIsLoading(true) 26 | try { 27 | const {data} = await axios.post(`/api/account/login`, { 28 | uid: currentAccount?.uid, 29 | accountId: currentAccount?._id, 30 | pin: value 31 | }) 32 | 33 | if(data.success) { 34 | setAccount(data.data as AccountProps) 35 | sessionStorage.setItem("account", JSON.stringify(data.data)) 36 | router.push(pathname) 37 | return toast({ 38 | title: "Account unlocked", 39 | description: "Your account has been unlocked successfully", 40 | }) 41 | }else { 42 | setError(true) 43 | } 44 | }catch (e) { 45 | return toast({ 46 | title: "Error", 47 | description: "An error occurred while logging in", 48 | variant: "destructive" 49 | }) 50 | }finally { 51 | setIsLoading(false) 52 | } 53 | } 54 | 55 | return ( 56 | <> 57 |

58 | Profile Lock is currently ON 59 |

60 | {error ? ( 61 |

62 | Whoops, wrong PIN. Please try again 63 |

64 | ) : ( 65 |

66 | Enter your PIN to access this profile 67 |

68 | )} 69 | 70 |
71 | setPin(value)} 77 | type="numeric" 78 | inputMode="number" 79 | style={{ padding: "20px", display: "flex", gap: "10px" }} 80 | inputStyle={{ 81 | borderColor: "white", 82 | height: "70px", 83 | width: "70px", 84 | fontSize: "40px", 85 | }} 86 | disabled={isLoading} 87 | inputFocusStyle={{ borderColor: "white" }} 88 | onComplete={(value) => onSubmit(value)} 89 | autoSelect={true} 90 | /> 91 | {isLoading && } 92 |
93 | 94 | ); 95 | }; 96 | 97 | export default LoginAccountForm; -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground !scrollbar-thin !scrollbar-track-transparent !scrollbar-thumb-red-600; 75 | } 76 | } 77 | 78 | @layer components { 79 | .header { 80 | @apply fixed top-0 z-50 flex w-full items-center justify-between px-4 transition-all lg:px-14 text-white; 81 | } 82 | } 83 | 84 | .circleLoader { 85 | position: absolute; 86 | top: 50%; 87 | left: 50%; 88 | margin: 37px 0 0 -25px; 89 | width: 50px; 90 | height: 50px; 91 | } 92 | .circleLoader:after { 93 | content: ""; 94 | background-image: url(https://assets.nflxext.com/en_us/pages/wiplayer/site-spinner.png); 95 | background-repeat: no-repeat; 96 | background-position-x: 50%; 97 | background-position-y: 50%; 98 | -moz-background-size: 100%; 99 | -o-background-size: 100%; 100 | background-size: 100%; 101 | position: absolute; 102 | margin: -6px; 103 | width: inherit; 104 | height: inherit; 105 | animation: circleLoader-spin 1.1s linear infinite,1!important; 106 | -webkit-animation: circleLoader-spin 1.1s linear infinite,1!important; 107 | } 108 | @keyframes circleLoader-spin { 109 | 100% { 110 | transform: rotate(360deg); 111 | } 112 | } 113 | @-webkit-keyframes circleLoader-spin { 114 | 100% { 115 | -webkit-transform: rotate(360deg); 116 | } 117 | } 118 | 119 | .cardWrapper:hover { 120 | background: black; 121 | } 122 | 123 | .cardWrapper:hover .buttonWrapper { 124 | display: flex; 125 | z-index: 2; 126 | width: 100%; 127 | justify-content: center; 128 | } 129 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /app/(root)/mylist/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {useSession} from "next-auth/react"; 4 | import {useGlobalContext} from "@/context"; 5 | import {useEffect, useState} from "react"; 6 | import {FavouriteProps, MovieProps} from "@/types"; 7 | import {toast} from "@/components/ui/use-toast"; 8 | import {getFavourites} from "@/lib/api"; 9 | import Login from "@/components/shared/login"; 10 | import ManageAccount from "@/components/shared/manage-account"; 11 | import Loader from "@/components/shared/loader"; 12 | import MovieItem from "@/components/shared/movie/movie-item"; 13 | import Navbar from "@/components/shared/navbar"; 14 | import Banner from "@/components/shared/banner"; 15 | import {useRouter} from "next/navigation"; 16 | 17 | const Page = () => { 18 | const [favourites, setFavourites] = useState([]) 19 | 20 | const {data: session}: any = useSession() 21 | const {account, setPageLoader, pageLoader} = useGlobalContext() 22 | const router = useRouter() 23 | 24 | useEffect(() => { 25 | const getData = async () => { 26 | try { 27 | const {data} = await getFavourites(session?.user?.uid, account?._id) 28 | setFavourites(data) 29 | } catch (e) { 30 | return toast({ 31 | title: "Error", 32 | description: "Something went wrong, please try again later", 33 | variant: "destructive" 34 | }) 35 | } finally { 36 | setPageLoader(false) 37 | } 38 | } 39 | if (session && account) { 40 | getData() 41 | } 42 | }, [account, session]); 43 | 44 | if (session === null) return 45 | if (account === null) return 46 | if (pageLoader) return 47 | 48 | return ( 49 |
50 | 51 |
52 | {favourites && favourites.length === 0 ? ( 53 |
54 |
55 |
56 |
57 |

58 | Looks like you don't have any favourites yet! 59 |

60 |

Sorry about that! Please visit our hompage to get where you need to go.

61 | 70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 | ) : ( 78 | <> 79 | {/*@ts-ignore*/} 80 | 81 |
82 |

84 | My list 85 |

86 | 87 |
88 |
89 | {favourites && favourites.map(((fav: FavouriteProps) => ( 90 | 103 | ))).reverse()} 104 |
105 |
106 |
107 | 108 | 109 | )} 110 |
111 |
112 | ); 113 | }; 114 | 115 | export default Page; 116 | -------------------------------------------------------------------------------- /components/shared/movie/movie-item.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import {FavouriteProps, MovieProps} from "@/types"; 4 | import {motion} from "framer-motion"; 5 | import Image from "next/image"; 6 | import {CheckIcon, ChevronDown, Loader2, MinusIcon, PlusIcon} from "lucide-react"; 7 | import {useGlobalContext} from "@/context"; 8 | import {usePathname, useRouter} from "next/navigation"; 9 | import CustomImage from "@/components/shared/custom-image"; 10 | import {toast} from "@/components/ui/use-toast"; 11 | import axios from "axios"; 12 | import {useSession} from "next-auth/react"; 13 | import {Dispatch, SetStateAction, useState} from "react"; 14 | 15 | interface Props { 16 | movie: MovieProps 17 | favouriteId?: string 18 | setFavourites?: Dispatch> 19 | } 20 | 21 | const MovieItem = ({movie, favouriteId = "", setFavourites}: Props) => { 22 | const {setOpen, setMovie, account} = useGlobalContext() 23 | const {data: session}: any = useSession() 24 | const [isLoading, setIsLoading] = useState(false) 25 | 26 | const onHandlerPopup = () => { 27 | setMovie(movie) 28 | setOpen(true) 29 | } 30 | 31 | const onAdd = async () => { 32 | try { 33 | setIsLoading(true) 34 | const {data} = await axios.post("/api/favourite", { 35 | uid: session?.user?.uid, 36 | accountId: account?._id, 37 | backdrop_path: movie?.backdrop_path, 38 | poster_path: movie?.poster_path, 39 | movieId: movie?.id, 40 | type: movie?.type, 41 | title: movie?.title || movie?.name, 42 | overview: movie?.overview, 43 | }) 44 | 45 | if (data?.success) { 46 | return toast({ 47 | title: "Success", 48 | description: "Movie added to your favourite list", 49 | }) 50 | } else { 51 | return toast({ 52 | title: "Error", 53 | description: data.message, 54 | variant: "destructive" 55 | }) 56 | } 57 | 58 | } catch (e) { 59 | return toast({ 60 | title: "Error", 61 | description: "Something went wrong", 62 | variant: "destructive" 63 | }) 64 | } finally { 65 | setIsLoading(false) 66 | } 67 | } 68 | 69 | const onRemove = async () => { 70 | try { 71 | setIsLoading(true) 72 | const {data} = await axios.delete(`/api/favourite?id=${favouriteId}`) 73 | if (data?.success) { 74 | if (setFavourites) { 75 | setFavourites((prev: FavouriteProps[]) => prev.filter((item: FavouriteProps) => item._id !== favouriteId)) 76 | } 77 | return toast({ 78 | title: "Success", 79 | description: "Movie removed from your favourite list", 80 | }) 81 | } else { 82 | return toast({ 83 | title: "Error", 84 | description: data.message, 85 | variant: "destructive" 86 | }) 87 | } 88 | } catch (e) { 89 | return toast({ 90 | title: "Error", 91 | description: "Something went wrong", 92 | variant: "destructive" 93 | }) 94 | } finally { 95 | setIsLoading(false) 96 | } 97 | } 98 | 99 | return ( 100 |
102 | 108 | 109 |
110 | 126 | 131 |
132 |
133 | ); 134 | }; 135 | 136 | export default MovieItem; -------------------------------------------------------------------------------- /components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | // Inspired by react-hot-toast library 2 | import * as React from "react" 3 | 4 | import type { 5 | ToastActionElement, 6 | ToastProps, 7 | } from "@/components/ui/toast" 8 | 9 | const TOAST_LIMIT = 1 10 | const TOAST_REMOVE_DELAY = 1000000 11 | 12 | type ToasterToast = ToastProps & { 13 | id: string 14 | title?: React.ReactNode 15 | description?: React.ReactNode 16 | action?: ToastActionElement 17 | } 18 | 19 | const actionTypes = { 20 | ADD_TOAST: "ADD_TOAST", 21 | UPDATE_TOAST: "UPDATE_TOAST", 22 | DISMISS_TOAST: "DISMISS_TOAST", 23 | REMOVE_TOAST: "REMOVE_TOAST", 24 | } as const 25 | 26 | let count = 0 27 | 28 | function genId() { 29 | count = (count + 1) % Number.MAX_VALUE 30 | return count.toString() 31 | } 32 | 33 | type ActionType = typeof actionTypes 34 | 35 | type Action = 36 | | { 37 | type: ActionType["ADD_TOAST"] 38 | toast: ToasterToast 39 | } 40 | | { 41 | type: ActionType["UPDATE_TOAST"] 42 | toast: Partial 43 | } 44 | | { 45 | type: ActionType["DISMISS_TOAST"] 46 | toastId?: ToasterToast["id"] 47 | } 48 | | { 49 | type: ActionType["REMOVE_TOAST"] 50 | toastId?: ToasterToast["id"] 51 | } 52 | 53 | interface State { 54 | toasts: ToasterToast[] 55 | } 56 | 57 | const toastTimeouts = new Map>() 58 | 59 | const addToRemoveQueue = (toastId: string) => { 60 | if (toastTimeouts.has(toastId)) { 61 | return 62 | } 63 | 64 | const timeout = setTimeout(() => { 65 | toastTimeouts.delete(toastId) 66 | dispatch({ 67 | type: "REMOVE_TOAST", 68 | toastId: toastId, 69 | }) 70 | }, TOAST_REMOVE_DELAY) 71 | 72 | toastTimeouts.set(toastId, timeout) 73 | } 74 | 75 | export const reducer = (state: State, action: Action): State => { 76 | switch (action.type) { 77 | case "ADD_TOAST": 78 | return { 79 | ...state, 80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 81 | } 82 | 83 | case "UPDATE_TOAST": 84 | return { 85 | ...state, 86 | toasts: state.toasts.map((t) => 87 | t.id === action.toast.id ? { ...t, ...action.toast } : t 88 | ), 89 | } 90 | 91 | case "DISMISS_TOAST": { 92 | const { toastId } = action 93 | 94 | // ! Side effects ! - This could be extracted into a dismissToast() action, 95 | // but I'll keep it here for simplicity 96 | if (toastId) { 97 | addToRemoveQueue(toastId) 98 | } else { 99 | state.toasts.forEach((toast) => { 100 | addToRemoveQueue(toast.id) 101 | }) 102 | } 103 | 104 | return { 105 | ...state, 106 | toasts: state.toasts.map((t) => 107 | t.id === toastId || toastId === undefined 108 | ? { 109 | ...t, 110 | open: false, 111 | } 112 | : t 113 | ), 114 | } 115 | } 116 | case "REMOVE_TOAST": 117 | if (action.toastId === undefined) { 118 | return { 119 | ...state, 120 | toasts: [], 121 | } 122 | } 123 | return { 124 | ...state, 125 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 126 | } 127 | } 128 | } 129 | 130 | const listeners: Array<(state: State) => void> = [] 131 | 132 | let memoryState: State = { toasts: [] } 133 | 134 | function dispatch(action: Action) { 135 | memoryState = reducer(memoryState, action) 136 | listeners.forEach((listener) => { 137 | listener(memoryState) 138 | }) 139 | } 140 | 141 | type Toast = Omit 142 | 143 | function toast({ ...props }: Toast) { 144 | const id = genId() 145 | 146 | const update = (props: ToasterToast) => 147 | dispatch({ 148 | type: "UPDATE_TOAST", 149 | toast: { ...props, id }, 150 | }) 151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 152 | 153 | dispatch({ 154 | type: "ADD_TOAST", 155 | toast: { 156 | ...props, 157 | id, 158 | open: true, 159 | onOpenChange: (open) => { 160 | if (!open) dismiss() 161 | }, 162 | }, 163 | }) 164 | 165 | return { 166 | id: id, 167 | dismiss, 168 | update, 169 | } 170 | } 171 | 172 | function useToast() { 173 | const [state, setState] = React.useState(memoryState) 174 | 175 | React.useEffect(() => { 176 | listeners.push(setState) 177 | return () => { 178 | const index = listeners.indexOf(setState) 179 | if (index > -1) { 180 | listeners.splice(index, 1) 181 | } 182 | } 183 | }, [state]) 184 | 185 | return { 186 | ...state, 187 | toast, 188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 189 | } 190 | } 191 | 192 | export { useToast, toast } 193 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |