├── .gitignore ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── error.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 │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ └── video │ │ │ ├── constants.ts │ │ │ └── page.tsx │ ├── error.tsx │ └── layout.tsx ├── (landing) │ ├── error.tsx │ ├── 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 ├── 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 ├── query-provider.tsx ├── sidebar.tsx ├── subscription-button.tsx ├── theme-provider.tsx ├── toaster-provider.tsx ├── tool-card.tsx ├── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── empty.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── progress.tsx │ ├── select.tsx │ ├── separator.tsx │ └── sheet.tsx └── user-avatar.tsx ├── constants.ts ├── hooks ├── use-messages.tsx └── use-pro-modal.ts ├── lib ├── api-limit.ts ├── prismadb.ts ├── stripe.ts ├── subscription.ts └── utils.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── empty.png ├── logo.png ├── next.svg └── vercel.svg ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | .next 4 | .env 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aarav Mishra 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 | # Genius-SaaS 2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 | ![Next.js Version](https://img.shields.io/badge/Next.js-13-green) 10 | ![React Version](https://img.shields.io/badge/React-18-blue) 11 | ![License](https://img.shields.io/badge/license-MIT-orange) 12 | 13 |
14 | 15 | ## Introduction 16 | 17 | [Genius](https://genius-saas.vercel.app) is a cutting-edge web application built using Next.js, React, and other modern [technologies](#technologies). It is designed to showcase the implementation of Artificial Intelligence (AI) features in a Software-as-a-Service (SaaS) application. This repository serves as a foundation for creating AI-powered SaaS products, with an emphasis on the user experience and seamless integration of AI capabilities. 18 | 19 | ## Table of Contents 20 | 21 | - [Features](#features) 22 | - [Getting Started](#getting-started) 23 | - [Prerequisites](#prerequisites) 24 | - [Installation](#installation) 25 | - [Usage](#usage) 26 | - [Configuration](#configuration) 27 | - [AI Integration](#ai-integration) 28 | - [Technologies](#technologies) 29 | - [Contributing](#contributing) 30 | - [License](#license) 31 | 32 | ## Features 33 | 34 | - **AI-driven Recommendations**: The application leverages AI algorithms to provide personalized recommendations to users based on their preferences and behavior. 35 | 36 | - **Dynamic Content Generation**: AI-powered content generation allows for creating dynamic and relevant content on the fly, optimizing user engagement. 37 | 38 | - **User Behavior Prediction**: The app uses AI models to predict user behavior, aiding in user retention and improving customer satisfaction. 39 | 40 | ## Getting Started 41 | 42 | Follow the instructions below to set up the project locally on your machine. 43 | 44 | ### Prerequisites 45 | 46 | - Node.js (v14 or higher) 47 | - npm or yarn 48 | 49 | ### Installation 50 | 51 | 1. Clone the repository to your local machine: 52 | 53 | 2. Navigate to the project directory: 54 | 55 | 3. Install the dependencies: 56 | 57 | ```bash 58 | npm install 59 | ``` 60 | 61 | or 62 | 63 | ```bash 64 | yarn install 65 | ``` 66 | 67 | ## Usage 68 | 69 | Create a `.env.local` file in the root directory of the project and add the following environment variables: 70 | 71 | ```bash 72 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_api_key 73 | CLERK_SECRET_KEY=your_secret_key 74 | 75 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 76 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 77 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard 78 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard 79 | 80 | OPENAI_API_KEY=your_api_key 81 | 82 | REPLICATE_API_TOKEN=your_api_token 83 | 84 | DATABASE_URL=your_database_connection_string 85 | 86 | STRIPE_API_KEY=your_stripe_api_key 87 | 88 | NEXT_PUBLIC_APP_URL=http://localhost:3000 89 | 90 | CRISP_WEBSITE_ID=your_crisp_website_id 91 | 92 | ``` 93 | 94 | Start the development server: 95 | 96 | ```bash 97 | npm run dev 98 | ``` 99 | 100 | or 101 | 102 | ```bash 103 | yarn dev 104 | ``` 105 | 106 | ## Screenshots 107 | Screenshot 2023-07-28 at 10 37 17 PM 108 | Screenshot 2023-07-28 at 11 01 59 PM 109 | 110 | ## Configuration 111 | 112 | The application uses [Clerk](https://clerk.dev/) for authentication and user management. You can sign up for a free Clerk account [here](https://dashboard.clerk.dev/). 113 | 114 | The app also uses [OpenAI](https://openai.com/) for AI-powered content generation. You can sign up for a free OpenAI account [here](https://beta.openai.com/). 115 | 116 | The app uses [Replicate](https://replicate.ai/) for AI model deployment. You can sign up for a free Replicate account [here](https://replicate.ai/). 117 | 118 | The app uses [Stripe](https://stripe.com/) for payment processing. You can sign up for a free Stripe account [here](https://dashboard.stripe.com/register). 119 | 120 | The app uses [Crisp](https://crisp.chat/) for customer support. You can sign up for a free Crisp account [here](https://crisp.chat/en/). 121 | 122 | ## AI Integration 123 | 124 | The application uses AI models to provide personalized recommendations to users based on their preferences and behavior. It also uses AI-powered content generation to create dynamic and relevant content on the fly, optimizing user engagement. The app uses AI models to predict user behavior, aiding in user retention and improving customer satisfaction. 125 | 126 | ## Technologies 127 | 128 | - [Next.js](https://nextjs.org/) 129 | - [React](https://reactjs.org/) 130 | - [Tailwind CSS](https://tailwindcss.com/) 131 | - [Clerk](https://clerk.dev/) 132 | - [OpenAI](https://openai.com/) 133 | - [Replicate](https://replicate.ai/) 134 | - [Stripe](https://stripe.com/) 135 | - [Crisp](https://crisp.chat/) 136 | - [Prisma](https://www.prisma.io/) 137 | 138 | ## Contributing 139 | 140 | Contributions are welcome! Feel free to open an issue or submit a pull request if you have any questions or suggestions. 141 | 142 | ## License 143 | 144 | Distributed under the MIT License. See `LICENSE` for more information. 145 | 146 | ## Bonus 147 | 148 | I hope this project provides a valuable resource for developers interested in exploring the potential of AI in SaaS applications. Happy coding! 🚀 149 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /app/(auth)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/ui/empty"; 4 | 5 | const Error = () => { 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default Error; 12 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ 2 | children 3 | }: { 4 | children: React.ReactNode; 5 | }) => { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | 13 | export default AuthLayout; -------------------------------------------------------------------------------- /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 | 3 | import axios from "axios"; 4 | import { Code } from "lucide-react"; 5 | import { useRouter } from "next/navigation"; 6 | import { ChatCompletionRequestMessage } from "openai"; 7 | import { useEffect, useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import { toast } from "react-hot-toast"; 10 | import ReactMarkdown from "react-markdown"; 11 | import * as z from "zod"; 12 | 13 | import { BotAvatar } from "@/components/bot-avatar"; 14 | import { Heading } from "@/components/heading"; 15 | import { Loader } from "@/components/loader"; 16 | import { Button } from "@/components/ui/button"; 17 | import { Empty } from "@/components/ui/empty"; 18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 19 | import { Input } from "@/components/ui/input"; 20 | import { UserAvatar } from "@/components/user-avatar"; 21 | import { useProModal } from "@/hooks/use-pro-modal"; 22 | import { cn } from "@/lib/utils"; 23 | import { zodResolver } from "@hookform/resolvers/zod"; 24 | 25 | import { Message } from "@prisma/client"; 26 | import dayjs from "dayjs"; 27 | import { formSchema } from "./constants"; 28 | 29 | const CodePage = () => { 30 | const router = useRouter(); 31 | const proModal = useProModal(); 32 | const [messages, setMessages] = useState([]); 33 | 34 | const form = useForm>({ 35 | resolver: zodResolver(formSchema), 36 | defaultValues: { 37 | prompt: "", 38 | }, 39 | }); 40 | 41 | const isLoading = form.formState.isSubmitting; 42 | 43 | const onSubmit = async (values: z.infer) => { 44 | try { 45 | const prompt: ChatCompletionRequestMessage = { role: "user", content: values.prompt }; 46 | const newMessages = [prompt]; 47 | 48 | const response = await axios.post("/api/code", { messages: newMessages }); 49 | await axios.put("/api/code", { botMessage: response.data.content, userMessage: values.prompt }); 50 | 51 | const userMessage = { 52 | createdAt: new Date(), 53 | id: Math.random().toString(), 54 | updatedAt: new Date(), 55 | content: values.prompt, 56 | role: "user", 57 | type: "code", 58 | } as Message; 59 | 60 | const botMessage = { 61 | createdAt: new Date(), 62 | id: Math.random().toString(), 63 | updatedAt: new Date(), 64 | content: response.data.content as string, 65 | role: "assistant", 66 | type: "code", 67 | } as Message; 68 | 69 | setMessages((current) => [botMessage, userMessage, ...current]); 70 | 71 | form.reset(); 72 | } catch (error: any) { 73 | if (error?.response?.status === 403) { 74 | proModal.onOpen(); 75 | } else { 76 | toast.error("Something went wrong."); 77 | } 78 | } finally { 79 | router.refresh(); 80 | } 81 | }; 82 | 83 | const fethMessages = async () => { 84 | try { 85 | const response = await axios.get("/api/code"); 86 | setMessages(response.data); 87 | } catch (error: any) { 88 | if (error?.response?.status === 403) { 89 | return; 90 | } else { 91 | if (process.env.NODE_ENV === "development") { 92 | console.log(error); 93 | } 94 | } 95 | } 96 | }; 97 | 98 | useEffect(() => { 99 | fethMessages(); 100 | }, []); 101 | 102 | return ( 103 |
104 | 111 |
112 |
113 |
114 | 129 | ( 132 | 133 | 134 | 140 | 141 | 142 | )} 143 | /> 144 | 147 | 148 | 149 |
150 |
151 | {isLoading && ( 152 |
153 | 154 |
155 | )} 156 | {messages.length === 0 && !isLoading && } 157 |
158 | {messages.map((message) => ( 159 |
166 | {message.role === "user" ? : } 167 | ( 170 |
171 |
172 | 											
173 | ), 174 | code: ({ node, ...props }) => , 175 | }} 176 | className="text-sm overflow-hidden leading-7" 177 | > 178 | {message.content || ""} 179 |
180 | {message?.createdAt && ( 181 |
182 | {dayjs(message.createdAt).format("MMM D, h:mm A")} 183 |
184 | )} 185 |
186 | ))} 187 |
188 |
189 |
190 |
191 | ); 192 | }; 193 | 194 | export default CodePage; 195 | -------------------------------------------------------------------------------- /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 | 3 | import axios from "axios"; 4 | import dayjs from "dayjs"; 5 | import { MessageSquare } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { ChatCompletionRequestMessage } from "openai"; 8 | import { useEffect, useState } from "react"; 9 | import { useForm } from "react-hook-form"; 10 | import { toast } from "react-hot-toast"; 11 | import * as z from "zod"; 12 | 13 | import { BotAvatar } from "@/components/bot-avatar"; 14 | import { Heading } from "@/components/heading"; 15 | import { Loader } from "@/components/loader"; 16 | import { Button } from "@/components/ui/button"; 17 | import { Empty } from "@/components/ui/empty"; 18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 19 | import { Input } from "@/components/ui/input"; 20 | import { UserAvatar } from "@/components/user-avatar"; 21 | import { useProModal } from "@/hooks/use-pro-modal"; 22 | import { cn } from "@/lib/utils"; 23 | import { zodResolver } from "@hookform/resolvers/zod"; 24 | 25 | import { Message } from "@prisma/client"; 26 | import { formSchema } from "./constants"; 27 | 28 | const ConversationPage = () => { 29 | const router = useRouter(); 30 | const proModal = useProModal(); 31 | const [messages, setMessages] = useState>([]); 32 | 33 | const form = useForm>({ 34 | resolver: zodResolver(formSchema), 35 | defaultValues: { 36 | prompt: "", 37 | }, 38 | }); 39 | 40 | const isLoading = form.formState.isSubmitting; 41 | 42 | const onSubmit = async (values: z.infer) => { 43 | try { 44 | const prompt: ChatCompletionRequestMessage = { role: "user", content: values.prompt }; 45 | const newMessages = [prompt]; 46 | 47 | const response = await axios.post("/api/conversation", { messages: newMessages }); 48 | // send user message and bot response to the server 49 | await axios.put("/api/conversation", { botMessage: response.data.content, userMessage: values.prompt }); 50 | 51 | const userMessage = { 52 | createdAt: new Date(), 53 | id: Math.random().toString(), 54 | updatedAt: new Date(), 55 | content: values.prompt, 56 | role: "user", 57 | type: "conversation", 58 | } as Message; 59 | 60 | const botMessage = { 61 | createdAt: new Date(), 62 | id: Math.random().toString(), 63 | updatedAt: new Date(), 64 | content: response.data.content as string, 65 | role: "assistant", 66 | type: "conversation", 67 | } as Message; 68 | 69 | setMessages((messages) => [botMessage, userMessage, ...messages] as Message[]); 70 | 71 | form.reset(); 72 | } catch (error: any) { 73 | if (error?.response?.status === 403) { 74 | proModal.onOpen(); 75 | } else { 76 | toast.error("Something went wrong."); 77 | console.log(error); 78 | } 79 | } finally { 80 | router.refresh(); 81 | } 82 | }; 83 | 84 | const fetchConversation = async () => { 85 | try { 86 | const response = await axios.get("/api/conversation"); 87 | console.log(response.data); 88 | setMessages(response.data); 89 | } catch (error: any) { 90 | if (error?.response?.status === 404) { 91 | return; 92 | } else { 93 | if (process.env.NODE_ENV === "development") { 94 | console.log(error); 95 | } 96 | } 97 | } finally { 98 | router.refresh(); 99 | } 100 | }; 101 | 102 | useEffect(() => { 103 | fetchConversation(); 104 | }, []); 105 | 106 | return ( 107 |
108 | 115 |
116 |
117 |
118 | 133 | ( 136 | 137 | 138 | 144 | 145 | 146 | )} 147 | /> 148 | 151 | 152 | 153 |
154 |
155 | {isLoading && ( 156 |
157 | 158 |
159 | )} 160 | {messages.length === 0 && !isLoading && } 161 |
162 | {messages.map((message) => ( 163 |
170 | {message.role === "user" ? : } 171 |

{message.content}

172 | {message?.createdAt && ( 173 |
174 | {dayjs(message.createdAt).format("MMM D, h:mm A")} 175 |
176 | )} 177 |
178 | ))} 179 |
180 |
181 |
182 |
183 | ); 184 | }; 185 | 186 | export default ConversationPage; 187 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ArrowRight } from "lucide-react"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | import { Card } from "@/components/ui/card"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | import { tools } from "@/constants"; 10 | 11 | export default function HomePage() { 12 | const router = useRouter(); 13 | 14 | return ( 15 |
16 |
17 |

18 | Explore the power of AI 19 |

20 |

21 | Chat with the smartest AI - Experience the power of AI 22 |

23 |
24 |
25 | {tools.map((tool) => ( 26 | router.push(tool.href)} key={tool.href} className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer"> 27 |
28 |
29 | 30 |
31 |
32 | {tool.label} 33 |
34 |
35 | 36 |
37 | ))} 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /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: "Photo 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 | 49 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import axios from "axios"; 5 | import { Download, ImageIcon } from "lucide-react"; 6 | import Image from "next/image"; 7 | import { useRouter } from "next/navigation"; 8 | import { useState } from "react"; 9 | import { useForm } from "react-hook-form"; 10 | import { toast } from "react-hot-toast"; 11 | import * as z from "zod"; 12 | 13 | import { Heading } from "@/components/heading"; 14 | import { Loader } from "@/components/loader"; 15 | import { Button } from "@/components/ui/button"; 16 | import { Card, CardFooter } from "@/components/ui/card"; 17 | import { Empty } from "@/components/ui/empty"; 18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 19 | import { Input } from "@/components/ui/input"; 20 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 21 | import { useProModal } from "@/hooks/use-pro-modal"; 22 | 23 | import { amountOptions, formSchema, resolutionOptions } from "./constants"; 24 | 25 | const PhotoPage = () => { 26 | const proModal = useProModal(); 27 | const router = useRouter(); 28 | const [photos, setPhotos] = useState([]); 29 | 30 | const form = useForm>({ 31 | resolver: zodResolver(formSchema), 32 | defaultValues: { 33 | prompt: "", 34 | amount: "1", 35 | resolution: "512x512", 36 | }, 37 | }); 38 | 39 | const isLoading = form.formState.isSubmitting; 40 | 41 | const onSubmit = async (values: z.infer) => { 42 | try { 43 | setPhotos([]); 44 | 45 | const response = await axios.post("/api/image", values); 46 | 47 | const urls = response.data.map((image: { url: string }) => image.url); 48 | 49 | setPhotos(urls); 50 | } catch (error: any) { 51 | if (error?.response?.status === 403) { 52 | proModal.onOpen(); 53 | } else { 54 | toast.error("Something went wrong."); 55 | } 56 | } finally { 57 | router.refresh(); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 | 70 |
71 |
72 | 87 | ( 90 | 91 | 92 | 98 | 99 | 100 | )} 101 | /> 102 | ( 106 | 107 | 126 | 127 | )} 128 | /> 129 | ( 133 | 134 | 153 | 154 | )} 155 | /> 156 | 159 | 160 | 161 | {isLoading && ( 162 |
163 | 164 |
165 | )} 166 | {photos.length === 0 && !isLoading && } 167 |
168 | {photos.map((src) => ( 169 | 170 |
171 | Generated 172 |
173 | 174 | 178 | 179 |
180 | ))} 181 |
182 |
183 |
184 | ); 185 | }; 186 | 187 | export default PhotoPage; 188 | -------------------------------------------------------------------------------- /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 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import axios from "axios"; 5 | import { Music } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import { toast } from "react-hot-toast"; 10 | import * as z from "zod"; 11 | 12 | import { Heading } from "@/components/heading"; 13 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 14 | import { Input } from "@/components/ui/input"; 15 | import { useProModal } from "@/hooks/use-pro-modal"; 16 | 17 | import { Loader } from "@/components/loader"; 18 | import { Button } from "@/components/ui/button"; 19 | import { Empty } from "@/components/ui/empty"; 20 | import { formSchema } from "./constants"; 21 | 22 | const MusicPage = () => { 23 | const proModal = useProModal(); 24 | const router = useRouter(); 25 | const [music, setMusic] = useState(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(formSchema), 29 | defaultValues: { 30 | prompt: "", 31 | }, 32 | }); 33 | 34 | const isLoading = form.formState.isSubmitting; 35 | 36 | const onSubmit = async (values: z.infer) => { 37 | try { 38 | setMusic(undefined); 39 | 40 | const response = await axios.post("/api/music", values); 41 | console.log(response); 42 | 43 | setMusic(response.data.audio); 44 | form.reset(); 45 | } catch (error: any) { 46 | if (error?.response?.status === 403) { 47 | proModal.onOpen(); 48 | } else { 49 | toast.error("Something went wrong."); 50 | } 51 | } finally { 52 | router.refresh(); 53 | } 54 | }; 55 | 56 | return ( 57 |
58 | 65 |
66 |
67 | 82 | ( 85 | 86 | 87 | 93 | 94 | 95 | )} 96 | /> 97 | 100 | 101 | 102 | {isLoading && ( 103 |
104 | 105 |
106 | )} 107 | {!music && !isLoading && } 108 | {music && ( 109 | 112 | )} 113 |
114 |
115 | ); 116 | }; 117 | 118 | export default MusicPage; 119 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/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)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Settings } from "lucide-react"; 2 | 3 | import { Heading } from "@/components/heading"; 4 | import { SubscriptionButton } from "@/components/subscription-button"; 5 | import { checkSubscription } from "@/lib/subscription"; 6 | 7 | const SettingsPage = async () => { 8 | const isPro = await checkSubscription(); 9 | 10 | return ( 11 |
12 | 19 |
20 |
21 | {isPro ? "You are currently on a Pro plan." : "You are currently on a free plan."} 22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | export default SettingsPage; 30 | 31 | -------------------------------------------------------------------------------- /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: "Prompt is required." 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import axios from "axios"; 5 | import { FileAudio } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import { toast } from "react-hot-toast"; 10 | import * as z from "zod"; 11 | 12 | import { Heading } from "@/components/heading"; 13 | import { Loader } from "@/components/loader"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Empty } from "@/components/ui/empty"; 16 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 17 | import { Input } from "@/components/ui/input"; 18 | import { useProModal } from "@/hooks/use-pro-modal"; 19 | 20 | import { formSchema } from "./constants"; 21 | 22 | const VideoPage = () => { 23 | const router = useRouter(); 24 | const proModal = useProModal(); 25 | const [video, setVideo] = useState(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(formSchema), 29 | defaultValues: { 30 | prompt: "", 31 | }, 32 | }); 33 | 34 | const isLoading = form.formState.isSubmitting; 35 | 36 | const onSubmit = async (values: z.infer) => { 37 | try { 38 | setVideo(undefined); 39 | 40 | const response = await axios.post("/api/video", values); 41 | 42 | setVideo(response.data[0]); 43 | form.reset(); 44 | } catch (error: any) { 45 | if (error?.response?.status === 403) { 46 | proModal.onOpen(); 47 | } else { 48 | toast.error("Something went wrong."); 49 | } 50 | } finally { 51 | router.refresh(); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 | 64 |
65 |
66 | 81 | ( 84 | 85 | 86 | 92 | 93 | 94 | )} 95 | /> 96 | 99 | 100 | 101 | {isLoading && ( 102 |
103 | 104 |
105 | )} 106 | {!video && !isLoading && } 107 | {video && ( 108 | 111 | )} 112 |
113 |
114 | ); 115 | }; 116 | 117 | export default VideoPage; 118 | -------------------------------------------------------------------------------- /app/(dashboard)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/ui/empty"; 4 | 5 | const Error = () => { 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default Error; 12 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar"; 2 | import { Sidebar } from "@/components/sidebar"; 3 | import { checkSubscription } from "@/lib/subscription"; 4 | import { getApiLimitCount } from "@/lib/api-limit"; 5 | 6 | const DashboardLayout = async ({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) => { 11 | const apiLimitCount = await getApiLimitCount(); 12 | const isPro = await checkSubscription(); 13 | 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | 21 | {children} 22 |
23 |
24 | ); 25 | } 26 | 27 | export default DashboardLayout; 28 | -------------------------------------------------------------------------------- /app/(landing)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/ui/empty"; 4 | 5 | const Error = () => { 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default Error; 12 | -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | const LandingLayout = ({ 2 | children 3 | }: { 4 | children: React.ReactNode; 5 | }) => { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | } 14 | 15 | export default LandingLayout; -------------------------------------------------------------------------------- /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 | 5 | const LandingPage = () => { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default LandingPage; 16 | -------------------------------------------------------------------------------- /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 prismadb from "@/lib/prismadb"; 6 | 7 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 8 | import { checkSubscription } from "@/lib/subscription"; 9 | 10 | const configuration = new Configuration({ 11 | apiKey: process.env.OPENAI_API_KEY, 12 | }); 13 | 14 | const openai = new OpenAIApi(configuration); 15 | 16 | const instructionMessage: ChatCompletionRequestMessage = { 17 | role: "system", 18 | content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.", 19 | }; 20 | 21 | export async function POST(req: Request) { 22 | try { 23 | const { userId } = auth(); 24 | const body = await req.json(); 25 | const { messages } = body; 26 | 27 | if (!userId) { 28 | return new NextResponse("Unauthorized", { status: 401 }); 29 | } 30 | 31 | if (!configuration.apiKey) { 32 | return new NextResponse("OpenAI API Key not configured.", { status: 500 }); 33 | } 34 | 35 | if (!messages) { 36 | return new NextResponse("Messages are required", { status: 400 }); 37 | } 38 | 39 | const freeTrial = await checkApiLimit(); 40 | const isPro = await checkSubscription(); 41 | 42 | if (!freeTrial && !isPro) { 43 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 44 | } 45 | 46 | const response = await openai.createChatCompletion({ 47 | model: "gpt-3.5-turbo", 48 | messages: [instructionMessage, ...messages], 49 | }); 50 | 51 | if (!isPro) { 52 | await incrementApiLimit(); 53 | } 54 | 55 | return NextResponse.json(response.data.choices[0].message); 56 | } catch (error) { 57 | console.log("[CODE_ERROR]", error); 58 | return new NextResponse("Internal Error", { status: 500 }); 59 | } 60 | } 61 | 62 | export async function GET() { 63 | try { 64 | const { userId } = auth(); 65 | 66 | if (!userId) { 67 | return new NextResponse("Unauthorized", { status: 401 }); 68 | } 69 | 70 | const freeTrial = await checkApiLimit(); 71 | const isPro = await checkSubscription(); 72 | 73 | if (!freeTrial && !isPro) { 74 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 }); 75 | } 76 | 77 | const messages = await prismadb.message.findMany({ 78 | where: { 79 | userId, 80 | type: "code", 81 | }, 82 | orderBy: { 83 | createdAt: "desc", 84 | }, 85 | }); 86 | 87 | return new NextResponse(JSON.stringify(messages)); 88 | } catch (error) { 89 | console.log("[CODE_ERROR]", error); 90 | return new NextResponse("Internal Error", { status: 500 }); 91 | } 92 | } 93 | 94 | export async function PUT(req: Request) { 95 | try { 96 | const { userId } = auth(); 97 | const body = await req.json(); 98 | const { userMessage, botMessage } = body; 99 | 100 | if (!userId) { 101 | return new NextResponse("Unauthorized", { status: 401 }); 102 | } 103 | 104 | if (!userMessage || !botMessage) { 105 | return new NextResponse("Both user message and bot message is required.", { status: 400 }); 106 | } 107 | 108 | const freeTrial = await checkApiLimit(); 109 | const isPro = await checkSubscription(); 110 | 111 | if (!freeTrial && !isPro) { 112 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 }); 113 | } 114 | 115 | await prismadb.message.create({ 116 | data: { 117 | userId, 118 | type: "code", 119 | content: userMessage, 120 | role: "user", 121 | }, 122 | }); 123 | 124 | await prismadb.message.create({ 125 | data: { 126 | userId, 127 | type: "code", 128 | content: botMessage, 129 | role: "assistant", 130 | }, 131 | }); 132 | 133 | return new NextResponse(null, { 134 | status: 200, 135 | }); 136 | } catch (error) { 137 | console.log("[CODE_ERROR]", error); 138 | return new NextResponse("Internal Error", { status: 500 }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /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 prismadb from "@/lib/prismadb"; 6 | 7 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 8 | import { checkSubscription } from "@/lib/subscription"; 9 | 10 | const configuration = new Configuration({ 11 | apiKey: process.env.OPENAI_API_KEY, 12 | }); 13 | 14 | const openai = new OpenAIApi(configuration); 15 | 16 | export async function POST(req: Request) { 17 | try { 18 | const { userId } = auth(); 19 | const body = await req.json(); 20 | const { messages } = body; 21 | 22 | if (!userId) { 23 | return new NextResponse("Unauthorized", { status: 401 }); 24 | } 25 | 26 | if (!configuration.apiKey) { 27 | return new NextResponse("OpenAI API Key not configured.", { status: 500 }); 28 | } 29 | 30 | if (!messages) { 31 | return new NextResponse("Messages are required", { status: 400 }); 32 | } 33 | 34 | const freeTrial = await checkApiLimit(); 35 | const isPro = await checkSubscription(); 36 | 37 | if (!freeTrial && !isPro) { 38 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 39 | } 40 | 41 | const response = await openai.createChatCompletion({ 42 | model: "gpt-3.5-turbo", 43 | messages, 44 | }); 45 | 46 | if (!isPro) { 47 | await incrementApiLimit(); 48 | } 49 | 50 | return NextResponse.json(response.data.choices[0].message); 51 | } catch (error) { 52 | console.log("[CONVERSATION_ERROR]", error); 53 | return new NextResponse("Internal Error", { status: 500 }); 54 | } 55 | } 56 | 57 | export async function GET() { 58 | try { 59 | const { userId } = auth(); 60 | 61 | if (!userId) { 62 | return new NextResponse("Unauthorized", { status: 401 }); 63 | } 64 | 65 | const freeTrial = await checkApiLimit(); 66 | const isPro = await checkSubscription(); 67 | 68 | if (!freeTrial && !isPro) { 69 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 }); 70 | } 71 | 72 | const messages = await prismadb.message.findMany({ 73 | where: { 74 | userId, 75 | type: "conversation", 76 | }, 77 | orderBy: { 78 | createdAt: "desc", 79 | }, 80 | }); 81 | 82 | return new NextResponse(JSON.stringify(messages)); 83 | } catch (error) { 84 | console.log("[CODE_ERROR]", error); 85 | return new NextResponse("Internal Error", { status: 500 }); 86 | } 87 | } 88 | 89 | export async function PUT(req: Request) { 90 | try { 91 | const { userId } = auth(); 92 | const body = await req.json(); 93 | const { userMessage, botMessage } = body; 94 | 95 | if (!userId) { 96 | return new NextResponse("Unauthorized", { status: 401 }); 97 | } 98 | 99 | if (!userMessage || !botMessage) { 100 | return new NextResponse("Both user message and bot message is required.", { status: 400 }); 101 | } 102 | 103 | const freeTrial = await checkApiLimit(); 104 | const isPro = await checkSubscription(); 105 | 106 | if (!freeTrial && !isPro) { 107 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 }); 108 | } 109 | 110 | await prismadb.message.create({ 111 | data: { 112 | userId, 113 | type: "conversation", 114 | content: userMessage, 115 | role: "user", 116 | }, 117 | }); 118 | 119 | await prismadb.message.create({ 120 | data: { 121 | userId, 122 | type: "conversation", 123 | content: botMessage, 124 | role: "assistant", 125 | }, 126 | }); 127 | 128 | return new NextResponse(null, { 129 | status: 200, 130 | }); 131 | } catch (error) { 132 | console.log("[CODE_ERROR]", error); 133 | return new NextResponse("Internal Error", { status: 500 }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /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 { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 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( 15 | req: Request 16 | ) { 17 | try { 18 | const { userId } = auth(); 19 | const body = await req.json(); 20 | const { prompt, amount = 1, resolution = "512x512" } = body; 21 | 22 | if (!userId) { 23 | return new NextResponse("Unauthorized", { status: 401 }); 24 | } 25 | 26 | if (!configuration.apiKey) { 27 | return new NextResponse("OpenAI API Key not configured.", { status: 500 }); 28 | } 29 | 30 | if (!prompt) { 31 | return new NextResponse("Prompt is required", { status: 400 }); 32 | } 33 | 34 | if (!amount) { 35 | return new NextResponse("Amount is required", { status: 400 }); 36 | } 37 | 38 | if (!resolution) { 39 | return new NextResponse("Resolution is required", { status: 400 }); 40 | } 41 | 42 | const freeTrial = await checkApiLimit(); 43 | const isPro = await checkSubscription(); 44 | 45 | if (!freeTrial && !isPro) { 46 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 47 | } 48 | 49 | const response = await openai.createImage({ 50 | prompt, 51 | n: parseInt(amount, 10), 52 | size: resolution, 53 | }); 54 | 55 | if (!isPro) { 56 | await incrementApiLimit(); 57 | } 58 | 59 | return NextResponse.json(response.data.data); 60 | } catch (error) { 61 | console.log('[IMAGE_ERROR]', error); 62 | return new NextResponse("Internal Error", { status: 500 }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /app/api/music/route.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { incrementApiLimit, 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( 13 | req: Request 14 | ) { 15 | try { 16 | const { userId } = auth(); 17 | const body = await req.json(); 18 | const { prompt } = body; 19 | 20 | if (!userId) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | if (!prompt) { 25 | return new NextResponse("Prompt is required", { status: 400 }); 26 | } 27 | 28 | const freeTrial = await checkApiLimit(); 29 | const isPro = await checkSubscription(); 30 | 31 | if (!freeTrial && !isPro) { 32 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 33 | } 34 | 35 | const response = await replicate.run( 36 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", 37 | { 38 | input: { 39 | prompt_a: prompt 40 | } 41 | } 42 | ); 43 | 44 | if (!isPro) { 45 | await incrementApiLimit(); 46 | } 47 | 48 | return NextResponse.json(response); 49 | } catch (error) { 50 | console.log('[MUSIC_ERROR]', error); 51 | return new NextResponse("Internal Error", { status: 500 }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /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: "Genius 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 Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { incrementApiLimit, 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( 13 | req: Request 14 | ) { 15 | try { 16 | const { userId } = auth(); 17 | const body = await req.json(); 18 | const { prompt } = body; 19 | 20 | if (!userId) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | if (!prompt) { 25 | return new NextResponse("Prompt is required", { status: 400 }); 26 | } 27 | 28 | const freeTrial = await checkApiLimit(); 29 | const isPro = await checkSubscription(); 30 | 31 | if (!freeTrial && !isPro) { 32 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 33 | } 34 | 35 | const response = await replicate.run( 36 | "anotherjesse/zeroscope-v2-xl:71996d331e8ede8ef7bd76eba9fae076d31792e4ddf4ad057779b443d6aea62f", 37 | { 38 | input: { 39 | prompt, 40 | } 41 | } 42 | ); 43 | 44 | if (!isPro) { 45 | await incrementApiLimit(); 46 | } 47 | 48 | return NextResponse.json(response); 49 | } catch (error) { 50 | console.log('[VIDEO_ERROR]', error); 51 | return new NextResponse("Internal Error", { status: 500 }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import { NextResponse } from "next/server"; 3 | import Stripe from "stripe"; 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(body, signature, process.env.STRIPE_WEBHOOK_SECRET!); 16 | } catch (error: any) { 17 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); 18 | } 19 | 20 | const session = event.data.object as Stripe.Checkout.Session; 21 | 22 | if (event.type === "checkout.session.completed") { 23 | const subscription = await stripe.subscriptions.retrieve(session.subscription as string); 24 | 25 | if (!session?.metadata?.userId) { 26 | return new NextResponse("User id is required", { status: 400 }); 27 | } 28 | 29 | await prismadb.userSubscription.create({ 30 | data: { 31 | userId: session?.metadata?.userId, 32 | stripeSubscriptionId: subscription.id, 33 | stripeCustomerId: subscription.customer as string, 34 | stripePriceId: subscription.items.data[0].price.id, 35 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), 36 | }, 37 | }); 38 | } 39 | 40 | if (event.type === "invoice.payment_succeeded") { 41 | const subscription = await stripe.subscriptions.retrieve(session.subscription as string); 42 | 43 | await prismadb.userSubscription.update({ 44 | where: { 45 | stripeSubscriptionId: subscription.id, 46 | }, 47 | data: { 48 | stripePriceId: subscription.items.data[0].price.id, 49 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), 50 | }, 51 | }); 52 | } 53 | 54 | return new NextResponse(null, { status: 200 }); 55 | } 56 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devaaravmishra/genius-saas/e205eccd6cef0bc13939e15d1447ab492f220b0d/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: 20 14.3% 4.1%; 15 | 16 | --muted: 60 4.8% 95.9%; 17 | --muted-foreground: 25 5.3% 44.7%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 20 14.3% 4.1%; 21 | 22 | --card: 0 0% 100%; 23 | --card-foreground: 20 14.3% 4.1%; 24 | 25 | --border: 20 5.9% 90%; 26 | --input: 20 5.9% 90%; 27 | 28 | --primary: 248 90% 66%; 29 | --primary-foreground: 60 9.1% 97.8%; 30 | 31 | --secondary: 60 4.8% 95.9%; 32 | --secondary-foreground: 24 9.8% 10%; 33 | 34 | --accent: 60 4.8% 95.9%; 35 | --accent-foreground: 24 9.8% 10%; 36 | 37 | --destructive: 0 84.2% 60.2%; 38 | --destructive-foreground: 60 9.1% 97.8%; 39 | 40 | --ring: 248 90% 66%; 41 | 42 | --radius: 0.5rem; 43 | } 44 | 45 | .dark { 46 | --background: 20 14.3% 4.1%; 47 | --foreground: 60 9.1% 97.8%; 48 | 49 | --muted: 12 6.5% 15.1%; 50 | --muted-foreground: 24 5.4% 63.9%; 51 | 52 | --popover: 20 14.3% 4.1%; 53 | --popover-foreground: 60 9.1% 97.8%; 54 | 55 | --card: 20 14.3% 4.1%; 56 | --card-foreground: 60 9.1% 97.8%; 57 | 58 | --border: 12 6.5% 15.1%; 59 | --input: 12 6.5% 15.1%; 60 | 61 | --primary: 60 9.1% 97.8%; 62 | --primary-foreground: 24 9.8% 10%; 63 | 64 | --secondary: 12 6.5% 15.1%; 65 | --secondary-foreground: 60 9.1% 97.8%; 66 | 67 | --accent: 12 6.5% 15.1%; 68 | --accent-foreground: 60 9.1% 97.8%; 69 | 70 | --destructive: 0 62.8% 30.6%; 71 | --destructive-foreground: 0 85.7% 97.3%; 72 | 73 | --ring: 12 6.5% 15.1%; 74 | } 75 | } 76 | 77 | @layer base { 78 | * { 79 | @apply border-border; 80 | } 81 | body { 82 | @apply bg-background text-foreground; 83 | } 84 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider } from "@clerk/nextjs"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | 5 | import { CrispProvider } from "@/components/crisp-provider"; 6 | import { ModalProvider } from "@/components/modal-provider"; 7 | import { QueryProvider } from "@/components/query-provider"; 8 | import { ToasterProvider } from "@/components/toaster-provider"; 9 | 10 | import "./globals.css"; 11 | 12 | const font = Inter({ subsets: ["latin"] }); 13 | 14 | export const metadata: Metadata = { 15 | title: "Genius", 16 | description: "AI Platform", 17 | }; 18 | 19 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 20 | return ( 21 | 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 "@/components/ui/avatar"; 2 | 3 | export const BotAvatar = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /components/crisp-chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Crisp } from "crisp-sdk-web"; 4 | import { useEffect } from "react"; 5 | 6 | export const CrispChat = () => { 7 | useEffect(() => { 8 | Crisp.configure("afa20cf2-fe7d-446f-9e2d-81a1e6ab0299"); 9 | }, []); 10 | 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /components/crisp-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CrispChat } from "@/components/crisp-chat"; 4 | 5 | export const CrispProvider = () => { 6 | return 7 | }; 8 | -------------------------------------------------------------------------------- /components/free-counter.tsx: -------------------------------------------------------------------------------- 1 | import { Zap } from "lucide-react"; 2 | import { useEffect, useState } from "react"; 3 | 4 | import { MAX_FREE_COUNTS } from "@/constants"; 5 | import { Card, CardContent } from "@/components/ui/card"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Progress } from "@/components/ui/progress"; 8 | import { useProModal } from "@/hooks/use-pro-modal"; 9 | 10 | export const FreeCounter = ({ 11 | isPro = false, 12 | apiLimitCount = 0, 13 | }: { 14 | isPro: boolean, 15 | apiLimitCount: number 16 | }) => { 17 | const [mounted, setMounted] = useState(false); 18 | const proModal = useProModal(); 19 | 20 | useEffect(() => { 21 | setMounted(true); 22 | }, []); 23 | 24 | if (!mounted) { 25 | return null; 26 | } 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 | 42 |
43 | 47 |
48 |
49 |
50 | ) 51 | } -------------------------------------------------------------------------------- /components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "lucide-react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface HeadingProps { 6 | title: string; 7 | description: string; 8 | icon: Icon; 9 | iconColor?: string; 10 | bgColor?: string; 11 | } 12 | 13 | export const Heading = ({ 14 | title, 15 | description, 16 | icon: Icon, 17 | iconColor, 18 | bgColor, 19 | }: HeadingProps) => { 20 | return ( 21 | <> 22 |
23 |
24 | 25 |
26 |
27 |

{title}

28 |

29 | {description} 30 |

31 |
32 |
33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /components/landing-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | 5 | const testimonials = [ 6 | { 7 | name: "Joel", 8 | avatar: "J", 9 | title: "Software Engineer", 10 | description: "This is the best application I've ever used!", 11 | }, 12 | { 13 | name: "Antonio", 14 | avatar: "A", 15 | title: "Designer", 16 | description: "I use this daily for generating new photos!", 17 | }, 18 | { 19 | name: "Mark", 20 | avatar: "M", 21 | title: "CEO", 22 | description: "This app has changed my life, cannot imagine working without it!", 23 | }, 24 | { 25 | name: "Mary", 26 | avatar: "M", 27 | title: "CFO", 28 | description: "The best in class, definitely worth the premium subscription!", 29 | }, 30 | ]; 31 | 32 | export const LandingContent = () => { 33 | return ( 34 |
35 |

Testimonials

36 |
37 | {testimonials.map((item) => ( 38 | 39 | 40 | 41 |
42 |

{item.name}

43 |

{item.title}

44 |
45 |
46 | 47 | {item.description} 48 | 49 |
50 |
51 | ))} 52 |
53 |
54 | ) 55 | } -------------------------------------------------------------------------------- /components/landing-hero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TypewriterComponent from "typewriter-effect"; 4 | import Link from "next/link"; 5 | import { useAuth } from "@clerk/nextjs"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | 9 | export const LandingHero = () => { 10 | const { isSignedIn } = useAuth(); 11 | 12 | return ( 13 |
14 |
15 |

The Best AI Tool for

16 |
17 | 29 |
30 |
31 |
32 | Create content using AI 10x faster. 33 |
34 |
35 | 36 | 39 | 40 |
41 |
42 | No credit card required. 43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /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({ weight: '600', subsets: ['latin'] }); 12 | 13 | export const LandingNavbar = () => { 14 | const { isSignedIn } = useAuth(); 15 | 16 | return ( 17 | 34 | ) 35 | } -------------------------------------------------------------------------------- /components/loader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | 3 | export const Loader = () => { 4 | return ( 5 |
6 |
7 | Logo 12 |
13 |

14 | Genius is thinking... 15 |

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Menu } from "lucide-react"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 8 | import { Sidebar } from "@/components/sidebar"; 9 | 10 | export const MobileSidebar = ({ 11 | apiLimitCount = 0, 12 | isPro = false 13 | }: { 14 | apiLimitCount: number; 15 | isPro: boolean; 16 | }) => { 17 | const [isMounted, setIsMounted] = useState(false); 18 | 19 | useEffect(() => { 20 | setIsMounted(true); 21 | }, []); 22 | 23 | if (!isMounted) { 24 | return null; 25 | } 26 | 27 | return ( 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /components/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import { ProModal } from "@/components/pro-modal"; 6 | 7 | export const ModalProvider = () => { 8 | const [isMounted, setIsMounted] = useState(false); 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) { 15 | return null; 16 | } 17 | 18 | return ( 19 | <> 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@clerk/nextjs"; 2 | 3 | import { MobileSidebar } from "@/components/mobile-sidebar"; 4 | import { getApiLimitCount } from "@/lib/api-limit"; 5 | import { checkSubscription } from "@/lib/subscription"; 6 | 7 | const Navbar = async () => { 8 | const apiLimitCount = await getApiLimitCount(); 9 | const isPro = await checkSubscription(); 10 | 11 | return ( 12 |
13 | 14 |
15 | 16 |
17 |
18 | ); 19 | } 20 | 21 | export default Navbar; -------------------------------------------------------------------------------- /components/pro-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Check, Cloud, Zap } from "lucide-react"; 5 | import { useState } from "react"; 6 | import { toast } from "react-hot-toast"; 7 | 8 | import { Badge } from "@/components/ui/badge"; 9 | import { Button } from "@/components/ui/button"; 10 | import { Card } from "@/components/ui/card"; 11 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 12 | import { tools } from "@/constants"; 13 | import { useProModal } from "@/hooks/use-pro-modal"; 14 | import { cn } from "@/lib/utils"; 15 | 16 | export const ProModal = () => { 17 | const proModal = useProModal(); 18 | const [loading, setLoading] = useState(false); 19 | 20 | const onSubscribe = async () => { 21 | try { 22 | setLoading(true); 23 | const response = await axios.get("/api/stripe"); 24 | 25 | window.location.href = response.data.url; 26 | } catch (error) { 27 | toast.error("Something went wrong"); 28 | } finally { 29 | setLoading(false); 30 | } 31 | }; 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 |
39 | Upgrade to Genius 40 | 41 | pro 42 | 43 |
44 |
45 | 46 | {tools.map((tool) => ( 47 | 48 |
49 |
50 | 51 |
52 |
{tool.label}
53 |
54 | 55 |
56 | ))} 57 | 58 |
59 |
60 | 61 |
62 |
Cloud Storage
63 | Coming Soon 64 |
65 | 66 |
67 |
68 |
69 | 70 | 74 | 75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /components/query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import { useEffect, useState } from "react"; 6 | 7 | export const QueryProvider = () => { 8 | const [isMounted, setIsMounted] = useState(false); 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) { 15 | return null; 16 | } 17 | 18 | const queryClient = new QueryClient(); 19 | return ( 20 | <> 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { Montserrat } from 'next/font/google' 6 | import { Code, ImageIcon, LayoutDashboard, MessageSquare, Music, Settings, VideoIcon } from "lucide-react"; 7 | import { usePathname } from "next/navigation"; 8 | 9 | import { cn } from "@/lib/utils"; 10 | import { FreeCounter } from "@/components/free-counter"; 11 | 12 | const poppins = Montserrat ({ weight: '600', subsets: ['latin'] }); 13 | 14 | const routes = [ 15 | { 16 | label: 'Dashboard', 17 | icon: LayoutDashboard, 18 | href: '/dashboard', 19 | color: "text-sky-500" 20 | }, 21 | { 22 | label: 'Conversation', 23 | icon: MessageSquare, 24 | href: '/conversation', 25 | color: "text-violet-500", 26 | }, 27 | { 28 | label: 'Image Generation', 29 | icon: ImageIcon, 30 | color: "text-pink-700", 31 | href: '/image', 32 | }, 33 | { 34 | label: 'Video Generation', 35 | icon: VideoIcon, 36 | color: "text-orange-700", 37 | href: '/video', 38 | }, 39 | { 40 | label: 'Music Generation', 41 | icon: Music, 42 | color: "text-emerald-500", 43 | href: '/music', 44 | }, 45 | { 46 | label: 'Code Generation', 47 | icon: Code, 48 | color: "text-green-700", 49 | href: '/code', 50 | }, 51 | { 52 | label: 'Settings', 53 | icon: Settings, 54 | href: '/settings', 55 | }, 56 | ]; 57 | 58 | export const Sidebar = ({ 59 | apiLimitCount = 0, 60 | isPro = false 61 | }: { 62 | apiLimitCount: number; 63 | isPro: boolean; 64 | }) => { 65 | const pathname = usePathname(); 66 | 67 | return ( 68 |
69 |
70 | 71 |
72 | Logo 73 |
74 |

75 | Genius 76 |

77 | 78 |
79 | {routes.map((route) => ( 80 | 88 |
89 | 90 | {route.label} 91 |
92 | 93 | ))} 94 |
95 |
96 | 100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /components/subscription-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useState } from "react"; 5 | import { Zap } from "lucide-react"; 6 | import { toast } from "react-hot-toast"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | 10 | export const SubscriptionButton = ({ 11 | isPro = false 12 | }: { 13 | isPro: boolean; 14 | }) => { 15 | const [loading, setLoading] = useState(false); 16 | 17 | const onClick = async () => { 18 | try { 19 | setLoading(true); 20 | 21 | const response = await axios.get("/api/stripe"); 22 | 23 | window.location.href = response.data.url; 24 | } catch (error) { 25 | toast.error("Something went wrong"); 26 | } finally { 27 | setLoading(false); 28 | } 29 | }; 30 | 31 | return ( 32 | 36 | ) 37 | }; 38 | -------------------------------------------------------------------------------- /components/theme-provider.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/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "react-hot-toast" 4 | 5 | export const ToasterProvider = () => { 6 | return 7 | }; 8 | -------------------------------------------------------------------------------- /components/tool-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | 5 | import { 6 | Card, 7 | CardHeader, 8 | CardTitle, 9 | CardDescription, 10 | CardContent, 11 | } from "@/components/ui/card"; 12 | import { Badge } from "@/components/ui/badge"; 13 | import { useRouter } from "next/navigation"; 14 | 15 | interface ToolCardProps { 16 | src: string, 17 | href: string, 18 | title: string, 19 | description: string, 20 | premium?: boolean; 21 | } 22 | 23 | export const ToolCard = ({ 24 | src, 25 | href, 26 | title, 27 | description, 28 | premium 29 | }: ToolCardProps) => { 30 | const router = useRouter(); 31 | 32 | const onClick = () => { 33 | router.push(href); 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 |
41 | Icon 42 |
43 | {title} 44 |
45 | {description} 46 | {premium && ( 47 | 48 | pro 49 | 50 | )} 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /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.25 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: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0" 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | } 25 | ) 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ) 35 | } 36 | 37 | export { Badge, badgeVariants } 38 | -------------------------------------------------------------------------------- /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 | 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 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /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/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 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 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/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/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/empty.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | 4 | interface EmptyProps { 5 | label: string; 6 | } 7 | 8 | export const Empty = ({ 9 | label 10 | }: EmptyProps) => { 11 | return ( 12 |
13 |
14 | Empty 15 |
16 |

17 | {label} 18 |

19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /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 |