├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sing-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sing-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (dashboard) │ ├── (routes) │ │ ├── code │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── conversation │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── image │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── music │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ └── video │ │ │ ├── constants.ts │ │ │ └── page.tsx │ └── layout.tsx ├── (landing) │ ├── layout.tsx │ └── page.tsx ├── api │ ├── code │ │ └── route.ts │ ├── conversation │ │ └── route.ts │ ├── image │ │ └── route.ts │ ├── music │ │ └── route.ts │ ├── stripe │ │ └── route.ts │ ├── video │ │ └── route.ts │ └── webhook │ │ └── route.ts ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── bot-avatar.tsx ├── crisp-chat.tsx ├── crisp-provider.tsx ├── empty.tsx ├── footer.tsx ├── free-counter.tsx ├── heading.tsx ├── landing-content.tsx ├── landing-hero.tsx ├── landing-navbar.tsx ├── loader.tsx ├── mobile-sidebar.tsx ├── modal-provider.tsx ├── navbar.tsx ├── pro-modal.tsx ├── sidebar.tsx ├── subscription-button.tsx ├── toaster-provider.tsx ├── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── progress.tsx │ ├── select.tsx │ └── sheet.tsx └── user-avatar.tsx ├── constants.ts ├── hooks └── use-pro-modal.tsx ├── lib ├── api-limit.ts ├── prismadb.ts ├── stripe.ts ├── subscription.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── empty.png ├── logo-old.png ├── logo.png ├── next.svg └── vercel.svg ├── tailwind.config.js ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sammy Leths 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 |

Witty AI SaaS

2 | 3 | Witty AI Software As A Service is an AI tool that can be used to create content 10x faster. This tool allows automatic generation of contents such as Images, Videos, Music and Code. It can also generate meaningful conversational responses via chat. 4 | 5 | Witty AI Software As A Service (SAAS) is a modern responsive, simple saas platform suitable for use by digital marketing agencies and individuals. With this project, you can easily generate marketing assets such as Images, Videos, Music, Q&A Conversations and Code Snippets. Some of the features built into this project include: 6 | 7 | 19 | 20 | This project was developed using React, NextJS, TypeScript, Tailwind CSS, Prisma, MySql, Axios, NPM. 21 | 22 |

Screenshots

