├── .eslintrc.json ├── .gitignore ├── README.md ├── actions ├── google-login.ts └── register.ts ├── app ├── api │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── auth │ └── register │ │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── auth │ ├── auth-header.tsx │ ├── back-button.tsx │ ├── card-wrapper.tsx │ ├── form-error.tsx │ ├── form-success.tsx │ ├── forms │ │ └── register-form.tsx │ └── google-login.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── form.tsx │ ├── input.tsx │ └── label.tsx ├── lib └── utils.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── prisma.ts └── schema.prisma ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── schemas └── index.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /actions/google-login.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signIn } from "@/auth"; 4 | import { AuthError } from "next-auth"; 5 | 6 | export async function googleAuthenticate() { 7 | try { 8 | await signIn('google'); 9 | } catch (error) { 10 | if (error instanceof AuthError) { 11 | return 'google log in failed' 12 | } 13 | throw error; 14 | } 15 | } -------------------------------------------------------------------------------- /actions/register.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import * as z from "zod"; 4 | import { prisma } from "@/prisma/prisma"; 5 | import bcrypt from "bcryptjs"; 6 | import { RegisterSchema } from "@/schemas"; 7 | // import { generateVerificationToken } from "@/lib/token"; 8 | // import { sendVerificationEmail } from "@/lib/mail"; 9 | 10 | export const register = async (data: z.infer) => { 11 | try { 12 | // Validate the input data 13 | const validatedData = RegisterSchema.parse(data); 14 | 15 | // If the data is invalid, return an error 16 | if (!validatedData) { 17 | return { error: "Invalid input data" }; 18 | } 19 | 20 | // Destructure the validated data 21 | const { email, name, password, passwordConfirmation } = validatedData; 22 | 23 | // Check if passwords match 24 | if (password !== passwordConfirmation) { 25 | return { error: "Passwords do not match" }; 26 | } 27 | 28 | // Hash the password 29 | const hashedPassword = await bcrypt.hash(password, 10); 30 | 31 | // Check to see if user already exists 32 | const userExists = await prisma.user.findFirst({ 33 | where: { 34 | email, 35 | }, 36 | }); 37 | 38 | // If the user exists, return an error 39 | if (userExists) { 40 | return { error: "Email already is in use. Please try another one." }; 41 | } 42 | 43 | const lowerCaseEmail = email.toLowerCase(); 44 | 45 | // Create the user 46 | const user = await prisma.user.create({ 47 | data: { 48 | email: lowerCaseEmail, 49 | name, 50 | password: hashedPassword, 51 | }, 52 | }); 53 | 54 | // Generate Verification Token 55 | // const verificationToken = await generateVerificationToken(email); 56 | 57 | // await sendVerificationEmail(lowerCaseEmail, verificationToken.token); 58 | 59 | return { success: "Email Verification was sent" }; 60 | } catch (error) { 61 | // Handle the error, specifically check for a 503 error 62 | console.error("Database error:", error); 63 | 64 | if ((error as { code: string }).code === "ETIMEDOUT") { 65 | return { 66 | error: "Unable to connect to the database. Please try again later.", 67 | }; 68 | } else if ((error as { code: string }).code === "503") { 69 | return { 70 | error: "Service temporarily unavailable. Please try again later.", 71 | }; 72 | } else { 73 | return { error: "An unexpected error occurred. Please try again later." }; 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/auth' -------------------------------------------------------------------------------- /app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | import RegisterForm from '@/components/auth/forms/register-form' 2 | import React from 'react' 3 | 4 | const RegisterPage = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default RegisterPage -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwestwood11/authjs-v5-tutorial/d7f1cc2a0d19e6478d4c83e92fe69b300d1fdb26/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwestwood11/authjs-v5-tutorial/d7f1cc2a0d19e6478d4c83e92fe69b300d1fdb26/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwestwood11/authjs-v5-tutorial/d7f1cc2a0d19e6478d4c83e92fe69b300d1fdb26/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 240 10% 3.9%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --muted: 240 4.8% 95.9%; 22 | --muted-foreground: 240 3.8% 46.1%; 23 | --accent: 240 4.8% 95.9%; 24 | --accent-foreground: 240 5.9% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 5.9% 90%; 28 | --input: 240 5.9% 90%; 29 | --ring: 240 10% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 240 10% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 240 10% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 240 10% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 240 5.9% 10%; 46 | --secondary: 240 3.7% 15.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 240 3.7% 15.9%; 49 | --muted-foreground: 240 5% 64.9%; 50 | --accent: 240 3.7% 15.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 240 3.7% 15.9%; 55 | --input: 240 3.7% 15.9%; 56 | --ring: 240 4.9% 83.9%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Create Next App", 18 | description: "Generated by create next app", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import GoogleLogin from "@/components/auth/google-login"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import Google from "next-auth/providers/google"; 2 | import Credentials from "next-auth/providers/credentials"; 3 | import type { NextAuthConfig } from "next-auth" 4 | 5 | export default { providers: [ 6 | Google({ 7 | clientId: process.env.GOOGLE_CLIENT_ID, 8 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 9 | }), 10 | ] } satisfies NextAuthConfig -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import { PrismaAdapter } from "@auth/prisma-adapter"; 3 | import authConfig from "./auth.config"; 4 | import { prisma } from "./prisma/prisma"; 5 | 6 | 7 | export const { auth, handlers: { GET, POST }, signIn, signOut } = NextAuth({ 8 | adapter: PrismaAdapter(prisma), 9 | session: { strategy: "jwt" }, 10 | ...authConfig, 11 | }); 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/auth/auth-header.tsx: -------------------------------------------------------------------------------- 1 | interface HeaderProps { 2 | label: string; 3 | title: string; 4 | } 5 | 6 | const AuthHeader = ({ 7 | title, 8 | label 9 | }: HeaderProps) => { 10 | return ( 11 |
12 |

