├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── data └── users.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── actions │ │ └── actions.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── users │ │ ├── @modal │ │ ├── (.)edit │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ └── default.tsx │ │ ├── UserRow.tsx │ │ ├── edit │ │ └── [id] │ │ │ ├── UserForm.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx ├── components │ ├── AlertConfirmation.tsx │ ├── DisplayServerActionResponse.tsx │ ├── InputWithLabel.tsx │ ├── Modal.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ └── table.tsx ├── lib │ ├── getUser.ts │ ├── getUsers.ts │ ├── safe-action.ts │ └── utils.ts └── schemas │ └── User.ts ├── tailwind.config.ts └── 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 | .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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # "next-safe-action Example Refactor 2 | 3 | _Add type safe and validated server actions to your Next.js App Router project with next-safe-action._ 4 | --- 5 | ### Author Links 6 | 7 | 👋 Hello, I'm Dave Gray. 8 | 9 | 📚 [My Courses](https://courses.davegray.codes/) 10 | 11 | ✅ [Check out my YouTube Channel with hundreds of tutorials](https://www.youtube.com/DaveGrayTeachesCode). 12 | 13 | 🚩 [Subscribe to my channel](https://bit.ly/3nGHmNn) 14 | 15 | 💖 [Support My Content](https://patreon.com/davegray) 16 | 17 | 🚀 Follow Me: 18 | 19 | - [Twitter](https://twitter.com/yesdavidgray) 20 | - [LinkedIn](https://www.linkedin.com/in/davidagray/) 21 | - [Blog](https://davegray.codes) 22 | 23 | --- 24 | 25 | ### Description 26 | 27 | 📺 [YouTube Video](https://youtu.be/ahB3DgUMs1A) for this repository. 28 | 29 | --- 30 | 31 | ### ⚙ Usage 32 | 33 | - npx json-server -w data/users.json -p 3500 34 | - In a separate terminal window: 35 | - npm install 36 | - npm run dev 37 | 38 | --- 39 | 40 | ### 🎓 Academic Honesty 41 | 42 | **DO NOT COPY FOR AN ASSIGNMENT** - Avoid plagiarism and adhere to the spirit of this [Academic Honesty Policy](https://www.freecodecamp.org/news/academic-honesty-policy/). 43 | 44 | --- 45 | 46 | ### 📚 Tutorial References 47 | 48 | - 🔗 [next-safe-action](https://next-safe-action.dev/) 49 | - 🔗 [Next.js](https://nextjs.org/) 50 | - 🔗 [Next.js Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) 51 | - 🔗 [Zod](https://zod.dev/) -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /data/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "firstname": "Leanne", 6 | "lastname": "Graham", 7 | "email": "Sincere@april.biz" 8 | }, 9 | { 10 | "id": 2, 11 | "firstname": "Ervin", 12 | "lastname": "Howell", 13 | "email": "Shanna@melissa.tv" 14 | }, 15 | { 16 | "id": 3, 17 | "firstname": "Clementine", 18 | "lastname": "Bauch", 19 | "email": "Nathan@yesenia.net" 20 | }, 21 | { 22 | "id": 4, 23 | "firstname": "Patricia", 24 | "lastname": "Lebsack", 25 | "email": "Julianne.OConner@kory.org" 26 | }, 27 | { 28 | "id": 5, 29 | "firstname": "Chelsey", 30 | "lastname": "Dietrich", 31 | "email": "Lucio_Hettinger@annie.ca" 32 | }, 33 | { 34 | "id": 6, 35 | "firstname": "Mrs. Dennis", 36 | "lastname": "Schulist", 37 | "email": "Karley_Dach@jasper.info" 38 | }, 39 | { 40 | "id": 7, 41 | "firstname": "Kurtis", 42 | "lastname": "Weissnat", 43 | "email": "Telly.Hoeger@billy.biz" 44 | }, 45 | { 46 | "id": 8, 47 | "firstname": "Nicholas", 48 | "lastname": "Runolfsdottir V", 49 | "email": "Sherwood@rosamond.me" 50 | }, 51 | { 52 | "id": 9, 53 | "firstname": "Dave", 54 | "lastname": "Gray", 55 | "email": "dave@davegray.codes" 56 | }, 57 | { 58 | "id": 10, 59 | "firstname": "Clementina", 60 | "lastname": "DuBuque", 61 | "email": "Rey.Padberg@karina.biz" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-form-modal", 3 | "version": "0.1.0", 4 | "private": false, 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.6.0", 13 | "@radix-ui/react-alert-dialog": "^1.0.5", 14 | "@radix-ui/react-dialog": "^1.0.5", 15 | "@radix-ui/react-label": "^2.0.2", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "lucide-react": "^0.394.0", 20 | "next": "14.2.3", 21 | "next-safe-action": "^7.0.2", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "react-hook-form": "^7.51.5", 25 | "tailwind-merge": "^2.3.0", 26 | "tailwindcss-animate": "^1.0.7", 27 | "zod": "^3.23.8", 28 | "zod-fetch": "^0.1.1" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "eslint": "^8", 35 | "eslint-config-next": "14.2.3", 36 | "postcss": "^8", 37 | "tailwindcss": "^3.4.1", 38 | "typescript": "^5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/actions/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { UserSchema } from "@/schemas/User" 4 | import { actionClient } from "@/lib/safe-action" 5 | import { flattenValidationErrors } from "next-safe-action" 6 | 7 | export const saveUserAction = actionClient 8 | .schema(UserSchema, { 9 | handleValidationErrorsShape: (ve) => flattenValidationErrors(ve).fieldErrors, 10 | }) 11 | .action(async ({ parsedInput: { id, firstname, lastname, email } }) => { 12 | 13 | await fetch(`http://localhost:3500/users/${id}`, { 14 | method: 'PATCH', 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | body: JSON.stringify({ 19 | firstname: firstname, 20 | lastname: lastname, 21 | email: email, 22 | }) 23 | }) 24 | 25 | //throw new Error("Dave Error") 26 | 27 | return { message: "User Updated! 🎉" } 28 | }) -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/next-safe-action-example/af8be3ca9f00a132bcf16e4df7a47550bf7de459/src/app/favicon.ico -------------------------------------------------------------------------------- /src/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: 20 14.3% 4.1%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 20 14.3% 4.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 20 14.3% 4.1%; 15 | 16 | --primary: 24 9.8% 10%; 17 | --primary-foreground: 60 9.1% 97.8%; 18 | 19 | --secondary: 60 4.8% 95.9%; 20 | --secondary-foreground: 24 9.8% 10%; 21 | 22 | --muted: 60 4.8% 95.9%; 23 | --muted-foreground: 25 5.3% 44.7%; 24 | 25 | --accent: 60 4.8% 95.9%; 26 | --accent-foreground: 24 9.8% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 60 9.1% 97.8%; 30 | 31 | --border: 20 5.9% 90%; 32 | --input: 20 5.9% 90%; 33 | --ring: 20 14.3% 4.1%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | :root { 39 | --background: 20 14.3% 4.1%; 40 | --foreground: 60 9.1% 97.8%; 41 | 42 | --card: 20 14.3% 4.1%; 43 | --card-foreground: 60 9.1% 97.8%; 44 | 45 | --popover: 20 14.3% 4.1%; 46 | --popover-foreground: 60 9.1% 97.8%; 47 | 48 | --primary: 60 9.1% 97.8%; 49 | --primary-foreground: 24 9.8% 10%; 50 | 51 | --secondary: 12 6.5% 15.1%; 52 | --secondary-foreground: 60 9.1% 97.8%; 53 | 54 | --muted: 12 6.5% 15.1%; 55 | --muted-foreground: 24 5.4% 63.9%; 56 | 57 | --accent: 12 6.5% 15.1%; 58 | --accent-foreground: 60 9.1% 97.8%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 60 9.1% 97.8%; 62 | 63 | --border: 12 6.5% 15.1%; 64 | --input: 12 6.5% 15.1%; 65 | --ring: 24 5.7% 82.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | 74 | body { 75 | @apply bg-background text-foreground; 76 | } 77 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | View Users 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/app/users/@modal/(.)edit/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import UserForm from "@/app/users/edit/[id]/UserForm" 2 | import { getUser } from "@/lib/getUser" 3 | import { Modal } from "@/components/Modal" 4 | 5 | type Props = { 6 | params: { 7 | id: number, 8 | } 9 | } 10 | 11 | export default async function EditUser({ params }: Props) { 12 | const { id } = params 13 | 14 | const user = await getUser(id) 15 | 16 | if (!user?.id) { 17 | return ( 18 | 19 |
20 |

