├── .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 |
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 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/components/Designer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useDndMonitor, useDraggable, useDroppable } from "@dnd-kit/core";
5 | import DesignerSidebar from "./DesignerSidebar";
6 | import { cn } from "@/lib/utils";
7 | import useDesigner from "./hooks/useDesigner";
8 | import {
9 | ElementsType,
10 | FormElementInstance,
11 | FormElements,
12 | } from "./FormElements";
13 | import { idGenerator } from "@/lib/idGenerator";
14 | import { Button } from "./ui/button";
15 | import { Trash2 } from "lucide-react";
16 |
17 | function Designer() {
18 | const {
19 | elements,
20 | addElement,
21 | selectedElement,
22 | setSelectedElement,
23 | removeElement,
24 | } = useDesigner();
25 |
26 | const droppable = useDroppable({
27 | id: "designer-drop-area",
28 | data: {
29 | isDesignerDropArea: true,
30 | },
31 | });
32 |
33 | useDndMonitor({
34 | onDragEnd({ active, over }) {
35 | if (!active || !over) return;
36 |
37 | const isDesignerBtnElement = active.data?.current?.isDesignerBtnElement;
38 | const isDroppingOverDesignerDropArea =
39 | over.data?.current?.isDesignerDropArea;
40 |
41 | const droppingSidebarBtnOverDesignerDropArea =
42 | isDesignerBtnElement && isDroppingOverDesignerDropArea;
43 |
44 | // First check if the user is dropping a sidebar button over the designer drop area
45 | if (droppingSidebarBtnOverDesignerDropArea) {
46 | const type = active.data?.current?.type;
47 | const newElement = FormElements[type as ElementsType].construct(
48 | idGenerator(),
49 | );
50 | addElement(elements.length, newElement);
51 | return;
52 | }
53 |
54 | const isDroppingOverDesignerElementTopHalf =
55 | over?.data?.current?.isDesignerElementTopHalf;
56 | const isDroppingOverDesignerElementBottomHalf =
57 | over?.data?.current?.isDesignerElementBottomHalf;
58 |
59 | const isDroppingOverDesignerElement =
60 | isDroppingOverDesignerElementTopHalf ||
61 | isDroppingOverDesignerElementBottomHalf;
62 |
63 | const droppingSidebarBtnOverDesignerElement =
64 | isDesignerBtnElement && isDroppingOverDesignerElement;
65 |
66 | // Then check if the user is dropping a sidebar button over a designer element
67 | if (droppingSidebarBtnOverDesignerElement) {
68 | const type = active.data?.current?.type;
69 | const newElement = FormElements[type as ElementsType].construct(
70 | idGenerator(),
71 | );
72 |
73 | const overId = over.data?.current?.elementId;
74 | const overElementIndex = elements.findIndex((el) => el.id === overId);
75 | if (overElementIndex === -1) throw new Error("Element not found");
76 |
77 | let indexForNewElement = overElementIndex;
78 | if (isDroppingOverDesignerElementBottomHalf) {
79 | indexForNewElement = overElementIndex + 1;
80 | }
81 |
82 | addElement(indexForNewElement, newElement);
83 | return;
84 | }
85 |
86 | const isDraggingDesignerElement = active.data?.current?.isDesignerElement;
87 | const draggingDesignerElementOverAnotherDesignerElement =
88 | isDroppingOverDesignerElement && isDraggingDesignerElement;
89 |
90 | // Then check if the user is dragging a designer element over another designer element
91 | if (draggingDesignerElementOverAnotherDesignerElement) {
92 | const activeId = active.data?.current?.elementId;
93 | const overId = over.data?.current?.elementId;
94 |
95 | const activeElementIndex = elements.findIndex(
96 | (el) => el.id === activeId,
97 | );
98 | const overElementIndex = elements.findIndex((el) => el.id === overId);
99 |
100 | if (activeElementIndex === -1 || overElementIndex === -1)
101 | throw new Error("Element not found");
102 |
103 | const activeElement = { ...elements[activeElementIndex] };
104 | removeElement(activeId);
105 |
106 | let newIndex = overElementIndex;
107 | if (isDroppingOverDesignerElementBottomHalf) {
108 | newIndex = overElementIndex + 1;
109 | }
110 | addElement(newIndex, activeElement);
111 | }
112 | },
113 | });
114 |
115 | return (
116 |
117 |
{
120 | if (selectedElement) setSelectedElement(null);
121 | }}
122 | >
123 |
130 | {!droppable.isOver && elements.length === 0 && (
131 |
132 | Drop here
133 |
134 | )}
135 | {droppable.isOver && elements.length === 0 && (
136 |
139 | )}
140 | {elements.length > 0 && (
141 |
142 | {elements.map((element) => (
143 |
144 | ))}
145 |
146 | )}
147 |
148 |
149 |
150 |
151 | );
152 | }
153 |
154 | function DesignerElementWrapper({ element }: { element: FormElementInstance }) {
155 | const { removeElement, setSelectedElement } = useDesigner();
156 | const [mouseIsOver, setMouseIsOver] = useState(false);
157 |
158 | const topHalf = useDroppable({
159 | id: element.id + "-top",
160 | data: {
161 | type: element.type,
162 | elementId: element.id,
163 | isDesignerElementTopHalf: true,
164 | },
165 | });
166 |
167 | const bottomHalf = useDroppable({
168 | id: element.id + "-bottom",
169 | data: {
170 | type: element.type,
171 | elementId: element.id,
172 | isDesignerElementBottomHalf: true,
173 | },
174 | });
175 |
176 | const draggable = useDraggable({
177 | id: element.id + "-drag-handler",
178 | data: {
179 | type: element.type,
180 | elementId: element.id,
181 | isDesignerElement: true,
182 | },
183 | });
184 |
185 | const DesignerElement = FormElements[element.type].designerComponent;
186 |
187 | if (draggable.isDragging) return null;
188 |
189 | return (
190 | setMouseIsOver(true)}
196 | onMouseLeave={() => setMouseIsOver(false)}
197 | onClick={(e) => {
198 | e.stopPropagation();
199 | setSelectedElement(element);
200 | }}
201 | >
202 |
206 |
210 | {mouseIsOver && (
211 | <>
212 |
213 |
222 |
223 |
224 |
225 | Click for properties or drag to move
226 |
227 |
228 | >
229 | )}
230 | {topHalf.isOver && (
231 |
232 | )}
233 |
239 |
240 |
241 | {bottomHalf.isOver && (
242 |
243 | )}
244 |
245 | );
246 | }
247 |
248 | export default Designer;
249 |
--------------------------------------------------------------------------------
/components/DesignerSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useDesigner from "./hooks/useDesigner";
3 | import FormElementsSidebar from "./FormElementsSidebar";
4 | import PropertiesFormSidebar from "./PropertiesFormSidebar";
5 |
6 | function DesignerSidebar() {
7 | const { selectedElement } = useDesigner();
8 |
9 | return (
10 |
14 | );
15 | }
16 |
17 | export default DesignerSidebar;
18 |
--------------------------------------------------------------------------------
/components/DragOverlayWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { Active, DragOverlay, useDndMonitor } from "@dnd-kit/core";
2 | import React, { useState } from "react";
3 | import { SidebarBtnElementDragOverlay } from "./SidebarBtnElement";
4 | import { ElementsType, FormElements } from "./FormElements";
5 | import useDesigner from "./hooks/useDesigner";
6 |
7 | function DragOverlayWrapper() {
8 | const { elements } = useDesigner();
9 | const [draggedItem, setDraggedItem] = useState(null);
10 |
11 | useDndMonitor({
12 | onDragStart({ active }) {
13 | setDraggedItem(active);
14 | },
15 | onDragEnd() {
16 | setDraggedItem(null);
17 | },
18 | onDragCancel() {
19 | setDraggedItem(null);
20 | },
21 | });
22 |
23 | if (!draggedItem) return null;
24 |
25 | let node = No drag overlay
;
26 | const isSidebarBtnElement = draggedItem?.data?.current?.isDesignerBtnElement;
27 |
28 | if (isSidebarBtnElement) {
29 | const type = draggedItem?.data?.current?.type as ElementsType;
30 | node = ;
31 | }
32 |
33 | const isDesignerElement = draggedItem?.data?.current?.isDesignerElement;
34 |
35 | if (isDesignerElement) {
36 | const elementId = draggedItem.data?.current?.elementId;
37 | const element = elements.find((element) => element.id === elementId);
38 |
39 | if (!element) {
40 | node = Element not found!
;
41 | } else {
42 | const DesignerElementComponent =
43 | FormElements[element.type].designerComponent;
44 | node = (
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | return {node};
53 | }
54 |
55 | export default DragOverlayWrapper;
56 |
--------------------------------------------------------------------------------
/components/FormBuilder.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Form } from "@prisma/client";
4 | import Link from "next/link";
5 | import Confetti from "react-confetti";
6 | import {
7 | DndContext,
8 | MouseSensor,
9 | TouchSensor,
10 | useSensor,
11 | useSensors,
12 | } from "@dnd-kit/core";
13 | import PreviewDialogBtn from "./PreviewDialogBtn";
14 | import SaveFormBtn from "./SaveFormBtn";
15 | import PublishFormBtn from "./PublishFormBtn";
16 | import Designer from "./Designer";
17 | import DragOverlayWrapper from "./DragOverlayWrapper";
18 | import { useEffect, useState } from "react";
19 | import useDesigner from "./hooks/useDesigner";
20 | import { ArrowLeft, ArrowRight, Loader2 } from "lucide-react";
21 | import { Input } from "./ui/input";
22 | import { Button } from "./ui/button";
23 | import { toast } from "./ui/use-toast";
24 |
25 | interface Props {
26 | form: Form;
27 | }
28 |
29 | function FormBuilder({ form }: Props) {
30 | const { setElements, setSelectedElement } = useDesigner();
31 | const [isReady, setIsReady] = useState(false);
32 |
33 | const mouseSensor = useSensor(MouseSensor, {
34 | activationConstraint: {
35 | distance: 10, // 10px
36 | },
37 | });
38 |
39 | const touchSensor = useSensor(TouchSensor, {
40 | activationConstraint: {
41 | delay: 300,
42 | tolerance: 5,
43 | },
44 | });
45 |
46 | const sensors = useSensors(mouseSensor, touchSensor);
47 |
48 | const shareURL = `${window.location.origin}/submit/${form.shareURL}`;
49 |
50 | useEffect(() => {
51 | if (isReady) return;
52 |
53 | const elements = JSON.parse(form.content);
54 | setElements(elements);
55 | setSelectedElement(null);
56 | setIsReady(true);
57 | }, [form, setElements, isReady, setSelectedElement]);
58 |
59 | if (!isReady)
60 | return (
61 |
62 |
63 |
64 | );
65 |
66 | if (form.published) {
67 | return (
68 | <>
69 |
75 |
76 |
77 |
78 | 🎊🎊 Form Published
79 |
80 |
Share this form
81 |
82 | Anyone with the link can view and submit the form
83 |
84 |
85 |
86 |
98 |
99 |
100 |
105 |
110 |
111 |
112 |
113 | >
114 | );
115 | }
116 |
117 | return (
118 |
119 |
120 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | );
142 | }
143 |
144 | export default FormBuilder;
145 |
--------------------------------------------------------------------------------
/components/FormElements.tsx:
--------------------------------------------------------------------------------
1 | import { NumberFieldFormElement } from "./fields/NumberField";
2 | import { ParagraphFieldFormElement } from "./fields/ParagraphField";
3 | import { SeparatorFieldFormElement } from "./fields/SeparatorField";
4 | import { SpacerFieldFormElement } from "./fields/SpacerField";
5 | import { SubTitleFieldFormElement } from "./fields/SubTitleField";
6 | import { TextFieldFormElement } from "./fields/TextField";
7 | import { TextAreaFieldFormElement } from "./fields/TextAreaField";
8 | import { TitleFieldFormElement } from "./fields/TitleField";
9 | import { DateFieldFormElement } from "./fields/DateField";
10 | import { SelectFieldFormElement } from "./fields/SelectField";
11 | import { CheckboxFieldFormElement } from "./fields/CheckboxField";
12 |
13 | export type ElementsType =
14 | | "TextField"
15 | | "TitleField"
16 | | "SubTitleField"
17 | | "ParagraphField"
18 | | "SeparatorField"
19 | | "SpacerField"
20 | | "NumberField"
21 | | "TextAreaField"
22 | | "DateField"
23 | | "SelectField"
24 | | "CheckboxField";
25 |
26 | export type SubmitFunction = (key: string, value: string) => void;
27 |
28 | export type FormElement = {
29 | type: ElementsType;
30 | construct: (id: string) => FormElementInstance;
31 | designerBtnElement: {
32 | icon: React.ElementType;
33 | label: string;
34 | };
35 | designerComponent: React.FC<{ elementInstance: FormElementInstance }>;
36 | formComponent: React.FC<{
37 | elementInstance: FormElementInstance;
38 | submitValue?: (key: string, value: string) => void;
39 | isInvalid?: boolean;
40 | defaultValue?: string;
41 | }>;
42 | propertiesComponent: React.FC<{ elementInstance: FormElementInstance }>;
43 | validate: (formElement: FormElementInstance, currentValue: string) => boolean;
44 | };
45 |
46 | export type FormElementInstance = {
47 | id: string;
48 | type: ElementsType;
49 | extraAttributes?: Record;
50 | };
51 |
52 | type FormElementsType = {
53 | [key in ElementsType]: FormElement;
54 | };
55 |
56 | export const FormElements: FormElementsType = {
57 | TextField: TextFieldFormElement,
58 | TitleField: TitleFieldFormElement,
59 | SubTitleField: SubTitleFieldFormElement,
60 | ParagraphField: ParagraphFieldFormElement,
61 | SeparatorField: SeparatorFieldFormElement,
62 | SpacerField: SpacerFieldFormElement,
63 | NumberField: NumberFieldFormElement,
64 | TextAreaField: TextAreaFieldFormElement,
65 | DateField: DateFieldFormElement,
66 | SelectField: SelectFieldFormElement,
67 | CheckboxField: CheckboxFieldFormElement,
68 | };
69 |
--------------------------------------------------------------------------------
/components/FormElementsSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SidebarBtnElement from "./SidebarBtnElement";
3 | import { FormElements } from "./FormElements";
4 | import { Separator } from "./ui/separator";
5 |
6 | function FormElementsSidebar() {
7 | return (
8 |
9 |
Drag and drop elements
10 |
11 |
12 |
13 | Layout elements
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Form elements
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default FormElementsSidebar;
36 |
--------------------------------------------------------------------------------
/components/FormLinkShare.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { Button } from "./ui/button";
5 | import { Input } from "./ui/input";
6 | import { Share } from "lucide-react";
7 | import { toast } from "./ui/use-toast";
8 |
9 | function FormLinkShare({ shareURL }: { shareURL: string }) {
10 | const [mounted, isMounted] = useState(false);
11 |
12 | useEffect(() => {
13 | isMounted(true);
14 | }, []);
15 |
16 | if (!mounted) return null;
17 |
18 | const shareLink = `${window.location.origin}/submit/${shareURL}`;
19 |
20 | return (
21 |
22 |
23 |
35 |
36 | );
37 | }
38 |
39 | export default FormLinkShare;
40 |
--------------------------------------------------------------------------------
/components/FormSubmitComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Loader2, MousePointerClick } from "lucide-react";
4 | import { FormElementInstance, FormElements } from "./FormElements";
5 | import { Button } from "./ui/button";
6 | import { useCallback, useRef, useState, useTransition } from "react";
7 | import { toast } from "./ui/use-toast";
8 | import { SubmitForm } from "@/actions/form";
9 |
10 | interface Props {
11 | formContent: FormElementInstance[];
12 | formURL: string;
13 | }
14 |
15 | function FormSubmitComponent({ formContent, formURL }: Props) {
16 | const formValues = useRef<{ [key: string]: string }>({});
17 | const formErrors = useRef<{ [key: string]: boolean }>({});
18 | const [renderKey, setRenderKey] = useState(new Date().getTime());
19 | const [submitted, setSubmitted] = useState(false);
20 | const [pending, startTransition] = useTransition();
21 |
22 | const validateForm = useCallback(() => {
23 | for (const field of formContent) {
24 | const isValid = FormElements[field.type].validate(
25 | field,
26 | formValues.current[field.id] || "",
27 | );
28 | if (!isValid) {
29 | formErrors.current[field.id] = true;
30 | }
31 | }
32 | if (Object.keys(formErrors.current).length > 0) {
33 | return false;
34 | }
35 | return true;
36 | }, [formContent]);
37 |
38 | const submitValue = useCallback((key: string, value: string) => {
39 | formValues.current[key] = value;
40 | }, []);
41 |
42 | const submitForm = async () => {
43 | formErrors.current = {};
44 | const validForm = validateForm();
45 |
46 | if (!validForm) {
47 | setRenderKey(new Date().getTime());
48 | toast({
49 | title: "Error",
50 | description: "please check the form for errors",
51 | variant: "destructive",
52 | });
53 | return;
54 | }
55 |
56 | try {
57 | const JSONContent = JSON.stringify(formValues.current);
58 | await SubmitForm(formURL, JSONContent);
59 | setSubmitted(true);
60 | } catch (error) {
61 | toast({
62 | title: "Error",
63 | description: "Something went wrong",
64 | });
65 | }
66 | };
67 |
68 | if (submitted) {
69 | return (
70 |
71 |
72 |
Form submitted
73 |
74 | Thank you for submitting this form, you can close this page now.
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | return (
82 |
83 |
87 | {formContent.map((element) => {
88 | const FormElement = FormElements[element.type].formComponent;
89 | return (
90 |
97 | );
98 | })}
99 |
100 |
109 |
110 |
111 | );
112 | }
113 |
114 | export default FormSubmitComponent;
115 |
--------------------------------------------------------------------------------
/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | export default function Logo() {
4 | return (
5 |
9 | Formify
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/components/PreviewDialogBtn.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "./ui/button";
3 | import { Eye } from "lucide-react";
4 | import useDesigner from "./hooks/useDesigner";
5 | import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
6 | import { FormElements } from "./FormElements";
7 |
8 | function PreviewDialogBtn() {
9 | const { elements } = useDesigner();
10 |
11 | return (
12 |
40 | );
41 | }
42 |
43 | export default PreviewDialogBtn;
44 |
--------------------------------------------------------------------------------
/components/PropertiesFormSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useDesigner from "./hooks/useDesigner";
3 | import { FormElements } from "./FormElements";
4 | import { X } from "lucide-react";
5 | import { Button } from "./ui/button";
6 | import { Separator } from "./ui/separator";
7 |
8 | function PropertiesFormSidebar() {
9 | const { selectedElement, setSelectedElement } = useDesigner();
10 | if (!selectedElement) return null;
11 |
12 | const PropertiesForm =
13 | FormElements[selectedElement?.type].propertiesComponent;
14 |
15 | return (
16 |
17 |
18 |
Element properties
19 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default PropertiesFormSidebar;
36 |
--------------------------------------------------------------------------------
/components/PublishFormBtn.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowUpToLine, Loader2 } from "lucide-react";
2 | import { Button } from "./ui/button";
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogHeader,
10 | AlertDialogFooter,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from "./ui/alert-dialog";
14 | import { useTransition } from "react";
15 | import { PublishForm } from "@/actions/form";
16 | import { toast } from "./ui/use-toast";
17 | import { useRouter } from "next/navigation";
18 |
19 | interface Props {
20 | id: number;
21 | }
22 |
23 | function PublishFormBtn({ id }: Props) {
24 | const [loading, startTransition] = useTransition();
25 | const router = useRouter();
26 |
27 | async function publishForm() {
28 | try {
29 | await PublishForm(id);
30 | toast({
31 | title: "Success",
32 | description: "Your form is now available to the public.",
33 | });
34 | router.refresh();
35 | } catch (error) {
36 | toast({
37 | title: "Error",
38 | description: "Something went wrong. Please try again.",
39 | });
40 | }
41 | }
42 |
43 | return (
44 |
45 |
46 |
50 |
51 |
52 |
53 | Are you absolutely sure?
54 |
55 | This action cannot be undone. After publishing you will not be able
56 | to edit this form.
57 |
58 |
59 | By publishing this form you will make it available to the public
60 | and you will be able to collect submissions.
61 |
62 |
63 |
64 |
65 | Cancel
66 | {
69 | e.preventDefault();
70 | startTransition(publishForm);
71 | }}
72 | >
73 | Proceed {loading && }
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | export default PublishFormBtn;
82 |
--------------------------------------------------------------------------------
/components/SaveFormBtn.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2, SaveAll } from "lucide-react";
2 | import { Button } from "./ui/button";
3 | import { toast } from "./ui/use-toast";
4 | import { UpdateFormContent } from "@/actions/form";
5 | import { useTransition } from "react";
6 | import useDesigner from "./hooks/useDesigner";
7 |
8 | function SaveFormBtn({ id }: { id: number }) {
9 | const { elements } = useDesigner();
10 | const [loading, startTransition] = useTransition();
11 |
12 | const updateFormContent = async () => {
13 | try {
14 | const jsonElements = JSON.stringify(elements);
15 | await UpdateFormContent(id, jsonElements);
16 | toast({
17 | title: "Success",
18 | description: "Your form has been saved!",
19 | });
20 | } catch (error) {
21 | toast({
22 | title: "Error",
23 | description: "Something went wrong!",
24 | variant: "destructive",
25 | });
26 | }
27 | };
28 |
29 | return (
30 |
42 | );
43 | }
44 |
45 | export default SaveFormBtn;
46 |
--------------------------------------------------------------------------------
/components/SidebarBtnElement.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDraggable } from "@dnd-kit/core";
3 | import { FormElement } from "./FormElements";
4 | import { Button } from "./ui/button";
5 | import { cn } from "@/lib/utils";
6 |
7 | export function SidebarBtnElementDragOverlay({
8 | formElement,
9 | }: {
10 | formElement: FormElement;
11 | }) {
12 | const { label, icon: Icon } = formElement.designerBtnElement;
13 |
14 | return (
15 |
22 | );
23 | }
24 |
25 | function SidebarBtnElement({ formElement }: { formElement: FormElement }) {
26 | const { label, icon: Icon } = formElement.designerBtnElement;
27 | const draggable = useDraggable({
28 | id: `designer-btn-${formElement.type}`,
29 | data: {
30 | type: formElement.type,
31 | isDesignerBtnElement: true,
32 | },
33 | });
34 | return (
35 |
48 | );
49 | }
50 |
51 | export default SidebarBtnElement;
52 |
--------------------------------------------------------------------------------
/components/ThemeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useState } from 'react';
4 | import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
5 | import { MoonIcon, SunIcon, Monitor } from 'lucide-react';
6 | import { useTheme } from 'next-themes';
7 |
8 | export default function ThemeSwitcher() {
9 | const { theme, setTheme } = useTheme();
10 | const [mounted, setMounted] = useState(false);
11 |
12 | useEffect(() => {
13 | setMounted(true);
14 | }, []);
15 |
16 | if (!mounted) return null;
17 |
18 | return (
19 |
20 |
21 | setTheme('light')}>
22 |
23 |
24 | setTheme('dark')}>
25 |
26 |
27 | setTheme('light')}>
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/VisitBtn.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { Button } from "./ui/button";
5 |
6 | function VisitBtn({ shareURL }: { shareURL: string }) {
7 | const [mounted, isMounted] = useState(false);
8 |
9 | useEffect(() => {
10 | isMounted(true);
11 | }, []);
12 |
13 | if (!mounted) return null;
14 |
15 | const shareLink = `${window.location.origin}/submit/${shareURL}`;
16 |
17 | return (
18 |
24 | );
25 | }
26 |
27 | export default VisitBtn;
28 |
--------------------------------------------------------------------------------
/components/context/DesignerContext.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | createContext,
5 | ReactNode,
6 | useState,
7 | Dispatch,
8 | SetStateAction,
9 | } from "react";
10 | import { FormElementInstance } from "../FormElements";
11 |
12 | type DesignerContextType = {
13 | elements: FormElementInstance[];
14 | setElements: Dispatch>;
15 | selectedElement: FormElementInstance | null;
16 | setSelectedElement: Dispatch>;
17 | addElement: (idx: number, element: FormElementInstance) => void;
18 | removeElement: (idx: string) => void;
19 | updateElement: (idx: string, element: FormElementInstance) => void;
20 | };
21 |
22 | export const DesignerContext = createContext(null);
23 |
24 | export default function DesignerContextProvider({
25 | children,
26 | }: {
27 | children: ReactNode;
28 | }) {
29 | const [elements, setElements] = useState([]);
30 | const [selectedElement, setSelectedElement] =
31 | useState(null);
32 |
33 | const addElement = (idx: number, element: FormElementInstance) =>
34 | setElements((prev) => {
35 | const newElements = [...prev];
36 | newElements.splice(idx, 0, element);
37 | return newElements;
38 | });
39 |
40 | const removeElement = (idx: string) => {
41 | setElements((prev) => prev.filter((element) => element.id !== idx));
42 | };
43 |
44 | const updateElement = (idx: string, element: FormElementInstance) => {
45 | setElements((prev) => {
46 | const newElements = [...prev];
47 | const idxToUpdate = newElements.findIndex((el) => el.id === idx);
48 | newElements.splice(idxToUpdate, 1, element);
49 | return newElements;
50 | });
51 | };
52 |
53 | return (
54 |
65 | {children}
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/components/fields/CheckboxField.tsx:
--------------------------------------------------------------------------------
1 | import { CheckSquare } from "lucide-react";
2 | import * as z from "zod";
3 | import {
4 | ElementsType,
5 | FormElement,
6 | FormElementInstance,
7 | SubmitFunction,
8 | } from "../FormElements";
9 | import { Label } from "../ui/label";
10 | import { Input } from "../ui/input";
11 | import { useForm } from "react-hook-form";
12 | import { zodResolver } from "@hookform/resolvers/zod";
13 | import { useEffect, useState } from "react";
14 | import useDesigner from "../hooks/useDesigner";
15 |
16 | import {
17 | Form,
18 | FormControl,
19 | FormDescription,
20 | FormField,
21 | FormItem,
22 | FormLabel,
23 | FormMessage,
24 | } from "../ui/form";
25 | import { Switch } from "../ui/switch";
26 | import { cn } from "@/lib/utils";
27 | import { Checkbox } from "../ui/checkbox";
28 |
29 | const type: ElementsType = "CheckboxField";
30 | const extraAttributes = {
31 | label: "Checkbox field",
32 | helperText: "Helper text",
33 | required: false,
34 | };
35 |
36 | const propertiesSchema = z.object({
37 | label: z.string().max(200),
38 | helperText: z.string().max(50),
39 | required: z.boolean().default(false),
40 | });
41 |
42 | export const CheckboxFieldFormElement: FormElement = {
43 | type,
44 | construct: (id: string) => ({
45 | id,
46 | type,
47 | extraAttributes,
48 | }),
49 | designerBtnElement: {
50 | icon: CheckSquare,
51 | label: "Checkbox field",
52 | },
53 | designerComponent: DesignerComponent,
54 | formComponent: FormComponent,
55 | propertiesComponent: PropertiesComponent,
56 | validate: (formElement: FormElementInstance, currentValue: string) => {
57 | const element = formElement as CustomInstance;
58 | if (element.extraAttributes.required) {
59 | return currentValue === "true";
60 | }
61 | return true;
62 | },
63 | };
64 |
65 | type CustomInstance = FormElementInstance & {
66 | extraAttributes: typeof extraAttributes;
67 | };
68 |
69 | type PropertiesFormSchemaType = z.infer;
70 |
71 | function DesignerComponent({
72 | elementInstance,
73 | }: {
74 | elementInstance: FormElementInstance;
75 | }) {
76 | const element = elementInstance as CustomInstance;
77 | const { label, required, helperText } = element.extraAttributes;
78 | const id = `checkbox-${element.id}`;
79 |
80 | return (
81 |
82 |
83 |
84 |
88 | {helperText && (
89 |
{helperText}
90 | )}
91 |
92 |
93 | );
94 | }
95 |
96 | function FormComponent({
97 | elementInstance,
98 | submitValue,
99 | isInvalid,
100 | defaultValue,
101 | }: {
102 | elementInstance: FormElementInstance;
103 | submitValue?: SubmitFunction;
104 | isInvalid?: boolean;
105 | defaultValue?: string;
106 | }) {
107 | const element = elementInstance as CustomInstance;
108 | const [value, setValue] = useState(defaultValue === "true");
109 | const [error, setError] = useState(false);
110 |
111 | useEffect(() => {
112 | setError(isInvalid === true);
113 | }, [isInvalid]);
114 |
115 | const { label, required, helperText } = element.extraAttributes;
116 | const id = `checkbox-${element.id}`;
117 |
118 | return (
119 |
120 |
{
125 | setValue(value === true);
126 | if (!submitValue) return;
127 | const valid = CheckboxFieldFormElement.validate(
128 | element,
129 | value ? "true" : "false",
130 | );
131 | setError(!valid);
132 | submitValue?.(element.id, value.toString());
133 | }}
134 | />
135 |
136 |
140 | {helperText && (
141 |
147 | {helperText}
148 |
149 | )}
150 |
151 |
152 | );
153 | }
154 |
155 | function PropertiesComponent({
156 | elementInstance,
157 | }: {
158 | elementInstance: FormElementInstance;
159 | }) {
160 | const { updateElement } = useDesigner();
161 | const element = elementInstance as CustomInstance;
162 | const form = useForm({
163 | resolver: zodResolver(propertiesSchema),
164 | mode: "onBlur",
165 | defaultValues: {
166 | label: element.extraAttributes.label,
167 | helperText: element.extraAttributes.helperText,
168 | required: element.extraAttributes.required,
169 | },
170 | });
171 |
172 | useEffect(() => {
173 | form.reset(element.extraAttributes);
174 | }, [element, form]);
175 |
176 | function applyChanges(values: PropertiesFormSchemaType) {
177 | const { label, helperText, required } = values;
178 | updateElement(element.id, {
179 | ...element,
180 | extraAttributes: {
181 | label: label,
182 | helperText: helperText,
183 | required: required,
184 | },
185 | });
186 | }
187 |
188 | return (
189 |
265 |
266 | );
267 | }
268 |
--------------------------------------------------------------------------------
/components/fields/ParagraphField.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from "lucide-react";
2 | import * as z from "zod";
3 | import {
4 | ElementsType,
5 | FormElement,
6 | FormElementInstance,
7 | } from "../FormElements";
8 | import { Label } from "../ui/label";
9 | import { useForm } from "react-hook-form";
10 | import { zodResolver } from "@hookform/resolvers/zod";
11 | import { useEffect } from "react";
12 | import useDesigner from "../hooks/useDesigner";
13 |
14 | import {
15 | Form,
16 | FormControl,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from "../ui/form";
22 | import { Textarea } from "../ui/textarea";
23 |
24 | const type: ElementsType = "ParagraphField";
25 | const extraAttributes = {
26 | text: "Paragraph field",
27 | };
28 |
29 | const propertiesSchema = z.object({
30 | text: z.string().min(2).max(500),
31 | });
32 |
33 | export const ParagraphFieldFormElement: FormElement = {
34 | type,
35 | construct: (id: string) => ({
36 | id,
37 | type,
38 | extraAttributes,
39 | }),
40 | designerBtnElement: {
41 | icon: Text,
42 | label: "Paragraph field",
43 | },
44 | designerComponent: DesignerComponent,
45 | formComponent: FormComponent,
46 | propertiesComponent: PropertiesComponent,
47 | validate: () => true,
48 | };
49 |
50 | type CustomInstance = FormElementInstance & {
51 | extraAttributes: typeof extraAttributes;
52 | };
53 |
54 | type PropertiesFormSchemaType = z.infer;
55 |
56 | function DesignerComponent({
57 | elementInstance,
58 | }: {
59 | elementInstance: FormElementInstance;
60 | }) {
61 | const element = elementInstance as CustomInstance;
62 | const { text } = element.extraAttributes;
63 |
64 | return (
65 |
66 |
67 |
{text}
68 |
69 | );
70 | }
71 |
72 | function FormComponent({
73 | elementInstance,
74 | }: {
75 | elementInstance: FormElementInstance;
76 | }) {
77 | const element = elementInstance as CustomInstance;
78 | const { text } = element.extraAttributes;
79 |
80 | return {text}
;
81 | }
82 |
83 | function PropertiesComponent({
84 | elementInstance,
85 | }: {
86 | elementInstance: FormElementInstance;
87 | }) {
88 | const { updateElement } = useDesigner();
89 | const element = elementInstance as CustomInstance;
90 | const form = useForm({
91 | resolver: zodResolver(propertiesSchema),
92 | mode: "onBlur",
93 | defaultValues: {
94 | text: element.extraAttributes.title,
95 | },
96 | });
97 |
98 | useEffect(() => {
99 | form.reset(element.extraAttributes);
100 | }, [element, form]);
101 |
102 | function applyChanges(values: PropertiesFormSchemaType) {
103 | const { text } = values;
104 | updateElement(element.id, {
105 | ...element,
106 | extraAttributes: {
107 | text,
108 | },
109 | });
110 | }
111 |
112 | return (
113 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/components/fields/SeparatorField.tsx:
--------------------------------------------------------------------------------
1 | import { SeparatorVertical } from "lucide-react";
2 | import {
3 | ElementsType,
4 | FormElement,
5 | FormElementInstance,
6 | } from "../FormElements";
7 | import { Label } from "../ui/label";
8 |
9 | import { Separator } from "../ui/separator";
10 |
11 | const type: ElementsType = "SeparatorField";
12 |
13 | export const SeparatorFieldFormElement: FormElement = {
14 | type,
15 | construct: (id: string) => ({
16 | id,
17 | type,
18 | }),
19 | designerBtnElement: {
20 | icon: SeparatorVertical,
21 | label: "Separator field",
22 | },
23 | designerComponent: DesignerComponent,
24 | formComponent: FormComponent,
25 | propertiesComponent: PropertiesComponent,
26 | validate: () => true,
27 | };
28 |
29 | function DesignerComponent({
30 | elementInstance,
31 | }: {
32 | elementInstance: FormElementInstance;
33 | }) {
34 | return (
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | function FormComponent({
43 | elementInstance,
44 | }: {
45 | elementInstance: FormElementInstance;
46 | }) {
47 | return ;
48 | }
49 |
50 | function PropertiesComponent({
51 | elementInstance,
52 | }: {
53 | elementInstance: FormElementInstance;
54 | }) {
55 | return No properties for this element
;
56 | }
57 |
--------------------------------------------------------------------------------
/components/fields/SpacerField.tsx:
--------------------------------------------------------------------------------
1 | import { SeparatorHorizontal } from "lucide-react";
2 | import * as z from "zod";
3 | import {
4 | ElementsType,
5 | FormElement,
6 | FormElementInstance,
7 | } from "../FormElements";
8 | import { Label } from "../ui/label";
9 | import { useForm } from "react-hook-form";
10 | import { zodResolver } from "@hookform/resolvers/zod";
11 | import { useEffect } from "react";
12 | import useDesigner from "../hooks/useDesigner";
13 |
14 | import {
15 | Form,
16 | FormControl,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from "../ui/form";
22 | import { Slider } from "../ui/slider";
23 |
24 | const type: ElementsType = "SpacerField";
25 | const extraAttributes = {
26 | height: 20, // 20px
27 | };
28 |
29 | const propertiesSchema = z.object({
30 | height: z.number().min(5).max(200),
31 | });
32 |
33 | export const SpacerFieldFormElement: FormElement = {
34 | type,
35 | construct: (id: string) => ({
36 | id,
37 | type,
38 | extraAttributes,
39 | }),
40 | designerBtnElement: {
41 | icon: SeparatorHorizontal,
42 | label: "Spacer field",
43 | },
44 | designerComponent: DesignerComponent,
45 | formComponent: FormComponent,
46 | propertiesComponent: PropertiesComponent,
47 | validate: () => true,
48 | };
49 |
50 | type CustomInstance = FormElementInstance & {
51 | extraAttributes: typeof extraAttributes;
52 | };
53 |
54 | type PropertiesFormSchemaType = z.infer;
55 |
56 | function DesignerComponent({
57 | elementInstance,
58 | }: {
59 | elementInstance: FormElementInstance;
60 | }) {
61 | const element = elementInstance as CustomInstance;
62 | const { height } = element.extraAttributes;
63 |
64 | return (
65 |
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | function FormComponent({
73 | elementInstance,
74 | }: {
75 | elementInstance: FormElementInstance;
76 | }) {
77 | const element = elementInstance as CustomInstance;
78 | const { height } = element.extraAttributes;
79 |
80 | return ;
81 | }
82 |
83 | function PropertiesComponent({
84 | elementInstance,
85 | }: {
86 | elementInstance: FormElementInstance;
87 | }) {
88 | const { updateElement } = useDesigner();
89 | const element = elementInstance as CustomInstance;
90 | const form = useForm({
91 | resolver: zodResolver(propertiesSchema),
92 | mode: "onBlur",
93 | defaultValues: {
94 | height: element.extraAttributes.height,
95 | },
96 | });
97 |
98 | useEffect(() => {
99 | form.reset(element.extraAttributes);
100 | }, [element, form]);
101 |
102 | function applyChanges(values: PropertiesFormSchemaType) {
103 | const { height } = values;
104 | updateElement(element.id, {
105 | ...element,
106 | extraAttributes: {
107 | height,
108 | },
109 | });
110 | }
111 |
112 | return (
113 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/components/fields/SubTitleField.tsx:
--------------------------------------------------------------------------------
1 | import { Heading2 } from "lucide-react";
2 | import * as z from "zod";
3 | import {
4 | ElementsType,
5 | FormElement,
6 | FormElementInstance,
7 | } from "../FormElements";
8 | import { Label } from "../ui/label";
9 | import { Input } from "../ui/input";
10 | import { useForm } from "react-hook-form";
11 | import { zodResolver } from "@hookform/resolvers/zod";
12 | import { useEffect } from "react";
13 | import useDesigner from "../hooks/useDesigner";
14 |
15 | import {
16 | Form,
17 | FormControl,
18 | FormField,
19 | FormItem,
20 | FormLabel,
21 | FormMessage,
22 | } from "../ui/form";
23 |
24 | const type: ElementsType = "SubTitleField";
25 | const extraAttributes = {
26 | title: "SubTitle field",
27 | };
28 |
29 | const propertiesSchema = z.object({
30 | title: z.string().min(2).max(100),
31 | });
32 |
33 | export const SubTitleFieldFormElement: FormElement = {
34 | type,
35 | construct: (id: string) => ({
36 | id,
37 | type,
38 | extraAttributes,
39 | }),
40 | designerBtnElement: {
41 | icon: Heading2,
42 | label: "SubTitle field",
43 | },
44 | designerComponent: DesignerComponent,
45 | formComponent: FormComponent,
46 | propertiesComponent: PropertiesComponent,
47 | validate: () => true,
48 | };
49 |
50 | type CustomInstance = FormElementInstance & {
51 | extraAttributes: typeof extraAttributes;
52 | };
53 |
54 | type PropertiesFormSchemaType = z.infer;
55 |
56 | function DesignerComponent({
57 | elementInstance,
58 | }: {
59 | elementInstance: FormElementInstance;
60 | }) {
61 | const element = elementInstance as CustomInstance;
62 | const { title } = element.extraAttributes;
63 |
64 | return (
65 |
66 |
67 |
{title}
68 |
69 | );
70 | }
71 |
72 | function FormComponent({
73 | elementInstance,
74 | }: {
75 | elementInstance: FormElementInstance;
76 | }) {
77 | const element = elementInstance as CustomInstance;
78 | const { title } = element.extraAttributes;
79 |
80 | return {title}
;
81 | }
82 |
83 | function PropertiesComponent({
84 | elementInstance,
85 | }: {
86 | elementInstance: FormElementInstance;
87 | }) {
88 | const { updateElement } = useDesigner();
89 | const element = elementInstance as CustomInstance;
90 | const form = useForm({
91 | resolver: zodResolver(propertiesSchema),
92 | mode: "onBlur",
93 | defaultValues: {
94 | title: element.extraAttributes.title,
95 | },
96 | });
97 |
98 | useEffect(() => {
99 | form.reset(element.extraAttributes);
100 | }, [element, form]);
101 |
102 | function applyChanges(values: PropertiesFormSchemaType) {
103 | const { title } = values;
104 | updateElement(element.id, {
105 | ...element,
106 | extraAttributes: {
107 | title,
108 | },
109 | });
110 | }
111 |
112 | return (
113 |
139 |
140 | );
141 | }
142 |
--------------------------------------------------------------------------------
/components/fields/TitleField.tsx:
--------------------------------------------------------------------------------
1 | import { Heading1 } from "lucide-react";
2 | import * as z from "zod";
3 | import {
4 | ElementsType,
5 | FormElement,
6 | FormElementInstance,
7 | } from "../FormElements";
8 | import { Label } from "../ui/label";
9 | import { Input } from "../ui/input";
10 | import { useForm } from "react-hook-form";
11 | import { zodResolver } from "@hookform/resolvers/zod";
12 | import { useEffect } from "react";
13 | import useDesigner from "../hooks/useDesigner";
14 |
15 | import {
16 | Form,
17 | FormControl,
18 | FormField,
19 | FormItem,
20 | FormLabel,
21 | FormMessage,
22 | } from "../ui/form";
23 |
24 | const type: ElementsType = "TitleField";
25 | const extraAttributes = {
26 | title: "Title field",
27 | };
28 |
29 | const propertiesSchema = z.object({
30 | title: z.string().min(2).max(100),
31 | });
32 |
33 | export const TitleFieldFormElement: FormElement = {
34 | type,
35 | construct: (id: string) => ({
36 | id,
37 | type,
38 | extraAttributes,
39 | }),
40 | designerBtnElement: {
41 | icon: Heading1,
42 | label: "Title field",
43 | },
44 | designerComponent: DesignerComponent,
45 | formComponent: FormComponent,
46 | propertiesComponent: PropertiesComponent,
47 | validate: () => true,
48 | };
49 |
50 | type CustomInstance = FormElementInstance & {
51 | extraAttributes: typeof extraAttributes;
52 | };
53 |
54 | type PropertiesFormSchemaType = z.infer;
55 |
56 | function DesignerComponent({
57 | elementInstance,
58 | }: {
59 | elementInstance: FormElementInstance;
60 | }) {
61 | const element = elementInstance as CustomInstance;
62 | const { title } = element.extraAttributes;
63 |
64 | return (
65 |
66 |
67 |
{title}
68 |
69 | );
70 | }
71 |
72 | function FormComponent({
73 | elementInstance,
74 | }: {
75 | elementInstance: FormElementInstance;
76 | }) {
77 | const element = elementInstance as CustomInstance;
78 | const { title } = element.extraAttributes;
79 |
80 | return {title}
;
81 | }
82 |
83 | function PropertiesComponent({
84 | elementInstance,
85 | }: {
86 | elementInstance: FormElementInstance;
87 | }) {
88 | const { updateElement } = useDesigner();
89 | const element = elementInstance as CustomInstance;
90 | const form = useForm({
91 | resolver: zodResolver(propertiesSchema),
92 | mode: "onBlur",
93 | defaultValues: {
94 | title: element.extraAttributes.title,
95 | },
96 | });
97 |
98 | useEffect(() => {
99 | form.reset(element.extraAttributes);
100 | }, [element, form]);
101 |
102 | function applyChanges(values: PropertiesFormSchemaType) {
103 | const { title } = values;
104 | updateElement(element.id, {
105 | ...element,
106 | extraAttributes: {
107 | title,
108 | },
109 | });
110 | }
111 |
112 | return (
113 |
139 |
140 | );
141 | }
142 |
--------------------------------------------------------------------------------
/components/hooks/useDesigner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useContext } from "react";
4 | import { DesignerContext } from "../context/DesignerContext";
5 |
6 | function useDesigner() {
7 | const context = useContext(DesignerContext);
8 |
9 | if (!context) {
10 | throw new Error(
11 | "useDesigner must be used within a DesignerContextProvider",
12 | );
13 | }
14 |
15 | return context;
16 | }
17 |
18 | export default useDesigner;
19 |
--------------------------------------------------------------------------------
/components/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
5 | import { type ThemeProviderProps } from 'next-themes/dist/types';
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ))
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
61 |
--------------------------------------------------------------------------------
/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, children, ...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 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
56 | IconRight: ({ ...props }) => ,
57 | }}
58 | {...props}
59 | />
60 | )
61 | }
62 | Calendar.displayName = "Calendar"
63 |
64 | export { Calendar }
65 |
--------------------------------------------------------------------------------
/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 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/components/ui/context-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const ContextMenu = ContextMenuPrimitive.Root
10 |
11 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger
12 |
13 | const ContextMenuGroup = ContextMenuPrimitive.Group
14 |
15 | const ContextMenuPortal = ContextMenuPrimitive.Portal
16 |
17 | const ContextMenuSub = ContextMenuPrimitive.Sub
18 |
19 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
20 |
21 | const ContextMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
41 |
42 | const ContextMenuSubContent = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef
45 | >(({ className, ...props }, ref) => (
46 |
54 | ))
55 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
56 |
57 | const ContextMenuContent = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
62 |
70 |
71 | ))
72 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
73 |
74 | const ContextMenuItem = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef & {
77 | inset?: boolean
78 | }
79 | >(({ className, inset, ...props }, ref) => (
80 |
89 | ))
90 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
91 |
92 | const ContextMenuCheckboxItem = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, children, checked, ...props }, ref) => (
96 |
105 |
106 |
107 |
108 |
109 |
110 | {children}
111 |
112 | ))
113 | ContextMenuCheckboxItem.displayName =
114 | ContextMenuPrimitive.CheckboxItem.displayName
115 |
116 | const ContextMenuRadioItem = React.forwardRef<
117 | React.ElementRef,
118 | React.ComponentPropsWithoutRef
119 | >(({ className, children, ...props }, ref) => (
120 |
128 |
129 |
130 |
131 |
132 |
133 | {children}
134 |
135 | ))
136 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
137 |
138 | const ContextMenuLabel = React.forwardRef<
139 | React.ElementRef,
140 | React.ComponentPropsWithoutRef & {
141 | inset?: boolean
142 | }
143 | >(({ className, inset, ...props }, ref) => (
144 |
153 | ))
154 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
155 |
156 | const ContextMenuSeparator = React.forwardRef<
157 | React.ElementRef,
158 | React.ComponentPropsWithoutRef
159 | >(({ className, ...props }, ref) => (
160 |
165 | ))
166 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
167 |
168 | const ContextMenuShortcut = ({
169 | className,
170 | ...props
171 | }: React.HTMLAttributes) => {
172 | return (
173 |
180 | )
181 | }
182 | ContextMenuShortcut.displayName = "ContextMenuShortcut"
183 |
184 | export {
185 | ContextMenu,
186 | ContextMenuTrigger,
187 | ContextMenuContent,
188 | ContextMenuItem,
189 | ContextMenuCheckboxItem,
190 | ContextMenuRadioItem,
191 | ContextMenuLabel,
192 | ContextMenuSeparator,
193 | ContextMenuShortcut,
194 | ContextMenuGroup,
195 | ContextMenuPortal,
196 | ContextMenuSub,
197 | ContextMenuSubContent,
198 | ContextMenuSubTrigger,
199 | ContextMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/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 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
49 |
50 | ))
51 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
52 |
53 | export { ScrollArea, ScrollBar }
54 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ))
63 | SelectContent.displayName = SelectPrimitive.Content.displayName
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ))
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ))
98 | SelectItem.displayName = SelectPrimitive.Item.displayName
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator,
121 | }
122 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 |
48 | ))
49 | TableFooter.displayName = "TableFooter"
50 |
51 | const TableRow = React.forwardRef<
52 | HTMLTableRowElement,
53 | React.HTMLAttributes
54 | >(({ className, ...props }, ref) => (
55 |
63 | ))
64 | TableRow.displayName = "TableRow"
65 |
66 | const TableHead = React.forwardRef<
67 | HTMLTableCellElement,
68 | React.ThHTMLAttributes
69 | >(({ className, ...props }, ref) => (
70 | |
78 | ))
79 | TableHead.displayName = "TableHead"
80 |
81 | const TableCell = React.forwardRef<
82 | HTMLTableCellElement,
83 | React.TdHTMLAttributes
84 | >(({ className, ...props }, ref) => (
85 | |
90 | ))
91 | TableCell.displayName = "TableCell"
92 |
93 | const TableCaption = React.forwardRef<
94 | HTMLTableCaptionElement,
95 | React.HTMLAttributes
96 | >(({ className, ...props }, ref) => (
97 |
102 | ))
103 | TableCaption.displayName = "TableCaption"
104 |
105 | export {
106 | Table,
107 | TableHeader,
108 | TableBody,
109 | TableFooter,
110 | TableHead,
111 | TableRow,
112 | TableCell,
113 | TableCaption,
114 | }
115 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_VALUE
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/lib/idGenerator.ts:
--------------------------------------------------------------------------------
1 | export function idGenerator(): string {
2 | return Math.floor(Math.random() * 10001).toString();
3 | }
4 |
--------------------------------------------------------------------------------
/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | type PrismaClientSingleton = ReturnType;
8 |
9 | const globalForPrisma = globalThis as unknown as {
10 | prisma: PrismaClientSingleton | undefined;
11 | };
12 |
13 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
14 |
15 | export default prisma;
16 |
17 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
18 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from '@clerk/nextjs';
2 |
3 | // This example protects all routes including api/trpc routes
4 | // Please edit this to allow other routes to be public as needed.
5 | // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
6 | export default authMiddleware({});
7 |
8 | export const config = {
9 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
10 | };
11 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | module.exports = nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-dnd-formbuilder",
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 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^4.29.3",
14 | "@dnd-kit/core": "^6.0.8",
15 | "@hookform/resolvers": "^3.3.2",
16 | "@prisma/client": "^5.5.2",
17 | "@radix-ui/react-accordion": "^1.1.2",
18 | "@radix-ui/react-alert-dialog": "^1.0.5",
19 | "@radix-ui/react-aspect-ratio": "^1.0.3",
20 | "@radix-ui/react-avatar": "^1.0.4",
21 | "@radix-ui/react-checkbox": "^1.0.4",
22 | "@radix-ui/react-collapsible": "^1.0.3",
23 | "@radix-ui/react-context-menu": "^2.1.5",
24 | "@radix-ui/react-dialog": "^1.0.5",
25 | "@radix-ui/react-dropdown-menu": "^2.0.6",
26 | "@radix-ui/react-hover-card": "^1.0.7",
27 | "@radix-ui/react-label": "^2.0.2",
28 | "@radix-ui/react-menubar": "^1.0.4",
29 | "@radix-ui/react-navigation-menu": "^1.1.4",
30 | "@radix-ui/react-popover": "^1.0.7",
31 | "@radix-ui/react-progress": "^1.0.3",
32 | "@radix-ui/react-radio-group": "^1.1.3",
33 | "@radix-ui/react-scroll-area": "^1.0.5",
34 | "@radix-ui/react-select": "^2.0.0",
35 | "@radix-ui/react-separator": "^1.0.3",
36 | "@radix-ui/react-slider": "^1.1.2",
37 | "@radix-ui/react-slot": "^1.0.2",
38 | "@radix-ui/react-switch": "^1.0.3",
39 | "@radix-ui/react-tabs": "^1.0.4",
40 | "@radix-ui/react-toast": "^1.1.5",
41 | "@radix-ui/react-toggle": "^1.0.3",
42 | "@radix-ui/react-tooltip": "^1.0.7",
43 | "class-variance-authority": "^0.7.0",
44 | "clsx": "^2.0.0",
45 | "cmdk": "^0.2.0",
46 | "date-fns": "^2.30.0",
47 | "lucide-react": "^0.288.0",
48 | "next": "^14.1.1",
49 | "next-themes": "^0.2.1",
50 | "nextjs-toploader": "^1.5.3",
51 | "react": "^18",
52 | "react-confetti": "^6.1.0",
53 | "react-day-picker": "^8.9.1",
54 | "react-dom": "^18",
55 | "react-hook-form": "^7.47.0",
56 | "tailwind-merge": "^1.14.0",
57 | "tailwindcss-animate": "^1.0.7",
58 | "zod": "^3.22.4"
59 | },
60 | "devDependencies": {
61 | "@types/node": "^20",
62 | "@types/react": "^18",
63 | "@types/react-dom": "^18",
64 | "autoprefixer": "^10",
65 | "eslint": "^8",
66 | "eslint-config-next": "13.5.6",
67 | "postcss": "^8",
68 | "prettier": "^3.0.3",
69 | "prettier-plugin-tailwindcss": "^0.5.6",
70 | "prisma": "^5.5.2",
71 | "tailwindcss": "^3",
72 | "typescript": "^5"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['prettier-plugin-tailwindcss'],
3 | };
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20231027154929_initial/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Form" (
3 | "id" SERIAL NOT NULL,
4 | "userId" TEXT NOT NULL,
5 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6 | "updatedAt" TIMESTAMP(3) NOT NULL,
7 | "published" BOOLEAN NOT NULL DEFAULT false,
8 | "name" TEXT NOT NULL,
9 | "description" TEXT NOT NULL DEFAULT '',
10 | "content" TEXT NOT NULL DEFAULT '[]',
11 | "visits" INTEGER NOT NULL DEFAULT 0,
12 | "submissions" INTEGER NOT NULL DEFAULT 0,
13 | "shareURL" TEXT NOT NULL,
14 |
15 | CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
16 | );
17 |
18 | -- CreateTable
19 | CREATE TABLE "FormSubmissions" (
20 | "id" SERIAL NOT NULL,
21 | "formId" INTEGER NOT NULL,
22 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
23 | "updatedAt" TIMESTAMP(3) NOT NULL,
24 | "content" TEXT NOT NULL,
25 |
26 | CONSTRAINT "FormSubmissions_pkey" PRIMARY KEY ("id")
27 | );
28 |
29 | -- CreateIndex
30 | CREATE UNIQUE INDEX "Form_name_userId_key" ON "Form"("name", "userId");
31 |
32 | -- AddForeignKey
33 | ALTER TABLE "FormSubmissions" ADD CONSTRAINT "FormSubmissions_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
34 |
--------------------------------------------------------------------------------
/prisma/migrations/20231029052228_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[shareURL]` on the table `Form` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- CreateIndex
8 | CREATE UNIQUE INDEX "Form_shareURL_key" ON "Form"("shareURL");
9 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling
11 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
12 | }
13 |
14 | model Form {
15 | id Int @id @default(autoincrement())
16 | userId String
17 |
18 | createdAt DateTime @default(now())
19 | updatedAt DateTime @updatedAt
20 | published Boolean @default(false)
21 | name String
22 | description String @default("")
23 | content String @default("[]")
24 |
25 | visits Int @default(0)
26 | submissions Int @default(0)
27 |
28 | shareURL String @unique @default(uuid())
29 | FormSubmissions FormSubmissions[]
30 |
31 | @@unique([name, userId])
32 | }
33 |
34 | model FormSubmissions {
35 | id Int @id @default(autoincrement())
36 | formId Int
37 | createdAt DateTime @default(now())
38 | updatedAt DateTime @updatedAt
39 | Form Form @relation(fields: [formId], references: [id])
40 | content String
41 | }
42 |
--------------------------------------------------------------------------------
/public/paper-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/paper.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/schemas/form.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | name: z.string().min(4).max(100),
5 | description: z.string().max(500).optional(),
6 | });
7 |
8 | export type formSchemaType = z.infer;
9 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------