├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── GoogleSignInButton.tsx │ ├── Navbar.tsx │ ├── form │ │ ├── SignInForm.tsx │ │ └── SignUpForm.tsx │ └── ui │ │ ├── button.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ └── label.tsx ├── lib │ └── utils.ts └── styles │ └── globals.css ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 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.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/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 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /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": "@/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-auth", 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.1.1", 13 | "@radix-ui/react-label": "^2.0.2", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "@types/node": "20.4.5", 16 | "@types/react": "18.2.17", 17 | "@types/react-dom": "18.2.7", 18 | "autoprefixer": "10.4.14", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.0.0", 21 | "eslint": "8.45.0", 22 | "eslint-config-next": "13.4.12", 23 | "lucide-react": "^0.263.1", 24 | "next": "13.4.12", 25 | "postcss": "8.4.27", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "react-hook-form": "^7.45.2", 29 | "tailwind-merge": "^1.14.0", 30 | "tailwindcss": "3.3.3", 31 | "tailwindcss-animate": "^1.0.6", 32 | "typescript": "5.1.6", 33 | "zod": "^3.21.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | interface AuthLayoutProps { 4 | children: ReactNode; 5 | } 6 | 7 | const AuthLayout: FC = ({ children }) => { 8 | return
{children}
; 9 | }; 10 | 11 | export default AuthLayout; 12 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import SignInForm from '@/components/form/SignInForm'; 2 | 3 | const page = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default page; 12 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import SignUpForm from '@/components/form/SignUpForm'; 2 | 3 | const page = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default page; 12 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candraKriswinarto/nextjs-auth/296a5b68b18857c47d27f3f71cae2a96d6ec5e28/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/Navbar'; 2 | import '@/styles/globals.css'; 3 | import type { Metadata } from 'next'; 4 | import { Inter } from 'next/font/google'; 5 | 6 | const inter = Inter({ subsets: ['latin'] }); 7 | 8 | export const metadata: Metadata = { 9 | title: 'Create Next App', 10 | description: 'Generated by create next app', 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 |
22 | 23 | {children} 24 |
25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return

Home

; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/GoogleSignInButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | import { Button } from './ui/button'; 3 | 4 | interface GoogleSignInButtonProps { 5 | children: ReactNode; 6 | } 7 | const GoogleSignInButton: FC = ({ children }) => { 8 | const loginWithGoogle = () => console.log('login with google'); 9 | 10 | return ( 11 | 14 | ); 15 | }; 16 | 17 | export default GoogleSignInButton; 18 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { buttonVariants } from './ui/button'; 3 | import { HandMetal } from 'lucide-react'; 4 | 5 | const Navbar = () => { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 | 13 | Sign in 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Navbar; 21 | -------------------------------------------------------------------------------- /src/components/form/SignInForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useForm } from 'react-hook-form'; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage, 11 | } from '../ui/form'; 12 | import * as z from 'zod'; 13 | import { zodResolver } from '@hookform/resolvers/zod'; 14 | import { Input } from '../ui/input'; 15 | import { Button } from '../ui/button'; 16 | import Link from 'next/link'; 17 | import GoogleSignInButton from '../GoogleSignInButton'; 18 | 19 | const FormSchema = z.object({ 20 | email: z.string().min(1, 'Email is required').email('Invalid email'), 21 | password: z 22 | .string() 23 | .min(1, 'Password is required') 24 | .min(8, 'Password must have than 8 characters'), 25 | }); 26 | 27 | const SignInForm = () => { 28 | const form = useForm>({ 29 | resolver: zodResolver(FormSchema), 30 | defaultValues: { 31 | email: '', 32 | password: '', 33 | }, 34 | }); 35 | 36 | const onSubmit = (values: z.infer) => { 37 | console.log(values); 38 | }; 39 | 40 | return ( 41 |
42 | 43 |
44 | ( 48 | 49 | Email 50 | 51 | 52 | 53 | 54 | 55 | )} 56 | /> 57 | ( 61 | 62 | Password 63 | 64 | 69 | 70 | 71 | 72 | )} 73 | /> 74 |
75 | 78 |
79 |
80 | or 81 |
82 | Sign in with Google 83 |

84 | If you don't have an account, please  85 | 86 | Sign up 87 | 88 |

89 | 90 | ); 91 | }; 92 | 93 | export default SignInForm; 94 | -------------------------------------------------------------------------------- /src/components/form/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useForm } from 'react-hook-form'; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage, 11 | } from '../ui/form'; 12 | import * as z from 'zod'; 13 | import { zodResolver } from '@hookform/resolvers/zod'; 14 | import { Input } from '../ui/input'; 15 | import { Button } from '../ui/button'; 16 | import Link from 'next/link'; 17 | import GoogleSignInButton from '../GoogleSignInButton'; 18 | 19 | const FormSchema = z 20 | .object({ 21 | username: z.string().min(1, 'Username is required').max(100), 22 | email: z.string().min(1, 'Email is required').email('Invalid email'), 23 | password: z 24 | .string() 25 | .min(1, 'Password is required') 26 | .min(8, 'Password must have than 8 characters'), 27 | confirmPassword: z.string().min(1, 'Password confirmation is required'), 28 | }) 29 | .refine((data) => data.password === data.confirmPassword, { 30 | path: ['confirmPassword'], 31 | message: 'Password do not match', 32 | }); 33 | 34 | const SignUpForm = () => { 35 | const form = useForm>({ 36 | resolver: zodResolver(FormSchema), 37 | defaultValues: { 38 | username: '', 39 | email: '', 40 | password: '', 41 | confirmPassword: '', 42 | }, 43 | }); 44 | 45 | const onSubmit = (values: z.infer) => { 46 | console.log(values); 47 | }; 48 | 49 | return ( 50 |
51 | 52 |
53 | ( 57 | 58 | Username 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | /> 66 | ( 70 | 71 | Email 72 | 73 | 74 | 75 | 76 | 77 | )} 78 | /> 79 | ( 83 | 84 | Password 85 | 86 | 91 | 92 | 93 | 94 | )} 95 | /> 96 | ( 100 | 101 | Re-Enter your password 102 | 103 | 108 | 109 | 110 | 111 | )} 112 | /> 113 |
114 | 117 |
118 |
119 | or 120 |
121 | Sign up with Google 122 |

123 | If you don't have an account, please  124 | 125 | Sign in 126 | 127 |

128 | 129 | ); 130 | }; 131 | 132 | export default SignUpForm; 133 | -------------------------------------------------------------------------------- /src/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 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 | -------------------------------------------------------------------------------- /src/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 |