21 | No User Found for that ID. 22 |

23 |
24 |
25 | ) 26 | } 27 | 28 | return ( 29 | 30 |
31 |

Edit User {id}

32 | 33 |
34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /src/app/users/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null 3 | } -------------------------------------------------------------------------------- /src/app/users/UserRow.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { 3 | TableCell, 4 | TableRow, 5 | } from "@/components/ui/table" 6 | import { useRouter } from "next/navigation" 7 | import type { User } from "@/schemas/User" 8 | 9 | type Props = { 10 | user: User 11 | } 12 | 13 | export default function UserRow({ user }: Props) { 14 | const router = useRouter() 15 | 16 | const handleClick = () => { 17 | router.push(`/users/edit/${user.id}`) 18 | } 19 | 20 | return ( 21 | 22 | 23 | {user.email} 24 | 25 | 26 | {user.firstname} 27 | 28 | 29 | {user.lastname} 30 | 31 | 32 | ) 33 | } -------------------------------------------------------------------------------- /src/app/users/edit/[id]/UserForm.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useForm } from "react-hook-form" 4 | import { Form } from "@/components/ui/form" 5 | import { Button } from "@/components/ui/button" 6 | import { InputWithLabel } from "@/components/InputWithLabel" 7 | import { zodResolver } from "@hookform/resolvers/zod" 8 | import { UserSchema } from "@/schemas/User" 9 | import type { User } from "@/schemas/User" 10 | import { saveUserAction } from "@/app/actions/actions" 11 | import { useEffect } from "react" 12 | import { useRouter } from "next/navigation" 13 | 14 | import { useAction } from "next-safe-action/hooks" 15 | import { DisplayServerActionResponse } from "@/components/DisplayServerActionResponse" 16 | 17 | type Props = { 18 | user: User 19 | } 20 | 21 | export default function UserForm({ user }: Props) { 22 | const router = useRouter() 23 | const { execute, result, isExecuting } = useAction(saveUserAction) 24 | 25 | const form = useForm({ 26 | resolver: zodResolver(UserSchema), 27 | defaultValues: { ...user }, 28 | }) 29 | 30 | useEffect(() => { 31 | // boolean value to indicate form has not been saved 32 | localStorage.setItem("userFormModified", form.formState.isDirty.toString()) 33 | }, [form.formState.isDirty]) 34 | 35 | async function onSubmit() { 36 | /* No need to validate here because 37 | react-hook-form already validates with 38 | our Zod schema */ 39 | 40 | // Test validation errors: 41 | // execute({ 42 | // id: 9, 43 | // firstname: "D", 44 | // lastname: "G", 45 | // email: "davegray.codes", 46 | // }) 47 | execute(form.getValues()) 48 | router.refresh() // could grab a new timestamp from db 49 | // reset dirty fields 50 | form.reset(form.getValues()) 51 | //} 52 | } 53 | 54 | return ( 55 |
56 | 57 | 58 | 59 |
60 | { 61 | e.preventDefault() 62 | form.handleSubmit(onSubmit)(); 63 | }} className="flex flex-col gap-4"> 64 | 65 | 69 | 73 | 77 |
78 | 79 | 84 |
85 | 86 | 87 |
88 | ) 89 | } -------------------------------------------------------------------------------- /src/app/users/edit/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import UserForm from "./UserForm" 2 | import { getUser } from "@/lib/getUser" 3 | 4 | type Props = { 5 | params: { 6 | id: number, 7 | } 8 | } 9 | 10 | export default async function EditUser({ params }: Props) { 11 | const { id } = params 12 | 13 | const user = await getUser(id) 14 | 15 | if (!user?.id) { 16 | return ( 17 |
18 |

19 | No User Found for that ID. 20 |

21 |
22 | ) 23 | } 24 | 25 | return ( 26 |
27 |

Edit User {id}

28 | 29 |
30 | ) 31 | } -------------------------------------------------------------------------------- /src/app/users/layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function UsersLayout({ 3 | children, 4 | modal, 5 | }: Readonly<{ 6 | children: React.ReactNode; 7 | modal: React.ReactNode; 8 | }>) { 9 | return ( 10 | <> 11 | {modal} 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { getUsers } from "@/lib/getUsers" 2 | import { 3 | Table, 4 | TableBody, 5 | TableHead, 6 | TableHeader, 7 | TableRow, 8 | } from "@/components/ui/table" 9 | import UserRow from "./UserRow" 10 | 11 | export default async function Users() { 12 | const usersData = await getUsers() 13 | 14 | return ( 15 |
16 |

Users List

17 | 18 | 19 | 20 | Email 21 | First 22 | Last 23 | 24 | 25 | 26 | {usersData.map(user => ( 27 | 28 | ))} 29 | 30 |
31 |
32 | ) 33 | } -------------------------------------------------------------------------------- /src/components/AlertConfirmation.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertDialog, 3 | AlertDialogAction, 4 | AlertDialogCancel, 5 | AlertDialogContent, 6 | AlertDialogDescription, 7 | AlertDialogFooter, 8 | AlertDialogHeader, 9 | AlertDialogTitle, 10 | } from "@/components/ui/alert-dialog" 11 | 12 | type Props = { 13 | open: boolean, 14 | setOpen: React.Dispatch>, 15 | confirmationAction: () => void, 16 | message: string, 17 | } 18 | 19 | export function AlertConfirmation({ 20 | open, setOpen, confirmationAction, message, 21 | }: Props) { 22 | return ( 23 | 24 | 25 | 26 | Are you sure? 27 | 28 | {message} 29 | 30 | 31 | 32 | Cancel 33 | Continue 36 | 37 | 38 | 39 | ) 40 | } -------------------------------------------------------------------------------- /src/components/DisplayServerActionResponse.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | result: { 3 | data?: { 4 | message?: string, 5 | }, 6 | serverError?: string, 7 | fetchError?: string, 8 | validationErrors?: Record | undefined, 9 | 10 | } 11 | } 12 | 13 | export function DisplayServerActionResponse({ result }: Props) { 14 | 15 | const { data, serverError, fetchError, validationErrors } = result 16 | 17 | return ( 18 | <> 19 | {/* Success Message */} 20 | {data?.message ? ( 21 |

{data.message}

22 | ) : null} 23 | 24 | {serverError ? ( 25 |
26 |

{serverError}

27 |
28 | ) : null} 29 | 30 | {fetchError ? ( 31 |
32 |

{fetchError}

33 |
34 | ) : null} 35 | 36 | {validationErrors ? ( 37 |
38 | {Object.keys(validationErrors).map(key => ( 39 |

{`${key}: ${validationErrors && validationErrors[key as keyof typeof validationErrors]}`}

40 | ))} 41 |
42 | ) : null} 43 | 44 | ) 45 | } -------------------------------------------------------------------------------- /src/components/InputWithLabel.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useFormContext } from "react-hook-form" 4 | import { Input } from "@/components/ui/input" 5 | import { Button } from "@/components/ui/button" 6 | import { XIcon } from "lucide-react" 7 | import { 8 | FormField, 9 | FormControl, 10 | FormItem, 11 | FormLabel, 12 | FormMessage, 13 | } from "@/components/ui/form" 14 | 15 | type Props = { 16 | fieldTitle: string, 17 | nameInSchema: string, 18 | placeholder?: string, 19 | labelLeft?: boolean, 20 | readOnly?: boolean, 21 | } 22 | 23 | export function InputWithLabel({ fieldTitle, nameInSchema, placeholder, labelLeft, readOnly }: Props) { 24 | const form = useFormContext() 25 | 26 | const fieldTitleNoSpaces = fieldTitle.replaceAll(' ', '-') 27 | 28 | return ( 29 | ( 33 | 34 | 35 | {fieldTitle} 36 | 37 | 38 |
39 |
40 | 41 | field.onChange(e.target.value)} 50 | /> 51 | 52 |
53 | {!readOnly ? ( 54 | 67 | ) : null} 68 |
69 | 70 | 71 |
72 | )} 73 | /> 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Dialog, 5 | DialogOverlay, 6 | DialogContent, 7 | } from "@/components/ui/dialog" 8 | import { AlertConfirmation } from "./AlertConfirmation" 9 | import { useRouter } from "next/navigation" 10 | import { useState } from 'react' 11 | 12 | export function Modal({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | const [showExitConfirmation, setShowExitConfirmation] = useState(false) 18 | const router = useRouter() 19 | 20 | const closeModal = () => { 21 | router.back() 22 | } 23 | 24 | const handleOpenChange = () => { 25 | const isUserFormModified = localStorage.getItem("userFormModified") 26 | if (isUserFormModified && JSON.parse(isUserFormModified)) { 27 | setShowExitConfirmation(true) 28 | } else { 29 | router.back() 30 | } 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | 43 | {children} 44 | 45 | 46 | 47 | ) 48 | } -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /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 |