├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── actions └── form.ts ├── app ├── (auth) │ ├── layout.tsx │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── (dashboard) │ ├── builder │ │ └── [id] │ │ │ ├── error.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── forms │ │ └── [id] │ │ │ ├── error.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── submit │ └── [formURL] │ ├── layout.tsx │ ├── loading.tsx │ └── page.tsx ├── components.json ├── components ├── CreateFormBtn.tsx ├── Designer.tsx ├── DesignerSidebar.tsx ├── DragOverlayWrapper.tsx ├── FormBuilder.tsx ├── FormElements.tsx ├── FormElementsSidebar.tsx ├── FormLinkShare.tsx ├── FormSubmitComponent.tsx ├── Logo.tsx ├── PreviewDialogBtn.tsx ├── PropertiesFormSidebar.tsx ├── PublishFormBtn.tsx ├── SaveFormBtn.tsx ├── SidebarBtnElement.tsx ├── ThemeSwitcher.tsx ├── VisitBtn.tsx ├── context │ └── DesignerContext.tsx ├── fields │ ├── CheckboxField.tsx │ ├── DateField.tsx │ ├── NumberField.tsx │ ├── ParagraphField.tsx │ ├── SelectField.tsx │ ├── SeparatorField.tsx │ ├── SpacerField.tsx │ ├── SubTitleField.tsx │ ├── TextAreaField.tsx │ ├── TextField.tsx │ └── TitleField.tsx ├── hooks │ └── useDesigner.tsx ├── providers │ └── ThemeProvider.tsx └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── lib ├── idGenerator.ts ├── prisma.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── prisma ├── migrations │ ├── 20231027154929_initial │ │ └── migration.sql │ ├── 20231029052228_ │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── paper-dark.svg └── paper.svg ├── schemas └── form.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_SECRET_KEY= 3 | 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 6 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ 7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ 8 | 9 | POSTGRES_PRISMA_URL= 10 | POSTGRES_URL_NON_POOLING= 11 | -------------------------------------------------------------------------------- /.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 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mahmudul Alam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next DnD FormBuilder 2 | 3 | Next DnD FormBuilder is a drag-and-drop form builder built with Next.js, Prisma, and TypeScript. This project allows users to create dynamic forms by simply dragging and dropping form elements onto a canvas. Below is an overview of the project structure and setup instructions. 4 | 5 | ## Prerequisites 6 | 7 | Before running the project, ensure you have the following installed: 8 | 9 | - Node.js (v14 or higher) 10 | - npm or yarn 11 | - PostgreSQL 12 | 13 | ## Setup 14 | 15 | 1. Clone the repository: 16 | 17 | ```bash 18 | git clone https://github.com/devmahmud/next-dnd-formbuilder.git 19 | ``` 20 | 21 | 2. Install dependencies: 22 | 23 | ```bash 24 | npm install 25 | # or 26 | yarn 27 | ``` 28 | 29 | 3. For authentication, This project uses [Clerk](https://clerk.com/). Create a free clerk account and from `Developers/API Keys` you will get Publishable key and secret key for the Next.js project 30 | 31 | 4. Set up environment variables: 32 | 33 | Create a `.env` file in the root directory and provide the following variables: 34 | 35 | ```plaintext 36 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 37 | CLERK_SECRET_KEY= 38 | 39 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 40 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 41 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ 42 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ 43 | 44 | POSTGRES_PRISMA_URL=postgresql://username:password@hostname:port/database_name 45 | POSTGRES_URL_NON_POOLING=postgresql://username:password@hostname:port/database_name 46 | 47 | ``` 48 | 49 | Ensure to replace the placeholders with your actual values. 50 | 51 | 5. Initialize Prisma: 52 | 53 | ```bash 54 | npx prisma generate 55 | # or 56 | yarn prisma generate 57 | ``` 58 | 59 | 6. Run the development server: 60 | 61 | ```bash 62 | npm run dev 63 | # or 64 | yarn dev 65 | ``` 66 | 67 | 68 | ## Scripts 69 | 70 | - `dev`: Start the development server. 71 | - `build`: Build the Next.js application for production. 72 | - `start`: Start the production server. 73 | - `lint`: Lint the code using Next.js linting configurations. 74 | - `postinstall`: Generate Prisma client. 75 | 76 | ## Project Structure 77 | 78 | - `app/`: Contains Next.js pages. 79 | - `public/`: Contains static assets. 80 | - `components/`: Contains All the react components for the project. 81 | - `prisma/`: Contains Prisma schema and migrations. 82 | - `lib`: Contains helper functions. 83 | - `schema`: Contains zod form schema. 84 | 85 | ## Usage 86 | 87 | - Visit the homepage to access the drag-and-drop form builder interface. 88 | - Drag form elements from the toolbox onto the canvas to create your form. 89 | - Customize form element properties as needed. 90 | - Save the form configuration and integrate it with your application. 91 | 92 | Feel free to modify and extend the project according to your requirements! 93 | 94 | ## License 95 | 96 | This project is licensed under the [MIT License](LICENSE). -------------------------------------------------------------------------------- /actions/form.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import prisma from "@/lib/prisma"; 4 | import { formSchema, formSchemaType } from "@/schemas/form"; 5 | import { currentUser } from "@clerk/nextjs"; 6 | 7 | class UserNotFoundErr extends Error {} 8 | 9 | export async function GetFormStats() { 10 | const user = await currentUser(); 11 | 12 | if (!user) { 13 | throw new UserNotFoundErr("User not found"); 14 | } 15 | 16 | const stats = await prisma.form.aggregate({ 17 | where: { 18 | userId: user.id, 19 | }, 20 | _sum: { 21 | visits: true, 22 | submissions: true, 23 | }, 24 | }); 25 | 26 | const visits = stats._sum.visits || 0; 27 | const submissions = stats._sum.submissions || 0; 28 | 29 | const submissionRate = visits > 0 ? (submissions / visits) * 100 : 0; 30 | const bounceRate = 100 - submissionRate; 31 | 32 | return { visits, submissions, submissionRate, bounceRate }; 33 | } 34 | 35 | export async function CreateForm(data: formSchemaType) { 36 | const validation = formSchema.safeParse(data); 37 | 38 | if (!validation.success) { 39 | throw new Error("Invalid form data"); 40 | } 41 | 42 | const user = await currentUser(); 43 | 44 | if (!user) { 45 | throw new UserNotFoundErr("User not found"); 46 | } 47 | 48 | const form = await prisma.form.create({ 49 | data: { 50 | name: data.name, 51 | description: data.description, 52 | userId: user.id, 53 | }, 54 | }); 55 | 56 | if (!form) { 57 | throw new Error("Failed to create form"); 58 | } 59 | 60 | return form.id; 61 | } 62 | 63 | export async function GetForms() { 64 | const user = await currentUser(); 65 | 66 | if (!user) { 67 | throw new UserNotFoundErr("User not found"); 68 | } 69 | 70 | return await prisma.form.findMany({ 71 | where: { 72 | userId: user.id, 73 | }, 74 | orderBy: { 75 | createdAt: "desc", 76 | }, 77 | }); 78 | } 79 | 80 | export async function GetFormById(id: number) { 81 | const user = await currentUser(); 82 | if (!user) { 83 | throw new UserNotFoundErr("User not found"); 84 | } 85 | 86 | return await prisma.form.findUnique({ 87 | where: { 88 | userId: user.id, 89 | id, 90 | }, 91 | }); 92 | } 93 | 94 | export async function UpdateFormContent(id: number, jsonContent: string) { 95 | const user = await currentUser(); 96 | 97 | if (!user) { 98 | throw new UserNotFoundErr("User not found"); 99 | } 100 | 101 | return await prisma.form.update({ 102 | where: { 103 | userId: user.id, 104 | id, 105 | }, 106 | data: { 107 | content: jsonContent, 108 | }, 109 | }); 110 | } 111 | 112 | export async function PublishForm(id: number) { 113 | const user = await currentUser(); 114 | 115 | if (!user) { 116 | throw new UserNotFoundErr("User not found"); 117 | } 118 | 119 | return await prisma.form.update({ 120 | where: { 121 | userId: user.id, 122 | id, 123 | }, 124 | data: { 125 | published: true, 126 | }, 127 | }); 128 | } 129 | 130 | export async function GetFormContentByURL(formURL: string) { 131 | return await prisma.form.update({ 132 | where: { 133 | shareURL: formURL, 134 | }, 135 | data: { 136 | visits: { 137 | increment: 1, 138 | }, 139 | }, 140 | select: { 141 | content: true, 142 | }, 143 | }); 144 | } 145 | 146 | export async function SubmitForm(formURL: string, content: string) { 147 | return await prisma.form.update({ 148 | where: { 149 | shareURL: formURL, 150 | published: true, 151 | }, 152 | data: { 153 | submissions: { 154 | increment: 1, 155 | }, 156 | FormSubmissions: { 157 | create: { 158 | content, 159 | }, 160 | }, 161 | }, 162 | }); 163 | } 164 | 165 | export async function GetFormWithSubmissions(id: number) { 166 | const user = await currentUser(); 167 | 168 | if (!user) { 169 | throw new UserNotFoundErr("User not found"); 170 | } 171 | 172 | return await prisma.form.findUnique({ 173 | where: { 174 | userId: user.id, 175 | id, 176 | }, 177 | include: { 178 | FormSubmissions: true, 179 | }, 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import Logo from "@/components/Logo"; 3 | import ThemeSwitcher from "@/components/ThemeSwitcher"; 4 | 5 | const Layout = ({ children }: { children: ReactNode }) => { 6 | return ( 7 |
8 | 14 |
15 | {children} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Layout; 22 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(dashboard)/builder/[id]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Link from "next/link"; 5 | import { useEffect } from "react"; 6 | 7 | export default function ErrorPage({ error }: { error: Error }) { 8 | useEffect(() => { 9 | console.error(error); 10 | }, [error]); 11 | 12 | return ( 13 |
14 |

15 | {error?.message || "Something went wrong."} 16 |

17 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/(dashboard)/builder/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export default function Layout({ children }: { children: ReactNode }) { 4 | return
{children}
; 5 | } 6 | -------------------------------------------------------------------------------- /app/(dashboard)/builder/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/(dashboard)/builder/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetFormById } from "@/actions/form"; 2 | import FormBuilder from "@/components/FormBuilder"; 3 | 4 | interface Props { 5 | params: { 6 | id: string; 7 | }; 8 | } 9 | 10 | async function BuilderPage({ params }: Props) { 11 | const { id } = params; 12 | const form = await GetFormById(+id); 13 | if (!form) throw new Error("Form not found"); 14 | 15 | return ; 16 | } 17 | 18 | export default BuilderPage; 19 | -------------------------------------------------------------------------------- /app/(dashboard)/forms/[id]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Link from "next/link"; 5 | import { useEffect } from "react"; 6 | 7 | export default function ErrorPage({ error }: { error: Error }) { 8 | useEffect(() => { 9 | console.error(error); 10 | }, [error]); 11 | 12 | return ( 13 |
14 |

15 | {error?.message || "Something went wrong."} 16 |

17 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/(dashboard)/forms/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export default function Layout({ children }: { children: ReactNode }) { 4 | return ( 5 |
{children}
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/(dashboard)/forms/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/(dashboard)/forms/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ListChecks, 3 | LucideView, 4 | MousePointerClick, 5 | Waypoints, 6 | } from "lucide-react"; 7 | import { GetFormById, GetFormWithSubmissions } from "@/actions/form"; 8 | import FormLinkShare from "@/components/FormLinkShare"; 9 | import VisitBtn from "@/components/VisitBtn"; 10 | import { StatsCard } from "../../page"; 11 | import { ElementsType, FormElementInstance } from "@/components/FormElements"; 12 | import { 13 | Table, 14 | TableBody, 15 | TableCell, 16 | TableHead, 17 | TableHeader, 18 | TableRow, 19 | } from "@/components/ui/table"; 20 | import { format, formatDistance } from "date-fns"; 21 | import { Badge } from "@/components/ui/badge"; 22 | import { Checkbox } from "@/components/ui/checkbox"; 23 | 24 | interface Props { 25 | params: { 26 | id: string; 27 | }; 28 | } 29 | 30 | async function FormDetailPage({ params }: Props) { 31 | const { id } = params; 32 | const form = await GetFormById(+id); 33 | if (!form) throw new Error("Form not found"); 34 | 35 | const { submissions, visits } = form; 36 | 37 | const submissionRate = visits > 0 ? (submissions / visits) * 100 : 0; 38 | const bounceRate = 100 - submissionRate; 39 | 40 | return ( 41 | <> 42 |
43 |
44 |

{form.name}

45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 | } 58 | helperText="All time form visits" 59 | loading={false} 60 | className="shadow-md shadow-blue-600" 61 | /> 62 | } 66 | helperText="All time submissions" 67 | loading={false} 68 | className="shadow-md shadow-yellow-600" 69 | /> 70 | } 74 | helperText="Visits that result in a form submission" 75 | loading={false} 76 | className="shadow-md shadow-green-600" 77 | /> 78 | } 82 | helperText="Visits that leaves without interacting" 83 | loading={false} 84 | className="shadow-md shadow-red-600" 85 | /> 86 |
87 | 88 |
89 | 90 |
91 | 92 | ); 93 | } 94 | 95 | export default FormDetailPage; 96 | 97 | type Row = { [key: string]: string } & { 98 | submittedAt: Date; 99 | }; 100 | 101 | async function SubmissionsTable({ id }: { id: number }) { 102 | const form = await GetFormWithSubmissions(id); 103 | 104 | if (!form) throw new Error("Form not found"); 105 | 106 | const formElements = JSON.parse(form.content) as FormElementInstance[]; 107 | const columns: { 108 | id: string; 109 | label: string; 110 | required: boolean; 111 | type: ElementsType; 112 | }[] = []; 113 | 114 | formElements.forEach((element) => { 115 | switch (element.type) { 116 | case "TextField": 117 | case "NumberField": 118 | case "TextAreaField": 119 | case "DateField": 120 | case "SelectField": 121 | case "CheckboxField": 122 | columns.push({ 123 | id: element.id, 124 | label: element.extraAttributes?.label, 125 | required: element.extraAttributes?.required, 126 | type: element.type, 127 | }); 128 | break; 129 | default: 130 | break; 131 | } 132 | }); 133 | 134 | const rows: Row[] = form.FormSubmissions.map((submission) => ({ 135 | ...JSON.parse(submission.content), 136 | submittedAt: submission.createdAt, 137 | })); 138 | 139 | return ( 140 | <> 141 |

Submissions

142 |
143 | 144 | 145 | 146 | {columns.map((column) => ( 147 | 148 | {column.label} 149 | 150 | ))} 151 | 152 | Submitted at 153 | 154 | 155 | 156 | 157 | {rows.map((row, idx) => ( 158 | 159 | {columns.map((column) => ( 160 | 165 | ))} 166 | 167 | {formatDistance(row.submittedAt, new Date(), { 168 | addSuffix: true, 169 | })} 170 | 171 | 172 | ))} 173 | 174 |
175 |
176 | 177 | ); 178 | } 179 | 180 | function RowCell({ type, value }: { type: ElementsType; value: string }) { 181 | let node: React.ReactNode = value; 182 | 183 | switch (type) { 184 | case "DateField": 185 | if (!value) break; 186 | node = ( 187 | {format(new Date(value), "dd/MM/yyyy")} 188 | ); 189 | break; 190 | case "CheckboxField": 191 | node = ( 192 | 193 | {value} 194 | 195 | ); 196 | break; 197 | } 198 | return {node}; 199 | } 200 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { UserButton } from "@clerk/nextjs"; 3 | import Logo from "@/components/Logo"; 4 | import ThemeSwitcher from "@/components/ThemeSwitcher"; 5 | 6 | const Layout = ({ children }: { children: ReactNode }) => { 7 | return ( 8 |
9 | 16 |
{children}
17 |
18 | ); 19 | }; 20 | 21 | export default Layout; 22 | -------------------------------------------------------------------------------- /app/(dashboard)/page.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, Suspense } from "react"; 2 | import Link from "next/link"; 3 | import { GetFormStats, GetForms } from "@/actions/form"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import { Separator } from "@/components/ui/separator"; 13 | import { Skeleton } from "@/components/ui/skeleton"; 14 | import { 15 | ArrowRight, 16 | Edit, 17 | ListChecks, 18 | LucideView, 19 | MousePointerClick, 20 | View, 21 | Waypoints, 22 | } from "lucide-react"; 23 | import CreateFormBtn from "@/components/CreateFormBtn"; 24 | import { Form } from "@prisma/client"; 25 | import { Badge } from "@/components/ui/badge"; 26 | import { formatDistance } from "date-fns"; 27 | import { Button } from "@/components/ui/button"; 28 | 29 | export default function Home() { 30 | return ( 31 |
32 | }> 33 | 34 | 35 | 36 |

Your forms

37 | 38 |
39 | 40 | ( 42 | 43 | ))} 44 | > 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | 52 | async function CardStatsWrapper() { 53 | const stats = await GetFormStats(); 54 | return ; 55 | } 56 | 57 | interface StatsCardsProps { 58 | data?: Awaited>; 59 | loading: boolean; 60 | } 61 | 62 | function StatsCards({ data, loading }: StatsCardsProps) { 63 | return ( 64 |
65 | } 69 | helperText="All time form visits" 70 | loading={loading} 71 | className="shadow-md shadow-blue-600" 72 | /> 73 | } 77 | helperText="All time submissions" 78 | loading={loading} 79 | className="shadow-md shadow-yellow-600" 80 | /> 81 | } 85 | helperText="Visits that result in a form submission" 86 | loading={loading} 87 | className="shadow-md shadow-green-600" 88 | /> 89 | } 93 | helperText="Visits that leaves without interacting" 94 | loading={loading} 95 | className="shadow-md shadow-red-600" 96 | /> 97 |
98 | ); 99 | } 100 | 101 | interface StatsCardProps { 102 | title: string; 103 | icon: ReactNode; 104 | value: string; 105 | helperText: string; 106 | className: string; 107 | loading: boolean; 108 | } 109 | 110 | export function StatsCard({ 111 | title, 112 | icon, 113 | value, 114 | helperText, 115 | className, 116 | loading, 117 | }: StatsCardProps) { 118 | return ( 119 | 120 | 121 | {title} 122 | {icon} 123 | 124 | 125 |
126 | {loading ? ( 127 | 128 | 0 129 | 130 | ) : ( 131 | value 132 | )} 133 |
134 |

{helperText}

135 |
136 |
137 | ); 138 | } 139 | 140 | function FormCardSkeleton() { 141 | return ; 142 | } 143 | 144 | async function FormCards() { 145 | const forms = await GetForms(); 146 | return ( 147 | <> 148 | {forms.map((form) => ( 149 | 150 | ))} 151 | 152 | ); 153 | } 154 | 155 | function FormCard({ form }: { form: Form }) { 156 | return ( 157 | 158 | 159 | 160 | {form.name} 161 | {form.published ? ( 162 | Published 163 | ) : ( 164 | Draft 165 | )} 166 | 167 | 168 | {formatDistance(form.createdAt, new Date(), { addSuffix: true })} 169 | {form.published && ( 170 | 171 | 172 | {form.visits.toLocaleString()} 173 | 174 | {form.submissions.toLocaleString()} 175 | 176 | )} 177 | 178 | 179 | 180 | {form.description || "No description"} 181 | 182 | 183 | {form.published && ( 184 | 189 | )} 190 | {!form.published && ( 191 | 196 | )} 197 | 198 | 199 | ); 200 | } 201 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmahmud/next-dnd-formbuilder/b136abae0bb9f9890cb635e018459e820e5499bf/app/favicon.ico -------------------------------------------------------------------------------- /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: 224 71.4% 4.1%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 224 71.4% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 224 71.4% 4.1%; 13 | --primary: 220.9 39.3% 11%; 14 | --primary-foreground: 210 20% 98%; 15 | --secondary: 220 14.3% 95.9%; 16 | --secondary-foreground: 220.9 39.3% 11%; 17 | --muted: 220 14.3% 95.9%; 18 | --muted-foreground: 220 8.9% 46.1%; 19 | --accent: 220 14.3% 95.9%; 20 | --accent-foreground: 220.9 39.3% 11%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 20% 98%; 23 | --border: 220 13% 91%; 24 | --input: 220 13% 91%; 25 | --ring: 224 71.4% 4.1%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 224 71.4% 4.1%; 31 | --foreground: 210 20% 98%; 32 | --card: 224 71.4% 4.1%; 33 | --card-foreground: 210 20% 98%; 34 | --popover: 224 71.4% 4.1%; 35 | --popover-foreground: 210 20% 98%; 36 | --primary: 210 20% 98%; 37 | --primary-foreground: 220.9 39.3% 11%; 38 | --secondary: 215 27.9% 16.9%; 39 | --secondary-foreground: 210 20% 98%; 40 | --muted: 215 27.9% 16.9%; 41 | --muted-foreground: 217.9 10.6% 64.9%; 42 | --accent: 215 27.9% 16.9%; 43 | --accent-foreground: 210 20% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 210 20% 98%; 46 | --border: 215 27.9% 16.9%; 47 | --input: 215 27.9% 16.9%; 48 | --ring: 216 12.2% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import NextTopLoader from "nextjs-toploader"; 6 | 7 | import { ThemeProvider } from "@/components/providers/ThemeProvider"; 8 | import DesignerContextProvider from "@/components/context/DesignerContext"; 9 | import { Toaster } from "@/components/ui/toaster"; 10 | 11 | import "./globals.css"; 12 | 13 | const inter = Inter({ subsets: ["latin"] }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Formify", 17 | description: "Drag and drop form builder", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: { 23 | children: React.ReactNode; 24 | }) { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 37 | {children} 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/submit/[formURL]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import Logo from "@/components/Logo"; 3 | import ThemeSwitcher from "@/components/ThemeSwitcher"; 4 | 5 | const Layout = ({ children }: { children: ReactNode }) => { 6 | return ( 7 |
8 | 12 |
{children}
13 |
14 | ); 15 | }; 16 | 17 | export default Layout; 18 | -------------------------------------------------------------------------------- /app/submit/[formURL]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/submit/[formURL]/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetFormContentByURL } from "@/actions/form"; 2 | import { FormElementInstance } from "@/components/FormElements"; 3 | import FormSubmitComponent from "@/components/FormSubmitComponent"; 4 | 5 | interface Props { 6 | params: { 7 | formURL: string; 8 | }; 9 | } 10 | 11 | async function SubmitPage({ params }: Props) { 12 | const form = await GetFormContentByURL(params.formURL); 13 | if (!form) throw new Error("Form not found"); 14 | 15 | const formContent = JSON.parse(form.content) as FormElementInstance[]; 16 | 17 | return ( 18 | 19 | ); 20 | } 21 | 22 | export default SubmitPage; 23 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /components/CreateFormBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "./ui/dialog"; 12 | 13 | import { Button } from "./ui/button"; 14 | 15 | import { 16 | Form, 17 | FormControl, 18 | FormField, 19 | FormItem, 20 | FormLabel, 21 | FormMessage, 22 | } from "./ui/form"; 23 | import { zodResolver } from "@hookform/resolvers/zod"; 24 | import { useForm } from "react-hook-form"; 25 | import { Input } from "./ui/input"; 26 | import { Textarea } from "./ui/textarea"; 27 | import { FilePlus, Loader2 } from "lucide-react"; 28 | import { toast } from "./ui/use-toast"; 29 | import { formSchemaType, formSchema } from "@/schemas/form"; 30 | import { CreateForm } from "@/actions/form"; 31 | import { useRouter } from "next/navigation"; 32 | 33 | export default function CreateFormBtn() { 34 | const router = useRouter(); 35 | const form = useForm({ 36 | resolver: zodResolver(formSchema), 37 | }); 38 | 39 | async function onSubmit(data: formSchemaType) { 40 | try { 41 | const formId = await CreateForm(data); 42 | toast({ 43 | title: "Success", 44 | description: "Form created successfully.", 45 | }); 46 | router.push(`/builder/${formId}`); 47 | } catch (error) { 48 | toast({ 49 | title: "Error", 50 | description: "Something went wrong. Please try again later.", 51 | variant: "destructive", 52 | }); 53 | } 54 | } 55 | 56 | return ( 57 | 58 | 59 | 65 | 66 | 67 | 68 | Create form 69 | 70 | Create a new form to start collecting responses. 71 | 72 | 73 |
74 | 75 | ( 79 | 80 | Name 81 | 82 | 83 | 84 | 85 | 86 | )} 87 | /> 88 | ( 92 | 93 | Description 94 | 95 |