23 | 24 | ![proj11-witty-ai](https://github.com/SammyLeths/witty-ai-saas/assets/64320618/c46f4556-28bf-4b20-aaf1-51f636f1498c) 25 | 26 |

Links

27 | 28 | 31 | 32 |

Tech Stack

33 | 34 |

35 | REACT JS 36 | NEXT JS 37 | TYPESCRIPT 38 | TAILWIND CSS 39 | PRISMA 40 | MYSQL 41 | AXIOS 42 | NPM 43 | HTML 44 | CSS3 45 | SASS 46 | JAVASCRIPT 47 |

48 | 49 |

Helpful Resources

50 | 51 | 104 | 105 |

Author's Links

106 | 107 | 113 | 114 |
115 | 116 |
117 | 118 |
119 | 120 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 121 | 122 | ## Getting Started 123 | 124 | First, run the development server: 125 | 126 | ```bash 127 | npm run dev 128 | # or 129 | yarn dev 130 | # or 131 | pnpm dev 132 | ``` 133 | 134 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 135 | 136 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 137 | 138 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 139 | 140 | ## Learn More 141 | 142 | To learn more about Next.js, take a look at the following resources: 143 | 144 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 145 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 146 | 147 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 148 | 149 | ## Deploy on Vercel 150 | 151 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 152 | 153 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 154 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sing-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sing-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
{children}
4 | ); 5 | }; 6 | 7 | export default AuthLayout; 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as z from "zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { Code, Divide } from "lucide-react"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useRouter } from "next/navigation"; 7 | import { ChatCompletionRequestMessage } from "openai"; 8 | import axios from "axios"; 9 | 10 | import Heading from "@/components/heading"; 11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | 14 | import { formSchema } from "./constants"; 15 | import { Button } from "@/components/ui/button"; 16 | import { useState } from "react"; 17 | import Empty from "@/components/empty"; 18 | import Loader from "@/components/loader"; 19 | import { cn } from "@/lib/utils"; 20 | import UserAvatar from "@/components/user-avatar"; 21 | import BotAvatar from "@/components/bot-avatar"; 22 | import ReactMarkdown from "react-markdown"; 23 | import { useProModal } from "@/hooks/use-pro-modal"; 24 | import toast from "react-hot-toast"; 25 | 26 | const CodePage = () => { 27 | const proModal = useProModal(); 28 | const router = useRouter(); 29 | const [messages, setMessages] = useState([]); 30 | 31 | const form = useForm>({ 32 | resolver: zodResolver(formSchema), 33 | defaultValues: { 34 | prompt: "", 35 | }, 36 | }); 37 | 38 | const isLoading = form.formState.isSubmitting; 39 | 40 | const onSubmit = async (values: z.infer) => { 41 | try { 42 | const userMessage: ChatCompletionRequestMessage = { 43 | role: "user", 44 | content: values.prompt, 45 | }; 46 | const newMessages = [...messages, userMessage]; 47 | 48 | const response = await axios.post("/api/code", { 49 | messages: newMessages, 50 | }); 51 | 52 | setMessages((current) => [...current, userMessage, response.data]); 53 | form.reset(); 54 | } catch (error: any) { 55 | if (error?.response?.status === 403) { 56 | proModal.onOpen(); 57 | } else { 58 | toast.error("Something went wrong"); 59 | } 60 | } finally { 61 | router.refresh(); 62 | } 63 | }; 64 | 65 | return ( 66 |
67 | 74 |
75 |
76 |
77 | 81 | ( 84 | 85 | 86 | 92 | 93 | 94 | )} 95 | /> 96 | 103 | 104 | 105 |
106 |
107 | {isLoading && ( 108 |
109 | 110 |
111 | )} 112 | {messages.length === 0 && !isLoading && ( 113 |
114 | 115 |
116 | )} 117 |
118 | {messages.map((message) => ( 119 |
128 | {message.role === "user" ? : } 129 | ( 132 |
133 |
134 |                       
135 | ), 136 | code: ({ node, ...props }) => ( 137 | 138 | ), 139 | }} 140 | className="text-sm overflow-hidden leading-7" 141 | > 142 | {message.content || ""} 143 |
144 |
145 | ))} 146 |
147 |
148 |
149 |
150 | ); 151 | }; 152 | 153 | export default CodePage; 154 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as z from "zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { MessageSquare } from "lucide-react"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useRouter } from "next/navigation"; 7 | import { ChatCompletionRequestMessage } from "openai"; 8 | import axios from "axios"; 9 | 10 | import Heading from "@/components/heading"; 11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | 14 | import { formSchema } from "./constants"; 15 | import { Button } from "@/components/ui/button"; 16 | import { useState } from "react"; 17 | import Empty from "@/components/empty"; 18 | import Loader from "@/components/loader"; 19 | import { cn } from "@/lib/utils"; 20 | import UserAvatar from "@/components/user-avatar"; 21 | import BotAvatar from "@/components/bot-avatar"; 22 | import { useProModal } from "@/hooks/use-pro-modal"; 23 | import toast from "react-hot-toast"; 24 | 25 | const ConversationPage = () => { 26 | const proModal = useProModal(); 27 | const router = useRouter(); 28 | const [messages, setMessages] = useState([]); 29 | 30 | const form = useForm>({ 31 | resolver: zodResolver(formSchema), 32 | defaultValues: { 33 | prompt: "", 34 | }, 35 | }); 36 | 37 | const isLoading = form.formState.isSubmitting; 38 | 39 | const onSubmit = async (values: z.infer) => { 40 | try { 41 | const userMessage: ChatCompletionRequestMessage = { 42 | role: "user", 43 | content: values.prompt, 44 | }; 45 | const newMessages = [...messages, userMessage]; 46 | 47 | const response = await axios.post("/api/conversation", { 48 | messages: newMessages, 49 | }); 50 | 51 | setMessages((current) => [...current, userMessage, response.data]); 52 | form.reset(); 53 | } catch (error: any) { 54 | if (error?.response?.status === 403) { 55 | proModal.onOpen(); 56 | } else { 57 | toast.error("Something went wrong"); 58 | } 59 | } finally { 60 | router.refresh(); 61 | } 62 | }; 63 | 64 | return ( 65 |
66 | 73 |
74 |
75 |
76 | 80 | ( 83 | 84 | 85 | 91 | 92 | 93 | )} 94 | /> 95 | 102 | 103 | 104 |
105 |
106 | {isLoading && ( 107 |
108 | 109 |
110 | )} 111 | {messages.length === 0 && !isLoading && ( 112 |
113 | 114 |
115 | )} 116 |
117 | {messages.map((message) => ( 118 |
127 | {message.role === "user" ? : } 128 |

{message.content}

129 |
130 | ))} 131 |
132 |
133 |
134 |
135 | ); 136 | }; 137 | 138 | export default ConversationPage; 139 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import { Card } from "@/components/ui/card"; 5 | import { cn } from "@/lib/utils"; 6 | import { 7 | ArrowRight, 8 | Code, 9 | ImageIcon, 10 | MessageSquare, 11 | Music, 12 | VideoIcon, 13 | } from "lucide-react"; 14 | 15 | const tools = [ 16 | { 17 | label: "Conversation", 18 | icon: MessageSquare, 19 | color: "text-yellow-500", 20 | bgColor: "bg-yellow-500/10", 21 | href: "/conversation", 22 | }, 23 | { 24 | label: "Music Generation", 25 | icon: Music, 26 | color: "text-fuchsia-500", 27 | bgColor: "bg-fuchsia-500/10", 28 | href: "/music", 29 | }, 30 | { 31 | label: "Image Generation", 32 | icon: ImageIcon, 33 | color: "text-teal-700", 34 | bgColor: "bg-teal-700/10", 35 | href: "/image", 36 | }, 37 | { 38 | label: "Video Generation", 39 | icon: VideoIcon, 40 | color: "text-lime-700", 41 | bgColor: "bg-lime-700/10", 42 | href: "/video", 43 | }, 44 | { 45 | label: "Code Generation", 46 | icon: Code, 47 | color: "text-blue-700", 48 | bgColor: "bg-blue-700/10", 49 | href: "/code", 50 | }, 51 | ]; 52 | 53 | const DashboardPage = () => { 54 | const router = useRouter(); 55 | 56 | return ( 57 |
58 |
59 |

60 | Explore the power of AI 61 |

62 |

63 | Chat with the smartest AI - Experience the power of AI 64 |

65 |
66 |
67 | {tools.map((tool) => ( 68 | router.push(tool.href)} 70 | key={tool.href} 71 | className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer" 72 | > 73 |
74 |
75 | 76 |
77 |
{tool.label}
78 |
79 | 80 |
81 | ))} 82 |
83 |
84 | ); 85 | }; 86 | 87 | export default DashboardPage; 88 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Image Prompt is required", 6 | }), 7 | amount: z.string().min(1), 8 | resolution: z.string().min(1), 9 | }); 10 | 11 | export const amountOptions = [ 12 | { 13 | value: "1", 14 | label: "1 Photo", 15 | }, 16 | { 17 | value: "2", 18 | label: "2 Photos", 19 | }, 20 | { 21 | value: "3", 22 | label: "3 Photos", 23 | }, 24 | { 25 | value: "4", 26 | label: "4 Photos", 27 | }, 28 | { 29 | value: "5", 30 | label: "5 Photos", 31 | }, 32 | ]; 33 | 34 | export const resolutionOptions = [ 35 | { 36 | value: "256x256", 37 | label: "256x256", 38 | }, 39 | { 40 | value: "512x512", 41 | label: "512x512", 42 | }, 43 | { 44 | value: "1024x1024", 45 | label: "1024x1024", 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as z from "zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { Download, ImageIcon } from "lucide-react"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | import axios from "axios"; 9 | 10 | import Heading from "@/components/heading"; 11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | 14 | import { amountOptions, formSchema, resolutionOptions } from "./constants"; 15 | import { Button } from "@/components/ui/button"; 16 | import { useState } from "react"; 17 | import Empty from "@/components/empty"; 18 | import Loader from "@/components/loader"; 19 | import { cn } from "@/lib/utils"; 20 | 21 | import { 22 | Select, 23 | SelectContent, 24 | SelectItem, 25 | SelectTrigger, 26 | SelectValue, 27 | } from "@/components/ui/select"; 28 | import { Card, CardFooter } from "@/components/ui/card"; 29 | import Image from "next/image"; 30 | import { useProModal } from "@/hooks/use-pro-modal"; 31 | import toast from "react-hot-toast"; 32 | 33 | const ImagePage = () => { 34 | const proModal = useProModal(); 35 | const router = useRouter(); 36 | const [images, setImages] = useState([]); 37 | 38 | const form = useForm>({ 39 | resolver: zodResolver(formSchema), 40 | defaultValues: { 41 | prompt: "", 42 | amount: "1", 43 | resolution: "512x512", 44 | }, 45 | }); 46 | 47 | const isLoading = form.formState.isSubmitting; 48 | 49 | const onSubmit = async (values: z.infer) => { 50 | try { 51 | setImages([]); 52 | 53 | const response = await axios.post("/api/image", values); 54 | 55 | const urls = response.data.map((image: { url: string }) => image.url); 56 | 57 | setImages(urls); 58 | form.reset(); 59 | } catch (error: any) { 60 | if (error?.response?.status === 403) { 61 | proModal.onOpen(); 62 | } else { 63 | toast.error("Something went wrong"); 64 | } 65 | } finally { 66 | router.refresh(); 67 | } 68 | }; 69 | 70 | return ( 71 |
72 | 79 |
80 |
81 |
82 | 86 | ( 89 | 90 | 91 | 97 | 98 | 99 | )} 100 | /> 101 | ( 105 | 106 | 125 | 126 | )} 127 | /> 128 | ( 132 | 133 | 152 | 153 | )} 154 | /> 155 | 162 | 163 | 164 |
165 |
166 | {isLoading && ( 167 |
168 | 169 |
170 | )} 171 | {images.length === 0 && !isLoading && ( 172 |
173 | 174 |
175 | )} 176 |
177 | {images.map((src) => ( 178 | 179 |
180 | Image 181 |
182 | 183 | 191 | 192 |
193 | ))} 194 |
195 |
196 |
197 |
198 | ); 199 | }; 200 | 201 | export default ImagePage; 202 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Music Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as z from "zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { Music } from "lucide-react"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useRouter } from "next/navigation"; 7 | import { ChatCompletionRequestMessage } from "openai"; 8 | import axios from "axios"; 9 | 10 | import Heading from "@/components/heading"; 11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | 14 | import { formSchema } from "./constants"; 15 | import { Button } from "@/components/ui/button"; 16 | import { useState } from "react"; 17 | import Empty from "@/components/empty"; 18 | import Loader from "@/components/loader"; 19 | import { cn } from "@/lib/utils"; 20 | import UserAvatar from "@/components/user-avatar"; 21 | import BotAvatar from "@/components/bot-avatar"; 22 | import { useProModal } from "@/hooks/use-pro-modal"; 23 | import toast from "react-hot-toast"; 24 | 25 | const MusicPage = () => { 26 | const proModal = useProModal(); 27 | const router = useRouter(); 28 | const [music, setMusic] = useState(); 29 | 30 | const form = useForm>({ 31 | resolver: zodResolver(formSchema), 32 | defaultValues: { 33 | prompt: "", 34 | }, 35 | }); 36 | 37 | const isLoading = form.formState.isSubmitting; 38 | 39 | const onSubmit = async (values: z.infer) => { 40 | try { 41 | setMusic(undefined); 42 | 43 | const response = await axios.post("/api/music", values); 44 | 45 | setMusic(response.data.audio); 46 | form.reset(); 47 | } catch (error: any) { 48 | if (error?.response?.status === 403) { 49 | proModal.onOpen(); 50 | } else { 51 | toast.error("Something went wrong"); 52 | } 53 | } finally { 54 | router.refresh(); 55 | } 56 | }; 57 | 58 | return ( 59 |
60 | 67 |
68 |
69 |
70 | 74 | ( 77 | 78 | 79 | 85 | 86 | 87 | )} 88 | /> 89 | 96 | 97 | 98 |
99 |
100 | {isLoading && ( 101 |
102 | 103 |
104 | )} 105 | {!music && !isLoading && ( 106 |
107 | 108 |
109 | )} 110 | {music && ( 111 | 114 | )} 115 |
116 |
117 |
118 | ); 119 | }; 120 | 121 | export default MusicPage; 122 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Settings } from "lucide-react"; 2 | 3 | import Heading from "@/components/heading"; 4 | import { checkSubscription } from "@/lib/subscription"; 5 | import { SubscriptionButton } from "@/components/subscription-button"; 6 | 7 | const SettingsPage = async () => { 8 | const isPro = await checkSubscription(); 9 | 10 | return ( 11 |
12 | 19 |
20 |
21 | {isPro 22 | ? "You are currently on a pro plan." 23 | : "You are currently on a free plan."} 24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default SettingsPage; 32 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Video Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as z from "zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { VideoIcon } from "lucide-react"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useRouter } from "next/navigation"; 7 | import axios from "axios"; 8 | 9 | import Heading from "@/components/heading"; 10 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 11 | import { Input } from "@/components/ui/input"; 12 | 13 | import { formSchema } from "./constants"; 14 | import { Button } from "@/components/ui/button"; 15 | import { useState } from "react"; 16 | import Empty from "@/components/empty"; 17 | import Loader from "@/components/loader"; 18 | import { cn } from "@/lib/utils"; 19 | import UserAvatar from "@/components/user-avatar"; 20 | import BotAvatar from "@/components/bot-avatar"; 21 | import { useProModal } from "@/hooks/use-pro-modal"; 22 | import toast from "react-hot-toast"; 23 | 24 | const VideoPage = () => { 25 | const proModal = useProModal(); 26 | const router = useRouter(); 27 | const [video, setVideo] = useState(); 28 | 29 | const form = useForm>({ 30 | resolver: zodResolver(formSchema), 31 | defaultValues: { 32 | prompt: "", 33 | }, 34 | }); 35 | 36 | const isLoading = form.formState.isSubmitting; 37 | 38 | const onSubmit = async (values: z.infer) => { 39 | try { 40 | setVideo(undefined); 41 | 42 | const response = await axios.post("/api/video", values); 43 | 44 | setVideo(response.data[0]); 45 | form.reset(); 46 | } catch (error: any) { 47 | if (error?.response?.status === 403) { 48 | proModal.onOpen(); 49 | } else { 50 | toast.error("Something went wrong"); 51 | } 52 | } finally { 53 | router.refresh(); 54 | } 55 | }; 56 | 57 | return ( 58 |
59 | 66 |
67 |
68 |
69 | 73 | ( 76 | 77 | 78 | 84 | 85 | 86 | )} 87 | /> 88 | 95 | 96 | 97 |
98 |
99 | {isLoading && ( 100 |
101 | 102 |
103 | )} 104 | {!video && !isLoading && ( 105 |
106 | 107 |
108 | )} 109 | {video && ( 110 | 116 | )} 117 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default VideoPage; 124 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/components/footer"; 2 | import Navbar from "@/components/navbar"; 3 | import Sidebar from "@/components/sidebar"; 4 | import { getApiLimitCount } from "@/lib/api-limit"; 5 | import { checkSubscription } from "@/lib/subscription"; 6 | 7 | const DashboardLayout = async ({ children }: { children: React.ReactNode }) => { 8 | const apiLimitCount = await getApiLimitCount(); 9 | const isPro = await checkSubscription(); 10 | 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | {children}
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default DashboardLayout; 24 | -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | const LandingLayout = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
4 |
{children}
5 |
6 | ); 7 | }; 8 | 9 | export default LandingLayout; 10 | -------------------------------------------------------------------------------- /app/(landing)/page.tsx: -------------------------------------------------------------------------------- 1 | import LandingContent from "@/components/landing-content"; 2 | import LandingHero from "@/components/landing-hero"; 3 | import { LandingNavbar } from "@/components/landing-navbar"; 4 | import Link from "next/link"; 5 | 6 | const credit = ""; 7 | 8 | const LandingPage = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | A sandbox project by   16 | 21 | {credit} 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default LandingPage; 29 | -------------------------------------------------------------------------------- /app/api/code/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; 4 | 5 | import { increaseApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | 12 | const openai = new OpenAIApi(configuration); 13 | 14 | const instructionMessage: ChatCompletionRequestMessage = { 15 | role: "system", 16 | content: 17 | "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.", 18 | }; 19 | 20 | export async function POST(req: Request) { 21 | try { 22 | const { userId } = auth(); 23 | const body = await req.json(); 24 | const { messages } = body; 25 | 26 | if (!userId) { 27 | return new NextResponse("Unauthorized", { status: 401 }); 28 | } 29 | 30 | if (!configuration.apiKey) { 31 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 32 | } 33 | 34 | if (!messages) { 35 | return new NextResponse("Messages are required", { status: 400 }); 36 | } 37 | 38 | const freeTrial = await checkApiLimit(); 39 | const isPro = await checkSubscription(); 40 | 41 | if (!freeTrial && !isPro) { 42 | return new NextResponse("Free trial has expired.", { status: 403 }); 43 | } 44 | 45 | const response = await openai.createChatCompletion({ 46 | model: "gpt-3.5-turbo", 47 | messages: [instructionMessage, ...messages], 48 | }); 49 | 50 | if (!isPro) { 51 | await increaseApiLimit(); 52 | } 53 | 54 | return NextResponse.json(response.data.choices[0].message); 55 | } catch (error) { 56 | console.log("[CODE_ERROR]", error); 57 | return new NextResponse("Internal error", { status: 500 }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/api/conversation/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import { Configuration, OpenAIApi } from "openai"; 4 | 5 | import { increaseApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | 12 | const openai = new OpenAIApi(configuration); 13 | 14 | export async function POST(req: Request) { 15 | try { 16 | const { userId } = auth(); 17 | const body = await req.json(); 18 | const { messages } = body; 19 | 20 | if (!userId) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | if (!configuration.apiKey) { 25 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 26 | } 27 | 28 | if (!messages) { 29 | return new NextResponse("Messages are required", { status: 400 }); 30 | } 31 | 32 | const freeTrial = await checkApiLimit(); 33 | const isPro = await checkSubscription(); 34 | 35 | if (!freeTrial && !isPro) { 36 | return new NextResponse("Free trial has expired.", { status: 403 }); 37 | } 38 | 39 | const response = await openai.createChatCompletion({ 40 | model: "gpt-3.5-turbo", 41 | messages, 42 | }); 43 | 44 | if (!isPro) { 45 | await increaseApiLimit(); 46 | } 47 | 48 | return NextResponse.json(response.data.choices[0].message); 49 | } catch (error) { 50 | console.log("[CONVERSATION_ERROR]", error); 51 | return new NextResponse("Internal error", { status: 500 }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/api/image/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import { Configuration, OpenAIApi } from "openai"; 4 | 5 | import { increaseApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | 12 | const openai = new OpenAIApi(configuration); 13 | 14 | export async function POST(req: Request) { 15 | try { 16 | const { userId } = auth(); 17 | const body = await req.json(); 18 | const { prompt, amount = 1, resolution = "512x512" } = body; 19 | 20 | if (!userId) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | if (!configuration.apiKey) { 25 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 26 | } 27 | 28 | if (!prompt) { 29 | return new NextResponse("Prompt is required", { status: 400 }); 30 | } 31 | 32 | if (!amount) { 33 | return new NextResponse("Amount is required", { status: 400 }); 34 | } 35 | 36 | if (!resolution) { 37 | return new NextResponse("Resolution is required", { status: 400 }); 38 | } 39 | 40 | const freeTrial = await checkApiLimit(); 41 | const isPro = await checkSubscription(); 42 | 43 | if (!freeTrial && !isPro) { 44 | return new NextResponse("Free trial has expired.", { status: 403 }); 45 | } 46 | 47 | const response = await openai.createImage({ 48 | prompt, 49 | n: parseInt(amount, 10), 50 | size: resolution, 51 | }); 52 | 53 | if (!isPro) { 54 | await increaseApiLimit(); 55 | } 56 | 57 | return NextResponse.json(response.data.data); 58 | } catch (error) { 59 | console.log("[IMAGEERROR]", error); 60 | return new NextResponse("Internal error", { status: 500 }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/api/music/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import Replicate from "replicate"; 4 | 5 | import { increaseApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const replicate = new Replicate({ 9 | auth: process.env.REPLICATE_API_TOKEN!, 10 | }); 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const { userId } = auth(); 15 | const body = await req.json(); 16 | const { prompt } = body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | 22 | if (!prompt) { 23 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 24 | } 25 | 26 | const freeTrial = await checkApiLimit(); 27 | const isPro = await checkSubscription(); 28 | 29 | if (!freeTrial && !isPro) { 30 | return new NextResponse("Free trial has expired.", { status: 403 }); 31 | } 32 | 33 | const response = await replicate.run( 34 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", 35 | { 36 | input: { 37 | prompt_a: prompt, 38 | }, 39 | } 40 | ); 41 | 42 | if (!isPro) { 43 | await increaseApiLimit(); 44 | } 45 | 46 | return NextResponse.json(response); 47 | } catch (error) { 48 | console.log("[MUSIC_ERROR]", error); 49 | return new NextResponse("Internal error", { status: 500 }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/api/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, currentUser } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import { stripe } from "@/lib/stripe"; 6 | import { absoluteUrl } from "@/lib/utils"; 7 | 8 | const settingsUrl = absoluteUrl("/settings"); 9 | 10 | export async function GET() { 11 | try { 12 | const { userId } = auth(); 13 | const user = await currentUser(); 14 | 15 | if (!userId || !user) { 16 | return new NextResponse("Unauthorized", { status: 401 }); 17 | } 18 | 19 | const userSubscription = await prismadb.userSubscription.findUnique({ 20 | where: { 21 | userId, 22 | }, 23 | }); 24 | 25 | if (userSubscription && userSubscription.stripeCustomerId) { 26 | const stripeSession = await stripe.billingPortal.sessions.create({ 27 | customer: userSubscription.stripeCustomerId, 28 | return_url: settingsUrl, 29 | }); 30 | 31 | return new NextResponse(JSON.stringify({ url: stripeSession.url })); 32 | } 33 | 34 | const stripeSession = await stripe.checkout.sessions.create({ 35 | success_url: settingsUrl, 36 | cancel_url: settingsUrl, 37 | payment_method_types: ["card"], 38 | mode: "subscription", 39 | billing_address_collection: "auto", 40 | customer_email: user.emailAddresses[0].emailAddress, 41 | line_items: [ 42 | { 43 | price_data: { 44 | currency: "USD", 45 | product_data: { 46 | name: "Witty AI Pro", 47 | description: "Unlimited AI Generations", 48 | }, 49 | unit_amount: 2000, 50 | recurring: { 51 | interval: "month", 52 | }, 53 | }, 54 | quantity: 1, 55 | }, 56 | ], 57 | metadata: { 58 | userId, 59 | }, 60 | }); 61 | 62 | return new NextResponse(JSON.stringify({ url: stripeSession.url })); 63 | } catch (error) { 64 | console.log("[STRIPE_ERROR]", error); 65 | return new NextResponse("Internal error", { status: 500 }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/api/video/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import Replicate from "replicate"; 4 | 5 | import { increaseApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const replicate = new Replicate({ 9 | auth: process.env.REPLICATE_API_TOKEN!, 10 | }); 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const { userId } = auth(); 15 | const body = await req.json(); 16 | const { prompt } = body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | 22 | if (!prompt) { 23 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 24 | } 25 | 26 | const freeTrial = await checkApiLimit(); 27 | const isPro = await checkSubscription(); 28 | 29 | if (!freeTrial && !isPro) { 30 | return new NextResponse("Free trial has expired.", { status: 403 }); 31 | } 32 | 33 | const response = await replicate.run( 34 | "anotherjesse/zeroscope-v2-xl:9f747673945c62801b13b84701c783929c0ee784e4748ec062204894dda1a351", 35 | { 36 | input: { 37 | prompt: prompt, 38 | }, 39 | } 40 | ); 41 | 42 | if (!isPro) { 43 | await increaseApiLimit(); 44 | } 45 | 46 | return NextResponse.json(response); 47 | } catch (error) { 48 | console.log("[VIDEO_ERROR]", error); 49 | return new NextResponse("Internal error", { status: 500 }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { headers } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import prismadb from "@/lib/prismadb"; 6 | import { stripe } from "@/lib/stripe"; 7 | 8 | export async function POST(req: Request) { 9 | const body = await req.text(); 10 | const signature = headers().get("Stripe-Signature") as string; 11 | 12 | let event: Stripe.Event; 13 | 14 | try { 15 | event = stripe.webhooks.constructEvent( 16 | body, 17 | signature, 18 | process.env.STRIPE_WEBHOOK_SECRET! 19 | ); 20 | } catch (error: any) { 21 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); 22 | } 23 | 24 | const session = event.data.object as Stripe.Checkout.Session; 25 | 26 | if (event.type === "checkout.session.completed") { 27 | const subscription = await stripe.subscriptions.retrieve( 28 | session.subscription as string 29 | ); 30 | 31 | if (!session?.metadata?.userId) { 32 | return new NextResponse("User id is required", { status: 400 }); 33 | } 34 | 35 | await prismadb.userSubscription.create({ 36 | data: { 37 | userId: session?.metadata?.userId, 38 | stripeSubscriptionId: subscription.id, 39 | stripeCustomerId: subscription.customer as string, 40 | stripePriceId: subscription.items.data[0].price.id, 41 | stripeCurrentPeriodEnd: new Date( 42 | subscription.current_period_end * 1000 43 | ), 44 | }, 45 | }); 46 | } 47 | 48 | if (event.type === "invoice.payment_succeeded") { 49 | const subscription = await stripe.subscriptions.retrieve( 50 | session.subscription as string 51 | ); 52 | 53 | await prismadb.userSubscription.update({ 54 | where: { 55 | stripeSubscriptionId: subscription.id, 56 | }, 57 | data: { 58 | stripePriceId: subscription.items.data[0].price.id, 59 | stripeCurrentPeriodEnd: new Date( 60 | subscription.current_period_end * 1000 61 | ), 62 | }, 63 | }); 64 | } 65 | 66 | return new NextResponse(null, { status: 200 }); 67 | } 68 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SammyLeths/witty-ai-saas/9be9c563cddef98b92600c4e70ece8bc366ca6c7/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 222.2 84% 4.9%; 21 | 22 | --primary: 248 90% 66%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --muted: 210 40% 96.1%; 29 | --muted-foreground: 215.4 16.3% 46.9%; 30 | 31 | --accent: 210 40% 96.1%; 32 | --accent-foreground: 222.2 47.4% 11.2%; 33 | 34 | --destructive: 0 84.2% 60.2%; 35 | --destructive-foreground: 210 40% 98%; 36 | 37 | --border: 214.3 31.8% 91.4%; 38 | --input: 214.3 31.8% 91.4%; 39 | --ring: 222.2 84% 4.9%; 40 | 41 | --radius: 0.5rem; 42 | } 43 | 44 | .dark { 45 | --background: 222.2 84% 4.9%; 46 | --foreground: 210 40% 98%; 47 | 48 | --card: 222.2 84% 4.9%; 49 | --card-foreground: 210 40% 98%; 50 | 51 | --popover: 222.2 84% 4.9%; 52 | --popover-foreground: 210 40% 98%; 53 | 54 | --primary: 210 40% 98%; 55 | --primary-foreground: 222.2 47.4% 11.2%; 56 | 57 | --secondary: 217.2 32.6% 17.5%; 58 | --secondary-foreground: 210 40% 98%; 59 | 60 | --muted: 217.2 32.6% 17.5%; 61 | --muted-foreground: 215 20.2% 65.1%; 62 | 63 | --accent: 217.2 32.6% 17.5%; 64 | --accent-foreground: 210 40% 98%; 65 | 66 | --destructive: 0 62.8% 30.6%; 67 | --destructive-foreground: 210 40% 98%; 68 | 69 | --border: 217.2 32.6% 17.5%; 70 | --input: 217.2 32.6% 17.5%; 71 | --ring: hsl(212.7, 26.8%, 83.9); 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body { 80 | @apply bg-background text-foreground; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { ModalProvider } from "@/components/modal-provider"; 6 | import ToasterProvider from "@/components/toaster-provider"; 7 | import { CrispProvider } from "@/components/crisp-provider"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Witty AI Saas", 13 | description: "Witty AI Platform", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /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/bot-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarImage } from "./ui/avatar"; 2 | 3 | const BotAvatar = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default BotAvatar; 12 | -------------------------------------------------------------------------------- /components/crisp-chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { Crisp } from "crisp-sdk-web"; 5 | 6 | export const CrispChat = () => { 7 | useEffect(() => { 8 | Crisp.configure("e16f7ad2-4eac-4502-a226-c9c2aaac8d09"); 9 | }, []); 10 | 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /components/crisp-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CrispChat } from "./crisp-chat"; 4 | 5 | export const CrispProvider = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /components/empty.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface EmptyProps { 4 | label: string; 5 | } 6 | 7 | const Empty = ({ label }: EmptyProps) => { 8 | return ( 9 |
10 |
11 | Empty 12 |
13 |

{label}

14 |
15 | ); 16 | }; 17 | 18 | export default Empty; 19 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const credit = ""; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 | A sandbox project by   9 | 14 | {credit} 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Footer; 21 | -------------------------------------------------------------------------------- /components/free-counter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Card, CardContent } from "@/components/ui/card"; 3 | import { MAX_FREE_COUNTS } from "@/constants"; 4 | import { Progress } from "@/components/ui/progress"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Zap } from "lucide-react"; 7 | import { useProModal } from "@/hooks/use-pro-modal"; 8 | 9 | interface FreeCounterProps { 10 | apiLimitCount: number; 11 | isPro: boolean; 12 | } 13 | 14 | export const FreeCounter = ({ 15 | apiLimitCount = 0, 16 | isPro = false, 17 | }: FreeCounterProps) => { 18 | const proModal = useProModal(); 19 | const [mounted, setMounted] = useState(false); 20 | 21 | useEffect(() => { 22 | setMounted(true); 23 | }, []); 24 | 25 | if (!mounted) { 26 | return null; 27 | } 28 | 29 | if (isPro) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 |
38 |

39 | {apiLimitCount} / {MAX_FREE_COUNTS} Free Generations 40 |

41 | 45 |
46 | 53 |
54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { LucideIcon } from "lucide-react"; 3 | 4 | interface HeadingProps { 5 | title: string; 6 | description: string; 7 | icon: LucideIcon; 8 | iconColor?: string; 9 | bgColor?: string; 10 | } 11 | 12 | const Heading = ({ 13 | title, 14 | description, 15 | icon: Icon, 16 | iconColor, 17 | bgColor, 18 | }: HeadingProps) => { 19 | return ( 20 |
21 |
22 | 23 |
24 |
25 |

{title}

26 |

{description}

27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Heading; 33 | -------------------------------------------------------------------------------- /components/landing-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; 4 | 5 | const testimonials = [ 6 | { 7 | name: "Stanley", 8 | avatar: "S", 9 | title: "Content Writer", 10 | description: "I absolutely love this seamless application!", 11 | }, 12 | { 13 | name: "Morgan", 14 | avatar: "M", 15 | title: "Digital Marketer", 16 | description: "Content creation for marketing is a breeze!", 17 | }, 18 | { 19 | name: "Bridget", 20 | avatar: "B", 21 | title: "Data Analyst", 22 | description: "My research papers are now created in half the time", 23 | }, 24 | { 25 | name: "Svelte", 26 | avatar: "S", 27 | title: "Sales Manager", 28 | description: "I am now boosting sales like never before with AI", 29 | }, 30 | ]; 31 | 32 | const LandingContent = () => { 33 | return ( 34 |
35 |

36 | Testimonials 37 |

38 |
39 | {testimonials.map((item) => ( 40 | 44 | 45 | 46 |
47 |

{item.name}

48 |

{item.title}

49 |
50 |
51 | 52 | {item.description} 53 | 54 |
55 |
56 | ))} 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default LandingContent; 63 | -------------------------------------------------------------------------------- /components/landing-hero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@clerk/nextjs"; 4 | import Link from "next/link"; 5 | import TypewriterComponent from "typewriter-effect"; 6 | import { Button } from "./ui/button"; 7 | 8 | const LandingHero = () => { 9 | const { isSignedIn } = useAuth(); 10 | 11 | return ( 12 |
13 |
14 |

The Best AI Tool for

15 |
16 | 29 |
30 |
31 |
32 | Create content using AI 10x faster. 33 |
34 |
35 | 36 | 42 | 43 |
44 |
45 | No credit card required. 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default LandingHero; 52 | -------------------------------------------------------------------------------- /components/landing-navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Montserrat } from "next/font/google"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | import { useAuth } from "@clerk/nextjs"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "@/components/ui/button"; 10 | 11 | const font = Montserrat({ 12 | weight: "600", 13 | subsets: ["latin"], 14 | }); 15 | 16 | export const LandingNavbar = () => { 17 | const { isSignedIn } = useAuth(); 18 | 19 | return ( 20 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /components/loader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 |
7 | logo 8 |
9 |

Witty AI is thinking...

10 |
11 | ); 12 | }; 13 | 14 | export default Loader; 15 | -------------------------------------------------------------------------------- /components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Menu } from "lucide-react"; 5 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 6 | import Sidebar from "@/components/sidebar"; 7 | import { useEffect, useState } from "react"; 8 | 9 | interface MobileSidebarProps { 10 | apiLimitCount: number; 11 | isPro: boolean; 12 | } 13 | 14 | const MobileSidebar = ({ 15 | apiLimitCount = 0, 16 | isPro = false, 17 | }: MobileSidebarProps) => { 18 | const [isMounted, setIsMounted] = useState(false); 19 | 20 | useEffect(() => { 21 | setIsMounted(true); 22 | }, []); 23 | 24 | if (!isMounted) { 25 | return null; 26 | } 27 | 28 | return ( 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default MobileSidebar; 43 | -------------------------------------------------------------------------------- /components/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import ProModal from "@/components/pro-modal"; 5 | 6 | export const ModalProvider = () => { 7 | const [isMounted, setIsMounted] = useState(false); 8 | 9 | useEffect(() => { 10 | setIsMounted(true); 11 | }, []); 12 | 13 | if (!isMounted) { 14 | return null; 15 | } 16 | 17 | return ( 18 | <> 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@clerk/nextjs"; 2 | import MobileSidebar from "@/components/mobile-sidebar"; 3 | import { getApiLimitCount } from "@/lib/api-limit"; 4 | import { checkSubscription } from "@/lib/subscription"; 5 | 6 | const Navbar = async () => { 7 | const apiLimitCount = await getApiLimitCount(); 8 | const isPro = await checkSubscription(); 9 | 10 | return ( 11 |
12 | 13 |
14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Navbar; 21 | -------------------------------------------------------------------------------- /components/pro-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogTitle, 10 | } from "@/components/ui/dialog"; 11 | import { useProModal } from "@/hooks/use-pro-modal"; 12 | import { Badge } from "@/components/ui/badge"; 13 | import { 14 | Check, 15 | Code, 16 | ImageIcon, 17 | MessageSquare, 18 | Music, 19 | VideoIcon, 20 | Zap, 21 | } from "lucide-react"; 22 | import { Card } from "./ui/card"; 23 | import { cn } from "@/lib/utils"; 24 | import { Button } from "./ui/button"; 25 | import axios from "axios"; 26 | import { useState } from "react"; 27 | import toast from "react-hot-toast"; 28 | 29 | const tools = [ 30 | { 31 | label: "Conversation", 32 | icon: MessageSquare, 33 | color: "text-yellow-500", 34 | bgColor: "bg-yellow-500/10", 35 | }, 36 | { 37 | label: "Music Generation", 38 | icon: Music, 39 | color: "text-fuchsia-500", 40 | bgColor: "bg-fuchsia-500/10", 41 | }, 42 | { 43 | label: "Image Generation", 44 | icon: ImageIcon, 45 | color: "text-teal-700", 46 | bgColor: "bg-teal-700/10", 47 | }, 48 | { 49 | label: "Video Generation", 50 | icon: VideoIcon, 51 | color: "text-lime-700", 52 | bgColor: "bg-lime-700/10", 53 | }, 54 | { 55 | label: "Code Generation", 56 | icon: Code, 57 | color: "text-blue-700", 58 | bgColor: "bg-blue-700/10", 59 | }, 60 | ]; 61 | 62 | const ProModal = () => { 63 | const proModal = useProModal(); 64 | const [loading, setLoading] = useState(false); 65 | 66 | const onSubscribe = async () => { 67 | try { 68 | setLoading(true); 69 | const response = await axios.get("/api/stripe"); 70 | 71 | window.location.href = response.data.url; 72 | } catch (error) { 73 | toast.error("Something went wrong"); 74 | } finally { 75 | setLoading(false); 76 | } 77 | }; 78 | 79 | return ( 80 | 81 | 82 | 83 | 84 |
85 | Upgrade to Witty AI Pro 86 | 87 | Pro 88 | 89 |
90 |
91 | 92 | {tools.map((tool) => ( 93 | 97 |
98 |
99 | 100 |
101 |
{tool.label}
102 |
103 | 104 |
105 | ))} 106 |
107 |
108 | 109 | 118 | 119 |
120 |
121 | ); 122 | }; 123 | 124 | export default ProModal; 125 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { usePathname } from "next/navigation"; 6 | import { Montserrat } from "next/font/google"; 7 | import { cn } from "@/lib/utils"; 8 | import { 9 | Code, 10 | ImageIcon, 11 | LayoutDashboard, 12 | MessageSquare, 13 | Music, 14 | Settings, 15 | VideoIcon, 16 | } from "lucide-react"; 17 | import { FreeCounter } from "@/components/free-counter"; 18 | 19 | const montserrat = Montserrat({ weight: "600", subsets: ["latin"] }); 20 | 21 | const routes = [ 22 | { 23 | label: "Dashboard", 24 | icon: LayoutDashboard, 25 | href: "/dashboard", 26 | color: "text-blue-500", 27 | }, 28 | { 29 | label: "Conversation", 30 | icon: MessageSquare, 31 | href: "/conversation", 32 | color: "text-yellow-500", 33 | }, 34 | { 35 | label: "Image Generation", 36 | icon: ImageIcon, 37 | href: "/image", 38 | color: "text-teal-700", 39 | }, 40 | { 41 | label: "Video Generation", 42 | icon: VideoIcon, 43 | href: "/video", 44 | color: "text-lime-700", 45 | }, 46 | { 47 | label: "Music Generation", 48 | icon: Music, 49 | href: "/music", 50 | color: "text-fuchsia-500", 51 | }, 52 | { 53 | label: "Code Generation", 54 | icon: Code, 55 | href: "/code", 56 | color: "text-blue-700", 57 | }, 58 | { 59 | label: "Settings", 60 | icon: Settings, 61 | href: "/settings", 62 | }, 63 | ]; 64 | 65 | interface SidebarProps { 66 | apiLimitCount: number; 67 | isPro: boolean; 68 | } 69 | 70 | const Sidebar = ({ apiLimitCount = 0, isPro = false }: SidebarProps) => { 71 | const pathname = usePathname(); 72 | 73 | return ( 74 |
75 |
76 | 77 |
78 | Logo 79 |
80 |

81 | Witty AI 82 |

83 | 84 |
85 | {routes.map((route) => ( 86 | 96 |
97 | 98 | {route.label} 99 |
100 | 101 | ))} 102 |
103 |
104 | 105 |
106 | ); 107 | }; 108 | 109 | export default Sidebar; 110 | -------------------------------------------------------------------------------- /components/subscription-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Zap } from "lucide-react"; 4 | import { Button } from "./ui/button"; 5 | import axios from "axios"; 6 | import { useState } from "react"; 7 | import toast from "react-hot-toast"; 8 | 9 | interface SubscriptionButtonProps { 10 | isPro: boolean; 11 | } 12 | 13 | export const SubscriptionButton = ({ 14 | isPro = false, 15 | }: SubscriptionButtonProps) => { 16 | const [loading, setLoading] = useState(false); 17 | 18 | const onClick = async () => { 19 | try { 20 | setLoading(true); 21 | const response = await axios.get("/api/stripe"); 22 | window.location.href = response.data.url; 23 | } catch (error) { 24 | toast.error("Something went wrong"); 25 | } finally { 26 | setLoading(false); 27 | } 28 | }; 29 | 30 | return ( 31 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /components/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "react-hot-toast"; 4 | 5 | const ToasterProvider = () => { 6 | return ; 7 | }; 8 | 9 | export default ToasterProvider; 10 | -------------------------------------------------------------------------------- /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 | premium: 19 | "bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500 text-primary-foreground border-0", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ); 27 | 28 | export interface BadgeProps 29 | extends React.HTMLAttributes, 30 | VariantProps {} 31 | 32 | function Badge({ className, variant, ...props }: BadgeProps) { 33 | return ( 34 |
35 | ); 36 | } 37 | 38 | export { Badge, badgeVariants }; 39 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | generate: "bg-blue-600 text-white hover:bg-blue-600/70", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | premium: 23 | "bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500 text-white border-0", 24 | }, 25 | size: { 26 | default: "h-10 px-4 py-2", 27 | sm: "h-9 rounded-md px-3", 28 | lg: "h-11 rounded-md px-8", 29 | icon: "h-10 w-10", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | } 37 | ); 38 | 39 | export interface ButtonProps 40 | extends React.ButtonHTMLAttributes, 41 | VariantProps { 42 | asChild?: boolean; 43 | } 44 | 45 | const Button = React.forwardRef( 46 | ({ className, variant, size, asChild = false, ...props }, ref) => { 47 | const Comp = asChild ? Slot : "button"; 48 | return ( 49 | 54 | ); 55 | } 56 | ); 57 | Button.displayName = "Button"; 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /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/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 = ({ 14 | className, 15 | ...props 16 | }: DialogPrimitive.DialogPortalProps) => ( 17 | 18 | ) 19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName 20 | 21 | const DialogOverlay = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 33 | )) 34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 35 | 36 | const DialogContent = React.forwardRef< 37 | React.ElementRef, 38 | React.ComponentPropsWithoutRef 39 | >(({ className, children, ...props }, ref) => ( 40 | 41 | 42 | 50 | {children} 51 | 52 | 53 | Close 54 | 55 | 56 | 57 | )) 58 | DialogContent.displayName = DialogPrimitive.Content.displayName 59 | 60 | const DialogHeader = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | DialogHeader.displayName = "DialogHeader" 73 | 74 | const DialogFooter = ({ 75 | className, 76 | ...props 77 | }: React.HTMLAttributes) => ( 78 |
85 | ) 86 | DialogFooter.displayName = "DialogFooter" 87 | 88 | const DialogTitle = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 100 | )) 101 | DialogTitle.displayName = DialogPrimitive.Title.displayName 102 | 103 | const DialogDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | DialogDescription.displayName = DialogPrimitive.Description.displayName 114 | 115 | export { 116 | Dialog, 117 | DialogTrigger, 118 | DialogContent, 119 | DialogHeader, 120 | DialogFooter, 121 | DialogTitle, 122 | DialogDescription, 123 | } 124 | -------------------------------------------------------------------------------- /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 |