├── .eslintrc.json ├── src ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── form │ │ │ └── new │ │ │ │ └── route.ts │ │ └── stripe │ │ │ ├── webhook │ │ │ └── route.ts │ │ │ ├── create-portal │ │ │ └── route.ts │ │ │ └── checkout-session │ │ │ └── route.ts │ ├── favicon.ico │ ├── actions │ │ ├── navigateToForm.ts │ │ ├── getUserForms.ts │ │ ├── userSubscriptions.ts │ │ ├── mutateForm.ts │ │ └── generateForm.ts │ ├── forms │ │ ├── layout.tsx │ │ ├── [formId] │ │ │ ├── success │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── edit │ │ │ └── [formId] │ │ │ │ └── page.tsx │ │ ├── FormsList.tsx │ │ ├── FormPublishSuccess.tsx │ │ ├── FormField.tsx │ │ └── Form.tsx │ ├── page.tsx │ ├── (admin) │ │ ├── view-forms │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── ManageSubscription.tsx │ │ │ └── page.tsx │ │ ├── results │ │ │ ├── ResultsDisplay.tsx │ │ │ ├── page.tsx │ │ │ ├── FormsPicker.tsx │ │ │ └── Table.tsx │ │ └── layout.tsx │ ├── payment │ │ └── success │ │ │ └── page.tsx │ ├── layout.tsx │ ├── subscription │ │ └── SubscribeBtn.tsx │ ├── form-generator │ │ ├── UserSubscriptionWrapper.tsx │ │ └── index.tsx │ ├── globals.css │ └── landing-page │ │ └── index.tsx ├── lib │ ├── stripe.ts │ ├── utils.ts │ └── stripe-client.ts ├── types │ ├── form-types.d.ts │ └── nav-types.d.ts ├── db │ ├── index.ts │ └── schema.ts ├── components │ ├── progressBar.tsx │ ├── icons.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── switch.tsx │ │ ├── header.tsx │ │ ├── radio-group.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ └── select.tsx │ └── navigation │ │ ├── updgradeAccBtn.tsx │ │ └── navbar.tsx └── auth.ts ├── postcss.config.js ├── public ├── images │ └── app │ │ ├── demo1.png │ │ ├── demo2.png │ │ ├── demo3.png │ │ └── demo4.png ├── vercel.svg ├── next.svg ├── arrow.svg └── grid.svg ├── .vscode └── settings.json ├── drizzle ├── meta │ ├── _journal.json │ └── 0000_snapshot.json └── 0000_blue_logan.sql ├── .env.example ├── next.config.js ├── drizzle.config.ts ├── components.json ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@/auth"; 2 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaber-Saed/ai-form-builder-tutorial/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/app/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaber-Saed/ai-form-builder-tutorial/HEAD/public/images/app/demo1.png -------------------------------------------------------------------------------- /public/images/app/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaber-Saed/ai-form-builder-tutorial/HEAD/public/images/app/demo2.png -------------------------------------------------------------------------------- /public/images/app/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaber-Saed/ai-form-builder-tutorial/HEAD/public/images/app/demo3.png -------------------------------------------------------------------------------- /public/images/app/demo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaber-Saed/ai-form-builder-tutorial/HEAD/public/images/app/demo4.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSaveMode": "modificationsIfAvailable", 4 | "prettier.singleAttributePerLine": true 5 | } 6 | -------------------------------------------------------------------------------- /src/app/actions/navigateToForm.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | 5 | export async function navigate(id: number) { 6 | redirect(`/forms/edit/${id}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | 3 | export const stripe = new Stripe( 4 | process.env.STRIPE_SECRET_KEY || " ", 5 | { 6 | apiVersion: "2023-10-16", 7 | typescript: true, 8 | } 9 | ); 10 | -------------------------------------------------------------------------------- /src/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 | 8 | export const MAX_FREE_FROMS = 3; 9 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1706725403234, 9 | "tag": "0000_blue_logan", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="" 2 | GOOGLE_CLIENT_ID="" 3 | GOOGLE_CLIENT_SECRET=" 4 | AUTH_SECRET="" 5 | DATABASE_URL="" 6 | NEXT_PUBLIC_PUBLISHABLE_KEY="" 7 | STRIPE_SECRET_KEY="" 8 | STRIPE_WEBHOOK_SECRET="" 9 | STRIPE_WEBHOOK_LOCAL_SERCRET="" 10 | PLAUSIBLE_DOMAIN="" 11 | -------------------------------------------------------------------------------- /src/app/forms/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FormEditLayout = ({ 4 | children 5 | }: { 6 | children: React.ReactNode 7 | }) => { 8 | return ( 9 |
{children}
10 | ) 11 | } 12 | 13 | export default FormEditLayout -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "lh3.googleusercontent.com", 8 | port: "", 9 | pathname: "/a/**", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /src/types/form-types.d.ts: -------------------------------------------------------------------------------- 1 | import { InferSelectModel } from "drizzle-orm"; 2 | import { forms, questions, fieldOptions } from "@/db/schema"; 3 | 4 | export type FormSelectModel = InferSelectModel; 5 | export type QuestionSelectModel = InferSelectModel; 6 | export type FieldOptionSelectModel = InferSelectModel; 7 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/db/schema.ts", 5 | out: "./drizzle", 6 | driver: "pg", 7 | dbCredentials: { 8 | connectionString: 9 | process.env.DATABASE_URL || 10 | "postgres://postgres:postgres@localhost:5432/postgres", 11 | }, 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/postgres-js"; 2 | import postgres from "postgres"; 3 | import * as schema from "./schema"; 4 | 5 | const connectionString = 6 | process.env.DATABASE_URL || 7 | "postgres://postgres:postgres@localhost:5432/postgres"; 8 | 9 | const client = postgres(connectionString); 10 | export const db = drizzle(client, { schema }); 11 | -------------------------------------------------------------------------------- /src/components/progressBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | value: number 5 | } 6 | 7 | const ProgressBar = (props: Props) => { 8 | return ( 9 |
10 |
11 |
12 | ) 13 | } 14 | 15 | export default ProgressBar -------------------------------------------------------------------------------- /src/lib/stripe-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | loadStripe, 3 | Stripe, 4 | } from "@stripe/stripe-js"; 5 | 6 | let stripePromise: Promise; 7 | 8 | export const getStripe = () => { 9 | if (!stripePromise) { 10 | stripePromise = loadStripe( 11 | process.env 12 | .NEXT_PUBLIC_PUBLISHABLE_KEY ?? 13 | "" 14 | ); 15 | } 16 | 17 | return stripePromise; 18 | }; 19 | -------------------------------------------------------------------------------- /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": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/ui/header"; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import LandingPage from './landing-page'; 4 | 5 | export default function Home() { 6 | return ( 7 | 8 |
9 |
10 | 11 |
12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/forms/[formId]/success/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 3 | 4 | 5 | const page = () => { 6 | return ( 7 | 8 | Success 9 | 10 | Your answers were recorded successfully. Thank you for submitting the form! 11 | 12 | ) 13 | } 14 | 15 | export default page -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Library, 3 | LineChart, 4 | PieChart, 5 | Settings2, 6 | Settings, 7 | UserRoundCog, 8 | List, 9 | type IconNode as LucideIcon 10 | } from "lucide-react"; 11 | 12 | export type Icon = LucideIcon; 13 | 14 | export const Icons = { 15 | library: Library, 16 | lineChart: LineChart, 17 | pieChart: PieChart, 18 | settings2: Settings2, 19 | settings: Settings, 20 | userRoundCog: UserRoundCog, 21 | list: List, 22 | } -------------------------------------------------------------------------------- /src/types/nav-types.d.ts: -------------------------------------------------------------------------------- 1 | import { Icons } from "../components/icons"; 2 | 3 | type NavLink = { 4 | title: string; 5 | href: string; 6 | disabled?: boolean; 7 | }; 8 | 9 | type SidebarNavItem = { 10 | title: string; 11 | disabled?: boolean; 12 | external?: boolean; 13 | icon?: keyof typeof Icons; 14 | } & ( 15 | | { 16 | href: string; 17 | items?: never; 18 | } 19 | | { 20 | href?: string; 21 | items: NavLink[]; 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /src/app/actions/getUserForms.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { db } from "@/db"; 3 | import { eq } from "drizzle-orm"; 4 | import { forms } from "@/db/schema"; 5 | import { auth } from "@/auth"; 6 | 7 | export async function getUserForms() { 8 | const session = await auth(); 9 | const userId = session?.user?.id; 10 | if (!userId) { 11 | return []; 12 | } 13 | 14 | const userForms = await db.query.forms.findMany({ 15 | where: eq(forms.userId, userId), 16 | }); 17 | return userForms; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(admin)/view-forms/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FormsList from '@/app/forms/FormsList' 3 | import { getUserForms } from '@/app/actions/getUserForms' 4 | import { InferSelectModel } from 'drizzle-orm' 5 | import { forms as dbForms } from "@/db/schema"; 6 | 7 | type Props = {} 8 | 9 | const page = async (props: Props) => { 10 | const forms: InferSelectModel[] = await getUserForms(); 11 | 12 | return ( 13 | <> 14 | 15 | ) 16 | } 17 | 18 | export default page -------------------------------------------------------------------------------- /src/app/payment/success/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 3 | import Link from 'next/link' 4 | 5 | 6 | const page = () => { 7 | return ( 8 | 9 | Success 10 | 11 | Your account has been updated. Go to the dashboard to create more forms 12 | 13 | ) 14 | } 15 | 16 | export default page -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | 5 | import PlausibleProvider from 'next-plausible' 6 | 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const metadata: Metadata = { 10 | title: 'Create Next App', 11 | description: 'Generated by create next app', 12 | } 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode 18 | }) { 19 | return ( 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 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 | "@/*": ["./src/*", "./src/app/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type Session, type User } from "next-auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | import { DrizzleAdapter } from "@auth/drizzle-adapter"; 4 | import { db } from "./db/index"; 5 | 6 | export const { 7 | handlers: { GET, POST }, 8 | auth, 9 | signIn, 10 | signOut, 11 | } = NextAuth({ 12 | adapter: DrizzleAdapter(db), 13 | providers: [ 14 | GoogleProvider({ 15 | clientId: process.env.GOOGLE_CLIENT_ID, 16 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 17 | }), 18 | ], 19 | callbacks: { 20 | async session({ session, user }: { session: Session; user?: User }) { 21 | if (user && session?.user) { 22 | session.user.id = user.id; 23 | } 24 | return session; 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/app/forms/[formId]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { db } from '@/db'; 3 | import { forms } from '@/db/schema'; 4 | import { eq } from 'drizzle-orm'; 5 | import { auth } from '@/auth'; 6 | import Form from '../Form'; 7 | 8 | 9 | const page = async ({ params }: { 10 | params: { 11 | formId: string 12 | } 13 | }) => { 14 | const formId = params.formId; 15 | 16 | if (!formId) { 17 | return
Form not found
18 | }; 19 | 20 | const form = await db.query.forms.findFirst({ 21 | where: eq(forms.id, parseInt(formId)), 22 | with: { 23 | questions: { 24 | with: { 25 | fieldOptions: true 26 | } 27 | } 28 | } 29 | }) 30 | 31 | if (!form) { 32 | return
Form not found
33 | } 34 | 35 | return ( 36 |
37 | ) 38 | } 39 | export default page; -------------------------------------------------------------------------------- /src/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 |