├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── npm-gulp.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (dashboard) │ ├── (routes) │ │ ├── code │ │ │ ├── constants.tsx │ │ │ └── page.tsx │ │ ├── conversation │ │ │ ├── constants.tsx │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── image │ │ │ ├── constants.tsx │ │ │ └── page.tsx │ │ ├── music │ │ │ ├── constants.tsx │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ └── video │ │ │ ├── constants.tsx │ │ │ └── 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 ├── 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.png ├── next.svg └── vercel.svg ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_api_key 2 | CLERK_SECRET_KEY=your_secret_key 3 | 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 6 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard 7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard 8 | 9 | OPENAI_API_KEY=your_api_key 10 | 11 | REPLICATE_API_TOKEN=your_api_token 12 | 13 | DATABASE_URL=your_database_connection_string 14 | 15 | STRIPE_API_KEY=your_stripe_api_key 16 | 17 | NEXT_PUBLIC_APP_URL=http://localhost:3000 18 | 19 | CRISP_WEBSITE_ID=your_crisp_website_id -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/npm-gulp.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS with Gulp 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Build 26 | run: | 27 | npm install 28 | gulp 29 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ayush Rathore 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 | # AI-SaaS - AI-Powered Software-as-a-Service Application 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | [![License](https://img.shields.io/badge/React.js-v18.2.0-blue.svg)](https://opensource.org/licenses/MIT) 5 | [![Next.js](https://img.shields.io/badge/Next.js-v13.4.12-blueviolet.svg)](https://nextjs.org/) 6 | [![OpenAI](https://img.shields.io/badge/OpenAI-API-yellow.svg)](https://openai.com/) 7 | [![Replicate](https://img.shields.io/badge/Replicate-v0.12.3-orange.svg)](https://replicate.ai/) 8 | [![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-v3.3.3-blue.svg)](https://tailwindcss.com/) 9 | [![Prisma](https://img.shields.io/badge/Prisma-v5.0.0-lightgrey.svg)](https://prisma.io/) 10 | [![Stripe](https://img.shields.io/badge/Stripe-API%20v12.16.0-green.svg)](https://stripe.com/) 11 | 12 | AI-SaaS is an advanced and adaptable Software-as-a-Service (SaaS) application that harnesses the capabilities of cutting-edge technologies, including Next.js, OpenAI, Replicate, Tailwind CSS, Prisma, and Stripe. The primary goal of this application is to empower users by offering AI-powered services that facilitate easy access and utilization of artificial intelligence in their projects and workflows. 13 | 14 | ## Features 15 | 16 | - **AI Services**: AI-SaaS provides an extensive array of AI services, including conversation, code generation, image generation, music generation, and video generation. These services are accessible through an intuitive and user-friendly interface. 17 | 18 | - **Next.js**: AI-SaaS is built on the Next.js framework, offering server-side rendering, routing, and other essential features out of the box. This ensures superior performance and search engine optimization (SEO) for the application. 19 | 20 | - **OpenAI Integration**: The application seamlessly integrates with OpenAI's powerful AI models and APIs, enabling users to leverage state-of-the-art AI capabilities. From generating human-like text to answering questions, AI-SaaS harnesses the full potential of OpenAI. 21 | 22 | - **Replicate**: AI-SaaS employs Replicate to enhance model reproducibility and facilitate seamless experimentation with various AI models. This ensures the AI models used in the application are robust and reliable. 23 | 24 | - **Tailwind CSS**: The UI of AI-SaaS is meticulously styled using Tailwind CSS, a utility-first CSS framework. This enables easy customization and consistent design throughout the application. 25 | 26 | - **Prisma**: The application utilizes Prisma as its ORM (Object-Relational Mapping) tool, simplifying database access and management. This enhances the efficiency of handling user data and preferences. 27 | 28 | - **Stripe Integration**: AI-SaaS seamlessly incorporates Stripe for secure and efficient payment processing. Users can subscribe to premium plans and access additional AI services based on their subscription level. 29 | 30 | ## Screenshots 31 | Screenshot 2023-07-30 at 11 33 59 AM 32 | Screenshot 2023-07-30 at 11 40 43 AM 33 | Screenshot 2023-07-30 at 11 41 18 AM 34 | Screenshot 2023-07-30 at 11 41 53 AM 35 | Screenshot 2023-07-30 at 11 42 23 AM 36 | Screenshot 2023-07-30 at 11 42 38 AM 37 | Screenshot 2023-07-30 at 11 42 50 AM 38 | 39 | ## Getting Started 40 | 41 | To run AI-SaaS locally, follow these steps: 42 | 43 | 1. **Clone the repository**: 44 | 45 | ```bash 46 | git clone https://github.com/ayusshrathore/ai-saas.git 47 | cd ai-saas 48 | ``` 49 | 50 | 2. **Install dependencies**: 51 | 52 | ```bash 53 | npm install 54 | # or 55 | yarn install 56 | ``` 57 | 58 | 3. **Configure environment variables**: 59 | 60 | To ensure proper functionality, set up environment variables for API keys and other sensitive information. Create a `.env` file in the root directory and populate it with the necessary variables. For reference, consult the `.env.example` file for the required variables. 61 | 62 | 4. **Run the application**: 63 | 64 | ```bash 65 | npm run dev 66 | # or 67 | yarn dev 68 | ``` 69 | 70 | The application should now be running locally at `http://localhost:3000`. 71 | 72 | ## Deployment 73 | 74 | AI-SaaS can be deployed to various hosting platforms that support Next.js applications. Before deployment, make sure you have configured the necessary environment variables for production. 75 | 76 | ## Contributions 77 | 78 | Contributions to AI-SaaS are highly appreciated! If you encounter any bugs or have suggestions for new features, please feel free to open an issue or submit a pull request. 79 | 80 | When contributing, adhere to the existing code style and include comprehensive test cases for new features. 81 | 82 | ## License 83 | 84 | AI-SaaS is released under the [MIT License](https://opensource.org/licenses/MIT). 85 | 86 | ## Acknowledgments 87 | 88 | AI-SaaS is built with the invaluable support and integration of several open-source projects and technologies. I extend my gratitude to the developers and maintainers of Next.js, OpenAI, Replicate, Tailwind CSS, Prisma, and Stripe for their significant contributions to the development community. 89 | [![Netlify Status](https://api.netlify.com/api/v1/badges/6da7f929-c69e-4c0a-9fd6-596a41129274/deploy-status)](https://app.netlify.com/sites/superlative-malabi-796b55/deploys) 90 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /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)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AuthLayout = ({ children }: { children: React.ReactNode }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default AuthLayout; 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/constants.tsx: -------------------------------------------------------------------------------- 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 { zodResolver } from "@hookform/resolvers/zod"; 4 | import axios from "axios"; 5 | import { Code } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { ChatCompletionRequestMessage } from "openai"; 8 | import { useState } from "react"; 9 | import { useForm } from "react-hook-form"; 10 | import ReactMarkdown from "react-markdown"; 11 | import * as z from "zod"; 12 | 13 | import { BotAvatar } from "@/components/bot-avatar"; 14 | import { Empty } from "@/components/empty"; 15 | import { Heading } from "@/components/heading"; 16 | import { Loader } from "@/components/loader"; 17 | import { Button } from "@/components/ui/button"; 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 { cn } from "@/lib/utils"; 22 | 23 | import useProModal from "@/hooks/use-pro-modal"; 24 | import { toast } from "react-hot-toast"; 25 | import { formSchema } from "./constants"; 26 | 27 | const CodePage = () => { 28 | const router = useRouter(); 29 | const proModal = useProModal(); 30 | const [messages, setMessages] = useState([]); 31 | 32 | const form = useForm>({ 33 | resolver: zodResolver(formSchema), 34 | defaultValues: { 35 | prompt: "", 36 | }, 37 | }); 38 | 39 | const isLoading = form.formState.isSubmitting; 40 | 41 | const onSubmit = async (values: z.infer) => { 42 | console.log(values); 43 | try { 44 | const userMessage: ChatCompletionRequestMessage = { 45 | role: "user", 46 | content: values.prompt, 47 | }; 48 | const newMessages = [...messages, userMessage]; 49 | 50 | const response = await axios.post("/api/code", { 51 | messages: newMessages, 52 | }); 53 | 54 | setMessages((current) => [...current, userMessage, response.data]); 55 | form.reset(); 56 | } catch (error: any) { 57 | console.log(error); 58 | if (error?.response?.status === 403) { 59 | proModal.onOpen(); 60 | } else { 61 | toast.error("Something went wrong."); 62 | } 63 | } finally { 64 | router.refresh(); 65 | } 66 | }; 67 | 68 | return ( 69 |
70 | 77 |
78 |
79 |
80 | 84 | ( 87 | 88 | 89 | 95 | 96 | 97 | )} 98 | /> 99 | 102 | 103 | 104 |
105 |
106 | {isLoading && ( 107 |
108 | 109 |
110 | )} 111 | {messages.length === 0 && !isLoading && } 112 |
113 | {messages.map((message, index) => ( 114 |
121 | {message.role === "user" ? : } 122 | ( 126 |
127 |
128 |                       
129 | ), 130 | code: ({ node, ...props }) => , 131 | }} 132 | > 133 | {message.content || ""} 134 |
135 |
136 | ))} 137 |
138 |
139 |
140 |
141 | ); 142 | }; 143 | 144 | export default CodePage; 145 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/constants.tsx: -------------------------------------------------------------------------------- 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 { zodResolver } from "@hookform/resolvers/zod"; 4 | import axios from "axios"; 5 | import { MessageSquare } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { ChatCompletionRequestMessage } from "openai"; 8 | import { useState } from "react"; 9 | import { useForm } from "react-hook-form"; 10 | import * as z from "zod"; 11 | 12 | import { BotAvatar } from "@/components/bot-avatar"; 13 | import { Empty } from "@/components/empty"; 14 | import { Heading } from "@/components/heading"; 15 | import { Loader } from "@/components/loader"; 16 | import { Button } from "@/components/ui/button"; 17 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 18 | import { Input } from "@/components/ui/input"; 19 | import { UserAvatar } from "@/components/user-avatar"; 20 | import { cn } from "@/lib/utils"; 21 | 22 | import useProModal from "@/hooks/use-pro-modal"; 23 | import { toast } from "react-hot-toast"; 24 | import { formSchema } from "./constants"; 25 | 26 | const ConversationPage = () => { 27 | const router = useRouter(); 28 | const proModal = useProModal(); 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 | console.log(values); 42 | try { 43 | const userMessage: ChatCompletionRequestMessage = { 44 | role: "user", 45 | content: values.prompt, 46 | }; 47 | const newMessages = [...messages, userMessage]; 48 | 49 | const response = await axios.post("/api/conversation", { 50 | messages: newMessages, 51 | }); 52 | 53 | setMessages((current) => [...current, userMessage, response.data]); 54 | form.reset(); 55 | } catch (error: any) { 56 | console.log(error); 57 | if (error?.response?.status === 403) { 58 | proModal.onOpen(); 59 | } else { 60 | toast.error("Something went wrong."); 61 | } 62 | } finally { 63 | router.refresh(); 64 | } 65 | }; 66 | 67 | return ( 68 |
69 | 76 |
77 |
78 |
79 | 83 | ( 86 | 87 | 88 | 94 | 95 | 96 | )} 97 | /> 98 | 101 | 102 | 103 |
104 |
105 | {isLoading && ( 106 |
107 | 108 |
109 | )} 110 | {messages.length === 0 && !isLoading && } 111 |
112 | {messages.map((message, index) => ( 113 |
120 | {message.role === "user" ? : } 121 |

{message.content}

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

61 | Explore the power of AI 62 |

63 |

64 | Prometheus is a platform that allows you to generate music, videos, 65 | and code using the power of AI. 66 |

67 |
68 |
69 | {tools.map((tool) => ( 70 | router.push(tool.href)} 72 | key={tool.href} 73 | className={ 74 | "p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer" 75 | } 76 | > 77 |
78 |
79 | 80 |
81 |
{tool.label}
82 |
83 | 84 |
85 | ))} 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default DashboardPage; 92 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/constants.tsx: -------------------------------------------------------------------------------- 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 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import axios from "axios"; 5 | import { Download, ImageIcon } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import * as z from "zod"; 10 | 11 | import { Empty } from "@/components/empty"; 12 | import { Heading } from "@/components/heading"; 13 | import { Loader } from "@/components/loader"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 16 | import { Input } from "@/components/ui/input"; 17 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 18 | 19 | import { Card, CardFooter } from "@/components/ui/card"; 20 | import useProModal from "@/hooks/use-pro-modal"; 21 | import Image from "next/image"; 22 | import { toast } from "react-hot-toast"; 23 | import { amountOptions, formSchema, resolutionOptions } from "./constants"; 24 | 25 | const ImagePage = () => { 26 | const router = useRouter(); 27 | const proModal = useProModal(); 28 | const [images, setImages] = 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 | console.log(values); 43 | try { 44 | setImages([]); 45 | const response = await axios.post("/api/image", values); 46 | 47 | const urls = response.data.map((image: { url: string }) => image.url); 48 | setImages(urls); 49 | 50 | form.reset(); 51 | } catch (error: any) { 52 | console.log(error); 53 | if (error?.response?.status === 403) { 54 | proModal.onOpen(); 55 | } else { 56 | toast.error("Something went wrong."); 57 | } 58 | } finally { 59 | router.refresh(); 60 | } 61 | }; 62 | 63 | return ( 64 |
65 | 72 |
73 |
74 |
75 | 79 | ( 82 | 83 | 84 | 90 | 91 | 92 | )} 93 | /> 94 | ( 98 | 99 | 113 | 114 | )} 115 | /> 116 | ( 120 | 121 | 135 | 136 | )} 137 | /> 138 | 141 | 142 | 143 |
144 |
145 | {isLoading && ( 146 |
147 | 148 |
149 | )} 150 | {images.length === 0 && !isLoading && } 151 |
152 | {images.map((image, index) => ( 153 | 154 |
155 | image 156 |
157 | 158 | 162 | 163 |
164 | ))} 165 |
166 |
167 |
168 |
169 | ); 170 | }; 171 | 172 | export default ImagePage; 173 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/constants.tsx: -------------------------------------------------------------------------------- 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)/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 * as z from "zod"; 10 | 11 | import { Empty } from "@/components/empty"; 12 | import { Heading } from "@/components/heading"; 13 | import { Loader } from "@/components/loader"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 16 | import { Input } from "@/components/ui/input"; 17 | 18 | import useProModal from "@/hooks/use-pro-modal"; 19 | import { toast } from "react-hot-toast"; 20 | import { formSchema } from "./constants"; 21 | 22 | const MusicPage = () => { 23 | const router = useRouter(); 24 | const proModal = useProModal(); 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 | console.log(values); 38 | try { 39 | setMusic(""); 40 | const response = await axios.post("/api/music"); 41 | setMusic(response.data.audio); 42 | 43 | form.reset(); 44 | } catch (error: any) { 45 | console.log(error); 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 |
68 | 72 | ( 75 | 76 | 77 | 83 | 84 | 85 | )} 86 | /> 87 | 90 | 91 | 92 |
93 |
94 | {isLoading && ( 95 |
96 | 97 |
98 | )} 99 | {!music && !isLoading && } 100 | {music && ( 101 | 104 | )} 105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | export default MusicPage; 112 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Heading } from "@/components/heading"; 2 | import { SubscriptionButton } from "@/components/subscription-button"; 3 | import { checkSubscription } from "@/lib/subscription"; 4 | import { Settings } from "lucide-react"; 5 | 6 | const SettingsPage = async () => { 7 | const isPro = await checkSubscription(); 8 | 9 | return ( 10 |
11 | 12 |
13 |
14 | {isPro ? "You are currently on a pro plan." : "You are currently on a free plan."} 15 |
16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default SettingsPage; 23 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/constants.tsx: -------------------------------------------------------------------------------- 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 { Video } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import * as z from "zod"; 10 | 11 | import { Empty } from "@/components/empty"; 12 | import { Heading } from "@/components/heading"; 13 | import { Loader } from "@/components/loader"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 16 | import { Input } from "@/components/ui/input"; 17 | 18 | import useProModal from "@/hooks/use-pro-modal"; 19 | import { toast } from "react-hot-toast"; 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 | console.log(values); 38 | try { 39 | setVideo(""); 40 | const response = await axios.post("/api/music"); 41 | setVideo(response.data.audio); 42 | 43 | form.reset(); 44 | } catch (error: any) { 45 | console.log(error); 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 |
68 | 72 | ( 75 | 76 | 77 | 83 | 84 | 85 | )} 86 | /> 87 | 90 | 91 | 92 |
93 |
94 | {isLoading && ( 95 |
96 | 97 |
98 | )} 99 | {!video && !isLoading && } 100 | {video && ( 101 | 104 | )} 105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | export default VideoPage; 112 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar"; 2 | import Sidebar from "@/components/sidebar"; 3 | import { getApiLimitCount } from "@/lib/api-limit"; 4 | import { checkSubscription } from "@/lib/subscription"; 5 | 6 | const DashboardLayout = async ({ children }: { children: React.ReactNode }) => { 7 | const apiLimitCount = await getApiLimitCount(); 8 | const isPro = await checkSubscription(); 9 | 10 | return ( 11 |
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 { LandingNabvbar } from "@/components/landing-navbar"; 4 | 5 | function LandingPage() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default LandingPage; 16 | -------------------------------------------------------------------------------- /app/api/code/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscription"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; 6 | 7 | const configuration = new Configuration({ 8 | apiKey: process.env.OPENAI_API_KEY, 9 | }); 10 | 11 | const openAi = new OpenAIApi(configuration); 12 | 13 | const instructionMessage: ChatCompletionRequestMessage = { 14 | role: "system", 15 | content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.", 16 | }; 17 | 18 | export async function POST(req: Request) { 19 | try { 20 | const { userId } = auth(); 21 | const body = await req.json(); 22 | const { messages } = body; 23 | 24 | if (!userId) { 25 | return new NextResponse("Unauthorized", { status: 401 }); 26 | } 27 | 28 | if (!configuration) { 29 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 30 | } 31 | 32 | if (!messages) { 33 | return new NextResponse("Missing messages", { status: 400 }); 34 | } 35 | 36 | const isAllowed = await checkApiLimit(); 37 | const isPro = await checkSubscription(); 38 | 39 | if (!isAllowed && !isPro) { 40 | return new NextResponse("API Limit Exceeded", { status: 403 }); 41 | } 42 | 43 | const response = await openAi.createChatCompletion({ 44 | model: "gpt-3.5-turbo", 45 | messages: [instructionMessage, ...messages], 46 | }); 47 | 48 | if (!isPro) { 49 | await increaseApiLimit(); 50 | } 51 | 52 | return NextResponse.json(response.data.choices[0].message, { status: 200 }); 53 | } catch (error) { 54 | console.log("[CODE_ERROR]", error); 55 | return new NextResponse("Internal Server Error", { status: 500 }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 { checkApiLimit, increaseApiLimit } 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) { 25 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 26 | } 27 | 28 | if (!messages) { 29 | return new NextResponse("Missing messages", { status: 400 }); 30 | } 31 | 32 | const isAllowed = await checkApiLimit(); 33 | const isPro = await checkSubscription(); 34 | 35 | if (!isAllowed && !isPro) { 36 | return new NextResponse("API Limit Exceeded", { 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, { status: 200 }); 49 | } catch (error) { 50 | console.log("[CONVERSATION_ERROR]", error); 51 | return new NextResponse("Internal Server Error", { status: 500 }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/api/image/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscription"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import { Configuration, OpenAIApi } from "openai"; 6 | 7 | const configuration = new Configuration({ 8 | apiKey: process.env.OPENAI_API_KEY, 9 | }); 10 | 11 | const openAi = new OpenAIApi(configuration); 12 | 13 | export async function POST(req: Request) { 14 | try { 15 | const { userId } = auth(); 16 | const body = await req.json(); 17 | const { prompt, amount = 1, resolution = "512x512" } = body; 18 | 19 | if (!userId) { 20 | return new NextResponse("Unauthorized", { status: 401 }); 21 | } 22 | 23 | if (!configuration) { 24 | return new NextResponse("OpenAI API Key not configured", { status: 500 }); 25 | } 26 | 27 | if (!prompt) { 28 | return new NextResponse("Missing prompt", { status: 400 }); 29 | } 30 | 31 | if (!amount) { 32 | return new NextResponse("Missing amount", { status: 400 }); 33 | } 34 | 35 | if (!resolution) { 36 | return new NextResponse("Missing resolution", { status: 400 }); 37 | } 38 | 39 | const isAllowed = await checkApiLimit(); 40 | const isPro = await checkSubscription(); 41 | 42 | if (!isAllowed && !isPro) { 43 | return new NextResponse("API Limit Exceeded", { status: 403 }); 44 | } 45 | 46 | const response = await openAi.createImage({ 47 | prompt, 48 | n: parseInt(amount, 10), 49 | size: resolution, 50 | }); 51 | 52 | if (!isPro) { 53 | await increaseApiLimit(); 54 | } 55 | 56 | return NextResponse.json(response.data.data, { status: 200 }); 57 | } catch (error) { 58 | console.log("[CONVERSATION_ERROR]", error); 59 | return new NextResponse("Internal Server Error", { status: 500 }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/api/music/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscription"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import Replicate from "replicate"; 6 | 7 | const replicate = new Replicate({ 8 | auth: process.env.REPLICATE_API_TOKEN!, 9 | }); 10 | 11 | export async function POST(req: Request) { 12 | try { 13 | const { userId } = auth(); 14 | const body = await req.json(); 15 | const { prompt } = body; 16 | 17 | if (!userId) { 18 | return new NextResponse("Unauthorized", { status: 401 }); 19 | } 20 | 21 | if (!prompt) { 22 | return new NextResponse("Prompt is required", { status: 400 }); 23 | } 24 | 25 | const isAllowed = await checkApiLimit(); 26 | const isPro = await checkSubscription(); 27 | 28 | if (!isAllowed && !isPro) { 29 | return new NextResponse("API Limit Exceeded", { status: 403 }); 30 | } 31 | 32 | const response = await replicate.run("riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", { 33 | input: { 34 | prompt_a: prompt, 35 | }, 36 | }); 37 | 38 | if (!isPro) { 39 | await increaseApiLimit(); 40 | } 41 | 42 | return NextResponse.json(response, { status: 200 }); 43 | } catch (error) { 44 | console.log("[MUSIC_ERROR]", error); 45 | return new NextResponse("Internal Server Error", { status: 500 }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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 }), { status: 200 }); 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: "Prometheus Pro", 47 | description: "Prometheus Pro", 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 }), { status: 200 }); 63 | } catch (error) { 64 | console.log("[STRIPE_ERROR]", error); 65 | return new NextResponse("Internal Server Error", { status: 500 }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/api/video/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscription"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import Replicate from "replicate"; 6 | 7 | const replicate = new Replicate({ 8 | auth: process.env.REPLICATE_API_TOKEN!, 9 | }); 10 | 11 | export async function POST(req: Request) { 12 | try { 13 | const { userId } = auth(); 14 | const body = await req.json(); 15 | const { prompt } = body; 16 | 17 | if (!userId) { 18 | return new NextResponse("Unauthorized", { status: 401 }); 19 | } 20 | 21 | if (!prompt) { 22 | return new NextResponse("Prompt is required", { status: 400 }); 23 | } 24 | 25 | const isAllowed = await checkApiLimit(); 26 | const isPro = await checkSubscription(); 27 | 28 | if (!isAllowed && !isPro) { 29 | return new NextResponse("API Limit Exceeded", { status: 403 }); 30 | } 31 | 32 | const response = await replicate.run("anotherjesse/zeroscope-v2-xl:9f747673945c62801b13b84701c783929c0ee784e4748ec062204894dda1a351", { 33 | input: { 34 | prompt, 35 | }, 36 | }); 37 | 38 | if (!isPro) { 39 | await increaseApiLimit(); 40 | } 41 | 42 | return NextResponse.json(response, { status: 200 }); 43 | } catch (error) { 44 | console.log("[VIDEO_ERROR]", error); 45 | return new NextResponse("Internal Server Error", { status: 500 }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import Stripe from "stripe"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import { stripe } from "@/lib/stripe"; 6 | import { NextResponse } from "next/server"; 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("Unauthorized", { status: 401 }); 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("OK", { status: 200 }); 55 | } 56 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayusshrathore/ai-saas/623dba771e012fb2923c0ef6ded4127bed09181e/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 | --muted: 210 40% 96.1%; 17 | --muted-foreground: 215.4 16.3% 46.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 222.2 84% 4.9%; 21 | 22 | --card: 0 0% 100%; 23 | --card-foreground: 222.2 84% 4.9%; 24 | 25 | --border: 214.3 31.8% 91.4%; 26 | --input: 214.3 31.8% 91.4%; 27 | 28 | --primary: 248 90% 66%; 29 | --primary-foreground: 210 40% 98%; 30 | 31 | --secondary: 210 40% 96.1%; 32 | --secondary-foreground: 222.2 47.4% 11.2%; 33 | 34 | --accent: 210 40% 96.1%; 35 | --accent-foreground: 222.2 47.4% 11.2%; 36 | 37 | --destructive: 0 84.2% 60.2%; 38 | --destructive-foreground: 210 40% 98%; 39 | 40 | --ring: 215 20.2% 65.1%; 41 | 42 | --radius: 0.5rem; 43 | } 44 | 45 | .dark { 46 | --background: 222.2 84% 4.9%; 47 | --foreground: 210 40% 98%; 48 | 49 | --muted: 217.2 32.6% 17.5%; 50 | --muted-foreground: 215 20.2% 65.1%; 51 | 52 | --popover: 222.2 84% 4.9%; 53 | --popover-foreground: 210 40% 98%; 54 | 55 | --card: 222.2 84% 4.9%; 56 | --card-foreground: 210 40% 98%; 57 | 58 | --border: 217.2 32.6% 17.5%; 59 | --input: 217.2 32.6% 17.5%; 60 | 61 | --primary: 210 40% 98%; 62 | --primary-foreground: 222.2 47.4% 11.2%; 63 | 64 | --secondary: 217.2 32.6% 17.5%; 65 | --secondary-foreground: 210 40% 98%; 66 | 67 | --accent: 217.2 32.6% 17.5%; 68 | --accent-foreground: 210 40% 98%; 69 | 70 | --destructive: 0 62.8% 30.6%; 71 | --destructive-foreground: 0 85.7% 97.3%; 72 | 73 | --ring: 217.2 32.6% 17.5%; 74 | } 75 | } 76 | 77 | @layer base { 78 | * { 79 | @apply border-border; 80 | } 81 | body { 82 | @apply bg-background text-foreground; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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 { ToasterProvider } from "@/components/toaster-provider"; 8 | import "./globals.css"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | export const metadata: Metadata = { 13 | title: "Prometheus AI", 14 | description: "An AI platform.", 15 | }; 16 | 17 | export default function RootLayout({ children }: { children: React.ReactNode }) { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /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 | 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(process.env.CRISP_WEBSITE_ID!); 9 | }, []); 10 | 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /components/crisp-provider.tsx: -------------------------------------------------------------------------------- 1 | import { CrispChat } from "./crisp-chat"; 2 | 3 | export const CrispProvider = () => { 4 | return ; 5 | }; 6 | 7 | export default CrispProvider; 8 | -------------------------------------------------------------------------------- /components/empty.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface EmptyProps { 4 | label: string; 5 | } 6 | 7 | export const Empty = ({ label }: EmptyProps) => { 8 | return ( 9 |
10 |
11 | Empty 12 |

{label}

13 |
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /components/free-counter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC, useEffect, useState } from "react"; 4 | 5 | import { MAX_FREE_COUNTS } from "@/constants"; 6 | import useProModal from "@/hooks/use-pro-modal"; 7 | import { Zap } from "lucide-react"; 8 | import { Button } from "./ui/button"; 9 | import { Card, CardContent } from "./ui/card"; 10 | import { Progress } from "./ui/progress"; 11 | 12 | interface FreeCounterProps { 13 | apiLimitCount: number; 14 | isPro: boolean; 15 | } 16 | 17 | export const FreeCounter: FC = ({ apiLimitCount = 0, isPro = false }) => { 18 | const [mounted, setMounted] = useState(false); 19 | const proModal = useProModal(); 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 | 42 |
43 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default FreeCounter; 53 | -------------------------------------------------------------------------------- /components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from "lucide-react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface HeadingProps { 6 | title: string; 7 | description: string; 8 | icon: LucideIcon; 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 |

{title}

27 |

{description}

28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/landing-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; 4 | 5 | const testimonials = [ 6 | { 7 | name: "John Doe", 8 | avatar: "A", 9 | title: "Software Engineer", 10 | description: "This is the best application I've ever used!", 11 | }, 12 | { 13 | name: "John Doe", 14 | avatar: "A", 15 | title: "Software Engineer", 16 | description: "This is the best application I've ever used!", 17 | }, 18 | { 19 | name: "John Doe", 20 | avatar: "A", 21 | title: "Software Engineer", 22 | description: "This is the best application I've ever used!", 23 | }, 24 | { 25 | name: "John Doe", 26 | avatar: "A", 27 | title: "Software Engineer", 28 | description: "This is the best application I've ever used!", 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 | {item.description} 47 |
48 |
49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default LandingContent; 56 | -------------------------------------------------------------------------------- /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 | 7 | import { Button } from "./ui/button"; 8 | 9 | export const LandingHero = () => { 10 | const { isSignedIn } = useAuth(); 11 | return ( 12 |
13 |
14 |

The Best AI Tool for

15 |
16 | 23 |
24 |
25 |
Create content using the power of AI.
26 |
27 | 28 | 31 | 32 |
33 |
No credit card required. Cancel anytime.
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /components/landing-navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@clerk/nextjs"; 4 | import { Montserrat } from "next/font/google"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "./ui/button"; 10 | 11 | const font = Montserrat({ 12 | weight: "600", 13 | subsets: ["latin"], 14 | }); 15 | 16 | export const LandingNabvbar = () => { 17 | const { isSignedIn } = useAuth(); 18 | 19 | return ( 20 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /components/loader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export const Loader = () => { 4 | return ( 5 |
6 |
7 | Logo 8 |
9 |

Prometheus is thinking...

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

Prometheus

75 | 76 |
77 | {routes.map((route) => ( 78 | 86 |
87 | 88 | {route.label} 89 |
90 | 91 | ))} 92 |
93 |
94 | 95 |
96 | ); 97 | }; 98 | 99 | export default Sidebar; 100 | -------------------------------------------------------------------------------- /components/subscription-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Zap } from "lucide-react"; 5 | import { FC, useState } from "react"; 6 | import { toast } from "react-hot-toast"; 7 | import { Button } from "./ui/button"; 8 | 9 | interface SubscriptionButtonProps { 10 | isPro: boolean; 11 | } 12 | 13 | export const SubscriptionButton: FC = ({ isPro = false }) => { 14 | const [loading, setLoading] = useState(false); 15 | 16 | const onClick = async () => { 17 | try { 18 | setLoading(true); 19 | const response = await axios.get("/api/stripe"); 20 | 21 | window.location.href = response.data.url; 22 | } catch (error) { 23 | console.log("[BILLING_ERROR]", 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 | export const ToasterProvider = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 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: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 12 | secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 13 | destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 14 | outline: "text-foreground", 15 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0", 16 | }, 17 | }, 18 | defaultVariants: { 19 | variant: "default", 20 | }, 21 | } 22 | ); 23 | 24 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {} 25 | 26 | function Badge({ className, variant, ...props }: BadgeProps) { 27 | return
; 28 | } 29 | 30 | export { Badge, badgeVariants }; 31 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 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: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", 19 | }, 20 | size: { 21 | default: "h-10 px-4 py-2", 22 | sm: "h-9 rounded-md px-3", 23 | lg: "h-11 rounded-md px-8", 24 | icon: "h-10 w-10", 25 | }, 26 | }, 27 | defaultVariants: { 28 | variant: "default", 29 | size: "default", 30 | }, 31 | } 32 | ); 33 | 34 | export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { 35 | asChild?: boolean; 36 | } 37 | 38 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { 39 | const Comp = asChild ? Slot : "button"; 40 | return ; 41 | }); 42 | Button.displayName = "Button"; 43 | 44 | export { Button, buttonVariants }; 45 | -------------------------------------------------------------------------------- /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 |