{title}

13 |

14 | {label} 15 |

16 |
17 | ) 18 | } 19 | 20 | export default AuthHeader; -------------------------------------------------------------------------------- /components/auth/back-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import Link from "next/link"; 3 | 4 | interface BackButtonProps { 5 | label: string; 6 | href: string; 7 | } 8 | 9 | export const BackButton = ({ label, href }: BackButtonProps) => { 10 | return ( 11 | 16 | ) 17 | 18 | } -------------------------------------------------------------------------------- /components/auth/card-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardContent, 6 | CardHeader, 7 | CardFooter, 8 | } from "@/components/ui/card"; 9 | import AuthHeader from "./auth-header"; 10 | import { BackButton } from "./back-button"; 11 | 12 | interface CardWrapperProps { 13 | children: React.ReactNode; 14 | headerLabel: string; 15 | backButtonLabel: string; 16 | title: string; 17 | showSocial?: boolean; 18 | backButtonHref: string; 19 | } 20 | 21 | const CardWrapper = ({ children, headerLabel, backButtonLabel, backButtonHref, title, showSocial}: CardWrapperProps) => { 22 | return ( 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default CardWrapper; -------------------------------------------------------------------------------- /components/auth/form-error.tsx: -------------------------------------------------------------------------------- 1 | import { BsExclamationCircleFill } from "react-icons/bs"; 2 | 3 | interface FormSuccessProps { 4 | message?: string; 5 | } 6 | 7 | export const FormError = ({ message }: FormSuccessProps) => { 8 | if (!message) return null; 9 | return ( 10 |
11 | 12 |

{message}

13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /components/auth/form-success.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCheckIcon } from "lucide-react"; 2 | 3 | interface FormSuccessProps { 4 | message?: string; 5 | } 6 | 7 | export const FormSuccess = ({message}: FormSuccessProps) => { 8 | if (!message) return null; 9 | return ( 10 |
11 | 12 |

{message}

13 |
14 | ) 15 | 16 | } -------------------------------------------------------------------------------- /components/auth/forms/register-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useForm } from "react-hook-form"; 4 | import { 5 | Form, 6 | FormControl, 7 | FormDescription, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import CardWrapper from "../card-wrapper"; 14 | import { zodResolver } from "@hookform/resolvers/zod"; 15 | import { RegisterSchema } from "@/schemas"; 16 | import { Input } from "@/components/ui/input"; 17 | import { z } from "zod"; 18 | import { Button } from "@/components/ui/button"; 19 | import { useState } from "react"; 20 | import { register } from "@/actions/register"; 21 | import { FormSuccess } from "../form-success"; 22 | import { FormError } from "../form-error"; 23 | import GoogleLogin from "../google-login"; 24 | 25 | const RegisterForm = () => { 26 | const [loading, setLoading] = useState(false); 27 | const [error, setError] = useState(""); 28 | const [success, setSuccess] = useState(""); 29 | 30 | const form = useForm>({ 31 | resolver: zodResolver(RegisterSchema), 32 | defaultValues: { 33 | email: "", 34 | name: "", 35 | password: "", 36 | passwordConfirmation: "", 37 | }, 38 | }); 39 | 40 | const onSubmit = async (data: z.infer) => { 41 | setLoading(true); 42 | register(data).then((res) => { 43 | if (res.error) { 44 | setError(res.error); 45 | setLoading(false); 46 | } 47 | if (res.success) { 48 | setError(""); 49 | setSuccess(res.success); 50 | setLoading(false); 51 | } 52 | }); 53 | }; 54 | 55 | return ( 56 | 63 |
64 | 65 |
66 | ( 70 | 71 | Email 72 | 73 | 78 | 79 | 80 | 81 | )} 82 | /> 83 | ( 87 | 88 | Name 89 | 90 | 91 | 92 | 93 | 94 | )} 95 | /> 96 | ( 100 | 101 | Password 102 | 103 | 104 | 105 | 106 | 107 | )} 108 | /> 109 | ( 113 | 114 | Confirm Password 115 | 116 | 117 | 118 | 119 | 120 | )} 121 | /> 122 |
123 | 124 | 125 | 128 | 129 | 130 | 131 |
132 | ); 133 | }; 134 | 135 | export default RegisterForm; 136 | -------------------------------------------------------------------------------- /components/auth/google-login.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { googleAuthenticate } from '@/actions/google-login'; 4 | import React from 'react' 5 | import { useActionState } from 'react'; 6 | import { BsGoogle } from 'react-icons/bs'; 7 | import { Button } from '../ui/button'; 8 | 9 | const GoogleLogin = () => { 10 | const [errorMsgGoogle, dispatchGoogle] = useActionState(googleAuthenticate, undefined) //googleAuthenticate hook 11 | return ( 12 |
13 | 16 |

{errorMsgGoogle}

17 |
18 | ) 19 | } 20 | 21 | export default GoogleLogin; 22 | -------------------------------------------------------------------------------- /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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |