Repository: ayusshrathore/ai-saas Branch: main Commit: 623dba771e01 Files: 76 Total size: 97.7 KB Directory structure: gitextract_d84y1w4g/ ├── .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 │ ├── globals.css │ └── layout.tsx ├── 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 ├── components.json ├── 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.json ├── postcss.config.js ├── prisma/ │ └── schema.prisma ├── tailwind.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .github/workflows/npm-gulp.yml ================================================ name: NodeJS with Gulp on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Build run: | npm install gulp ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local .env # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Ayush Rathore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # AI-SaaS - AI-Powered Software-as-a-Service Application [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![License](https://img.shields.io/badge/React.js-v18.2.0-blue.svg)](https://opensource.org/licenses/MIT) [![Next.js](https://img.shields.io/badge/Next.js-v13.4.12-blueviolet.svg)](https://nextjs.org/) [![OpenAI](https://img.shields.io/badge/OpenAI-API-yellow.svg)](https://openai.com/) [![Replicate](https://img.shields.io/badge/Replicate-v0.12.3-orange.svg)](https://replicate.ai/) [![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-v3.3.3-blue.svg)](https://tailwindcss.com/) [![Prisma](https://img.shields.io/badge/Prisma-v5.0.0-lightgrey.svg)](https://prisma.io/) [![Stripe](https://img.shields.io/badge/Stripe-API%20v12.16.0-green.svg)](https://stripe.com/) 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. ## Features - **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. - **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. - **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. - **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. - **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. - **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. - **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. ## Screenshots Screenshot 2023-07-30 at 11 33 59 AM Screenshot 2023-07-30 at 11 40 43 AM Screenshot 2023-07-30 at 11 41 18 AM Screenshot 2023-07-30 at 11 41 53 AM Screenshot 2023-07-30 at 11 42 23 AM Screenshot 2023-07-30 at 11 42 38 AM Screenshot 2023-07-30 at 11 42 50 AM ## Getting Started To run AI-SaaS locally, follow these steps: 1. **Clone the repository**: ```bash git clone https://github.com/ayusshrathore/ai-saas.git cd ai-saas ``` 2. **Install dependencies**: ```bash npm install # or yarn install ``` 3. **Configure environment variables**: 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. 4. **Run the application**: ```bash npm run dev # or yarn dev ``` The application should now be running locally at `http://localhost:3000`. ## Deployment 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. ## Contributions 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. When contributing, adhere to the existing code style and include comprehensive test cases for new features. ## License AI-SaaS is released under the [MIT License](https://opensource.org/licenses/MIT). ## Acknowledgments 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. [![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) ================================================ FILE: app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx ================================================ import { SignIn } from "@clerk/nextjs"; export default function Page() { return ; } ================================================ FILE: app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx ================================================ import { SignUp } from "@clerk/nextjs"; export default function Page() { return ; } ================================================ FILE: app/(auth)/layout.tsx ================================================ import React from "react"; const AuthLayout = ({ children }: { children: React.ReactNode }) => { return
{children}
; }; export default AuthLayout; ================================================ FILE: app/(dashboard)/(routes)/code/constants.tsx ================================================ import * as z from "zod"; export const formSchema = z.object({ prompt: z.string().min(1, { message: "Prompt is required.", }), }); ================================================ FILE: app/(dashboard)/(routes)/code/page.tsx ================================================ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; import { Code } from "lucide-react"; import { useRouter } from "next/navigation"; import { ChatCompletionRequestMessage } from "openai"; import { useState } from "react"; import { useForm } from "react-hook-form"; import ReactMarkdown from "react-markdown"; import * as z from "zod"; import { BotAvatar } from "@/components/bot-avatar"; import { Empty } from "@/components/empty"; import { Heading } from "@/components/heading"; import { Loader } from "@/components/loader"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { UserAvatar } from "@/components/user-avatar"; import { cn } from "@/lib/utils"; import useProModal from "@/hooks/use-pro-modal"; import { toast } from "react-hot-toast"; import { formSchema } from "./constants"; const CodePage = () => { const router = useRouter(); const proModal = useProModal(); const [messages, setMessages] = useState([]); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { prompt: "", }, }); const isLoading = form.formState.isSubmitting; const onSubmit = async (values: z.infer) => { console.log(values); try { const userMessage: ChatCompletionRequestMessage = { role: "user", content: values.prompt, }; const newMessages = [...messages, userMessage]; const response = await axios.post("/api/code", { messages: newMessages, }); setMessages((current) => [...current, userMessage, response.data]); form.reset(); } catch (error: any) { console.log(error); if (error?.response?.status === 403) { proModal.onOpen(); } else { toast.error("Something went wrong."); } } finally { router.refresh(); } }; return (
( )} />
{isLoading && (
)} {messages.length === 0 && !isLoading && }
{messages.map((message, index) => (
{message.role === "user" ? : } (
                      
), code: ({ node, ...props }) => , }} > {message.content || ""}
))}
); }; export default CodePage; ================================================ FILE: app/(dashboard)/(routes)/conversation/constants.tsx ================================================ import * as z from "zod"; export const formSchema = z.object({ prompt: z.string().min(1, { message: "Prompt is required.", }), }); ================================================ FILE: app/(dashboard)/(routes)/conversation/page.tsx ================================================ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; import { MessageSquare } from "lucide-react"; import { useRouter } from "next/navigation"; import { ChatCompletionRequestMessage } from "openai"; import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { BotAvatar } from "@/components/bot-avatar"; import { Empty } from "@/components/empty"; import { Heading } from "@/components/heading"; import { Loader } from "@/components/loader"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { UserAvatar } from "@/components/user-avatar"; import { cn } from "@/lib/utils"; import useProModal from "@/hooks/use-pro-modal"; import { toast } from "react-hot-toast"; import { formSchema } from "./constants"; const ConversationPage = () => { const router = useRouter(); const proModal = useProModal(); const [messages, setMessages] = useState([]); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { prompt: "", }, }); const isLoading = form.formState.isSubmitting; const onSubmit = async (values: z.infer) => { console.log(values); try { const userMessage: ChatCompletionRequestMessage = { role: "user", content: values.prompt, }; const newMessages = [...messages, userMessage]; const response = await axios.post("/api/conversation", { messages: newMessages, }); setMessages((current) => [...current, userMessage, response.data]); form.reset(); } catch (error: any) { console.log(error); if (error?.response?.status === 403) { proModal.onOpen(); } else { toast.error("Something went wrong."); } } finally { router.refresh(); } }; return (
( )} />
{isLoading && (
)} {messages.length === 0 && !isLoading && }
{messages.map((message, index) => (
{message.role === "user" ? : }

{message.content}

))}
); }; export default ConversationPage; ================================================ FILE: app/(dashboard)/(routes)/dashboard/page.tsx ================================================ "use client"; import { ArrowRight, Code, ImageIcon, MessageSquare, Music, VideoIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; const tools = [ { label: "Conversation", icon: MessageSquare, color: "text-violet-500", bgColor: "bg-violet-500/10", href: "/conversation", }, { label: "Music Generation", icon: Music, color: "text-emerald-500", bgColor: "bg-emerald-500/10", href: "/music", }, { label: "Image Generation", icon: ImageIcon, color: "text-pink-700", bgColor: "bg-pink-700/10", href: "/image", }, { label: "Video Generation", icon: VideoIcon, color: "text-orange-700", bgColor: "bg-orange-700/10", href: "/video", }, { label: "Code Generation", icon: Code, color: "text-green-700", bgColor: "bg-green-700/10", href: "/code", }, ]; const DashboardPage = () => { const router = useRouter(); return (

Explore the power of AI

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

{tools.map((tool) => ( router.push(tool.href)} key={tool.href} className={ "p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer" } >
{tool.label}
))}
); }; export default DashboardPage; ================================================ FILE: app/(dashboard)/(routes)/image/constants.tsx ================================================ import * as z from "zod"; export const formSchema = z.object({ prompt: z.string().min(1, { message: "Image Prompt is required.", }), amount: z.string().min(1), resolution: z.string().min(1), }); export const amountOptions = [ { value: "1", label: "1 Photo", }, { value: "2", label: "2 Photos", }, { value: "3", label: "3 Photos", }, { value: "4", label: "4 Photos", }, { value: "5", label: "5 Photos", }, ]; export const resolutionOptions = [ { value: "256x256", label: "256x256", }, { value: "512x512", label: "512x512", }, { value: "1024x1024", label: "1024x1024", }, ]; ================================================ FILE: app/(dashboard)/(routes)/image/page.tsx ================================================ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; import { Download, ImageIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Empty } from "@/components/empty"; import { Heading } from "@/components/heading"; import { Loader } from "@/components/loader"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Card, CardFooter } from "@/components/ui/card"; import useProModal from "@/hooks/use-pro-modal"; import Image from "next/image"; import { toast } from "react-hot-toast"; import { amountOptions, formSchema, resolutionOptions } from "./constants"; const ImagePage = () => { const router = useRouter(); const proModal = useProModal(); const [images, setImages] = useState([]); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { prompt: "", amount: "1", resolution: "512x512", }, }); const isLoading = form.formState.isSubmitting; const onSubmit = async (values: z.infer) => { console.log(values); try { setImages([]); const response = await axios.post("/api/image", values); const urls = response.data.map((image: { url: string }) => image.url); setImages(urls); form.reset(); } catch (error: any) { console.log(error); if (error?.response?.status === 403) { proModal.onOpen(); } else { toast.error("Something went wrong."); } } finally { router.refresh(); } }; return (
( )} /> ( )} /> ( )} />
{isLoading && (
)} {images.length === 0 && !isLoading && }
{images.map((image, index) => (
image
))}
); }; export default ImagePage; ================================================ FILE: app/(dashboard)/(routes)/music/constants.tsx ================================================ import * as z from "zod"; export const formSchema = z.object({ prompt: z.string().min(1, { message: "Prompt is required.", }), }); ================================================ FILE: app/(dashboard)/(routes)/music/page.tsx ================================================ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; import { Music } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Empty } from "@/components/empty"; import { Heading } from "@/components/heading"; import { Loader } from "@/components/loader"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import useProModal from "@/hooks/use-pro-modal"; import { toast } from "react-hot-toast"; import { formSchema } from "./constants"; const MusicPage = () => { const router = useRouter(); const proModal = useProModal(); const [music, setMusic] = useState(""); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { prompt: "", }, }); const isLoading = form.formState.isSubmitting; const onSubmit = async (values: z.infer) => { console.log(values); try { setMusic(""); const response = await axios.post("/api/music"); setMusic(response.data.audio); form.reset(); } catch (error: any) { console.log(error); if (error?.response?.status === 403) { proModal.onOpen(); } else { toast.error("Something went wrong."); } } finally { router.refresh(); } }; return (
( )} />
{isLoading && (
)} {!music && !isLoading && } {music && ( )}
); }; export default MusicPage; ================================================ FILE: app/(dashboard)/(routes)/settings/page.tsx ================================================ import { Heading } from "@/components/heading"; import { SubscriptionButton } from "@/components/subscription-button"; import { checkSubscription } from "@/lib/subscription"; import { Settings } from "lucide-react"; const SettingsPage = async () => { const isPro = await checkSubscription(); return (
{isPro ? "You are currently on a pro plan." : "You are currently on a free plan."}
); }; export default SettingsPage; ================================================ FILE: app/(dashboard)/(routes)/video/constants.tsx ================================================ import * as z from "zod"; export const formSchema = z.object({ prompt: z.string().min(1, { message: "Prompt is required.", }), }); ================================================ FILE: app/(dashboard)/(routes)/video/page.tsx ================================================ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; import { Video } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Empty } from "@/components/empty"; import { Heading } from "@/components/heading"; import { Loader } from "@/components/loader"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import useProModal from "@/hooks/use-pro-modal"; import { toast } from "react-hot-toast"; import { formSchema } from "./constants"; const VideoPage = () => { const router = useRouter(); const proModal = useProModal(); const [video, setVideo] = useState(""); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { prompt: "", }, }); const isLoading = form.formState.isSubmitting; const onSubmit = async (values: z.infer) => { console.log(values); try { setVideo(""); const response = await axios.post("/api/music"); setVideo(response.data.audio); form.reset(); } catch (error: any) { console.log(error); if (error?.response?.status === 403) { proModal.onOpen(); } else { toast.error("Something went wrong."); } } finally { router.refresh(); } }; return (
( )} />
{isLoading && (
)} {!video && !isLoading && } {video && ( )}
); }; export default VideoPage; ================================================ FILE: app/(dashboard)/layout.tsx ================================================ import Navbar from "@/components/navbar"; import Sidebar from "@/components/sidebar"; import { getApiLimitCount } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; const DashboardLayout = async ({ children }: { children: React.ReactNode }) => { const apiLimitCount = await getApiLimitCount(); const isPro = await checkSubscription(); return (
{children}
); }; export default DashboardLayout; ================================================ FILE: app/(landing)/layout.tsx ================================================ const LandingLayout = ({ children }: { children: React.ReactNode }) => { return (
{children}
); }; export default LandingLayout; ================================================ FILE: app/(landing)/page.tsx ================================================ import LandingContent from "@/components/landing-content"; import { LandingHero } from "@/components/landing-hero"; import { LandingNabvbar } from "@/components/landing-navbar"; function LandingPage() { return (
); } export default LandingPage; ================================================ FILE: app/api/code/route.ts ================================================ import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openAi = new OpenAIApi(configuration); const instructionMessage: ChatCompletionRequestMessage = { role: "system", content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.", }; export async function POST(req: Request) { try { const { userId } = auth(); const body = await req.json(); const { messages } = body; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } if (!configuration) { return new NextResponse("OpenAI API Key not configured", { status: 500 }); } if (!messages) { return new NextResponse("Missing messages", { status: 400 }); } const isAllowed = await checkApiLimit(); const isPro = await checkSubscription(); if (!isAllowed && !isPro) { return new NextResponse("API Limit Exceeded", { status: 403 }); } const response = await openAi.createChatCompletion({ model: "gpt-3.5-turbo", messages: [instructionMessage, ...messages], }); if (!isPro) { await increaseApiLimit(); } return NextResponse.json(response.data.choices[0].message, { status: 200 }); } catch (error) { console.log("[CODE_ERROR]", error); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/conversation/route.ts ================================================ import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { Configuration, OpenAIApi } from "openai"; import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openAi = new OpenAIApi(configuration); export async function POST(req: Request) { try { const { userId } = auth(); const body = await req.json(); const { messages } = body; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } if (!configuration) { return new NextResponse("OpenAI API Key not configured", { status: 500 }); } if (!messages) { return new NextResponse("Missing messages", { status: 400 }); } const isAllowed = await checkApiLimit(); const isPro = await checkSubscription(); if (!isAllowed && !isPro) { return new NextResponse("API Limit Exceeded", { status: 403 }); } const response = await openAi.createChatCompletion({ model: "gpt-3.5-turbo", messages, }); if (!isPro) { await increaseApiLimit(); } return NextResponse.json(response.data.choices[0].message, { status: 200 }); } catch (error) { console.log("[CONVERSATION_ERROR]", error); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/image/route.ts ================================================ import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { Configuration, OpenAIApi } from "openai"; const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openAi = new OpenAIApi(configuration); export async function POST(req: Request) { try { const { userId } = auth(); const body = await req.json(); const { prompt, amount = 1, resolution = "512x512" } = body; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } if (!configuration) { return new NextResponse("OpenAI API Key not configured", { status: 500 }); } if (!prompt) { return new NextResponse("Missing prompt", { status: 400 }); } if (!amount) { return new NextResponse("Missing amount", { status: 400 }); } if (!resolution) { return new NextResponse("Missing resolution", { status: 400 }); } const isAllowed = await checkApiLimit(); const isPro = await checkSubscription(); if (!isAllowed && !isPro) { return new NextResponse("API Limit Exceeded", { status: 403 }); } const response = await openAi.createImage({ prompt, n: parseInt(amount, 10), size: resolution, }); if (!isPro) { await increaseApiLimit(); } return NextResponse.json(response.data.data, { status: 200 }); } catch (error) { console.log("[CONVERSATION_ERROR]", error); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/music/route.ts ================================================ import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import Replicate from "replicate"; const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN!, }); export async function POST(req: Request) { try { const { userId } = auth(); const body = await req.json(); const { prompt } = body; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } if (!prompt) { return new NextResponse("Prompt is required", { status: 400 }); } const isAllowed = await checkApiLimit(); const isPro = await checkSubscription(); if (!isAllowed && !isPro) { return new NextResponse("API Limit Exceeded", { status: 403 }); } const response = await replicate.run("riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", { input: { prompt_a: prompt, }, }); if (!isPro) { await increaseApiLimit(); } return NextResponse.json(response, { status: 200 }); } catch (error) { console.log("[MUSIC_ERROR]", error); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/stripe/route.ts ================================================ import { auth, currentUser } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import prismadb from "@/lib/prismadb"; import { stripe } from "@/lib/stripe"; import { absoluteUrl } from "@/lib/utils"; const settingsUrl = absoluteUrl("/settings"); export async function GET() { try { const { userId } = auth(); const user = await currentUser(); if (!userId || !user) { return new NextResponse("Unauthorized", { status: 401 }); } const userSubscription = await prismadb.userSubscription.findUnique({ where: { userId, }, }); if (userSubscription && userSubscription.stripeCustomerId) { const stripeSession = await stripe.billingPortal.sessions.create({ customer: userSubscription.stripeCustomerId, return_url: settingsUrl, }); return new NextResponse(JSON.stringify({ url: stripeSession.url }), { status: 200 }); } const stripeSession = await stripe.checkout.sessions.create({ success_url: settingsUrl, cancel_url: settingsUrl, payment_method_types: ["card"], mode: "subscription", billing_address_collection: "auto", customer_email: user.emailAddresses[0].emailAddress, line_items: [ { price_data: { currency: "USD", product_data: { name: "Prometheus Pro", description: "Prometheus Pro", }, unit_amount: 2000, recurring: { interval: "month", }, }, quantity: 1, }, ], metadata: { userId, }, }); return new NextResponse(JSON.stringify({ url: stripeSession.url }), { status: 200 }); } catch (error) { console.log("[STRIPE_ERROR]", error); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/video/route.ts ================================================ import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import Replicate from "replicate"; const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN!, }); export async function POST(req: Request) { try { const { userId } = auth(); const body = await req.json(); const { prompt } = body; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } if (!prompt) { return new NextResponse("Prompt is required", { status: 400 }); } const isAllowed = await checkApiLimit(); const isPro = await checkSubscription(); if (!isAllowed && !isPro) { return new NextResponse("API Limit Exceeded", { status: 403 }); } const response = await replicate.run("anotherjesse/zeroscope-v2-xl:9f747673945c62801b13b84701c783929c0ee784e4748ec062204894dda1a351", { input: { prompt, }, }); if (!isPro) { await increaseApiLimit(); } return NextResponse.json(response, { status: 200 }); } catch (error) { console.log("[VIDEO_ERROR]", error); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/webhook/route.ts ================================================ import { headers } from "next/headers"; import Stripe from "stripe"; import prismadb from "@/lib/prismadb"; import { stripe } from "@/lib/stripe"; import { NextResponse } from "next/server"; export async function POST(req: Request) { const body = await req.text(); const signature = headers().get("Stripe-Signature") as string; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!); } catch (error: any) { return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); } const session = event.data.object as Stripe.Checkout.Session; if (event.type === "checkout.session.completed") { const subscription = await stripe.subscriptions.retrieve(session.subscription as string); if (!session?.metadata?.userId) { return new NextResponse("Unauthorized", { status: 401 }); } await prismadb.userSubscription.create({ data: { userId: session.metadata.userId, stripeSubscriptionId: subscription.id, stripeCustomerId: subscription.customer as string, stripePriceId: subscription.items.data[0].price.id, stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), }, }); } if (event.type === "invoice.payment_succeeded") { const subscription = await stripe.subscriptions.retrieve(session.subscription as string); await prismadb.userSubscription.update({ where: { stripeSubscriptionId: subscription.id, }, data: { stripePriceId: subscription.items.data[0].price.id, stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), }, }); } return new NextResponse("OK", { status: 200 }); } ================================================ FILE: app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; html, body, :root { height: 100%; } @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --primary: 248 90% 66%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --ring: 215 20.2% 65.1%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 85.7% 97.3%; --ring: 217.2 32.6% 17.5%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: app/layout.tsx ================================================ import { ClerkProvider } from "@clerk/nextjs"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import CrispProvider from "@/components/crisp-provider"; import { ModalProvider } from "@/components/modal-provider"; import { ToasterProvider } from "@/components/toaster-provider"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Prometheus AI", description: "An AI platform.", }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ================================================ FILE: components/bot-avatar.tsx ================================================ import { Avatar, AvatarImage } from "./ui/avatar"; export const BotAvatar = () => { return ( ); }; ================================================ FILE: components/crisp-chat.tsx ================================================ "use client"; import { Crisp } from "crisp-sdk-web"; import { useEffect } from "react"; export const CrispChat = () => { useEffect(() => { Crisp.configure(process.env.CRISP_WEBSITE_ID!); }, []); return null; }; ================================================ FILE: components/crisp-provider.tsx ================================================ import { CrispChat } from "./crisp-chat"; export const CrispProvider = () => { return ; }; export default CrispProvider; ================================================ FILE: components/empty.tsx ================================================ import Image from "next/image"; interface EmptyProps { label: string; } export const Empty = ({ label }: EmptyProps) => { return (
Empty

{label}

); }; ================================================ FILE: components/free-counter.tsx ================================================ "use client"; import { FC, useEffect, useState } from "react"; import { MAX_FREE_COUNTS } from "@/constants"; import useProModal from "@/hooks/use-pro-modal"; import { Zap } from "lucide-react"; import { Button } from "./ui/button"; import { Card, CardContent } from "./ui/card"; import { Progress } from "./ui/progress"; interface FreeCounterProps { apiLimitCount: number; isPro: boolean; } export const FreeCounter: FC = ({ apiLimitCount = 0, isPro = false }) => { const [mounted, setMounted] = useState(false); const proModal = useProModal(); useEffect(() => { setMounted(true); }, []); if (!mounted) { return null; } if (isPro) { return null; } return (

{apiLimitCount} / {MAX_FREE_COUNTS} Free Generations

); }; export default FreeCounter; ================================================ FILE: components/heading.tsx ================================================ import { LucideIcon } from "lucide-react"; import { cn } from "@/lib/utils"; interface HeadingProps { title: string; description: string; icon: LucideIcon; iconColor?: string; bgColor?: string; } export const Heading = ({ title, description, icon: Icon, iconColor, bgColor, }: HeadingProps) => { return (

{title}

{description}

); }; ================================================ FILE: components/landing-content.tsx ================================================ "use client"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; const testimonials = [ { name: "John Doe", avatar: "A", title: "Software Engineer", description: "This is the best application I've ever used!", }, { name: "John Doe", avatar: "A", title: "Software Engineer", description: "This is the best application I've ever used!", }, { name: "John Doe", avatar: "A", title: "Software Engineer", description: "This is the best application I've ever used!", }, { name: "John Doe", avatar: "A", title: "Software Engineer", description: "This is the best application I've ever used!", }, ]; export const LandingContent = () => { return (

Testimonials

{testimonials.map((item) => (

{item.name}

{item.title}

{item.description}
))}
); }; export default LandingContent; ================================================ FILE: components/landing-hero.tsx ================================================ "use client"; import { useAuth } from "@clerk/nextjs"; import Link from "next/link"; import TypewriterComponent from "typewriter-effect"; import { Button } from "./ui/button"; export const LandingHero = () => { const { isSignedIn } = useAuth(); return (

The Best AI Tool for

Create content using the power of AI.
No credit card required. Cancel anytime.
); }; ================================================ FILE: components/landing-navbar.tsx ================================================ "use client"; import { useAuth } from "@clerk/nextjs"; import { Montserrat } from "next/font/google"; import Image from "next/image"; import Link from "next/link"; import { cn } from "@/lib/utils"; import { Button } from "./ui/button"; const font = Montserrat({ weight: "600", subsets: ["latin"], }); export const LandingNabvbar = () => { const { isSignedIn } = useAuth(); return ( ); }; ================================================ FILE: components/loader.tsx ================================================ import Image from "next/image"; export const Loader = () => { return (
Logo

Prometheus is thinking...

); }; ================================================ FILE: components/mobile-sidebar.tsx ================================================ "use client"; import { Menu } from "lucide-react"; import { useEffect, useState } from "react"; import Sidebar from "./sidebar"; import { Button } from "./ui/button"; import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"; const MobileSidebar = ({ apiLimitCount = 0, isPro = false }: { apiLimitCount: number; isPro: boolean }) => { const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); if (!isMounted) { return null; } return ( ); }; export default MobileSidebar; ================================================ FILE: components/modal-provider.tsx ================================================ "use client"; import { useEffect, useState } from "react"; import ProModal from "./pro-modal"; export const ModalProvider = () => { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) { return null; } return ( <> ); }; ================================================ FILE: components/navbar.tsx ================================================ import { UserButton } from "@clerk/nextjs"; import { getApiLimitCount } from "@/lib/api-limit"; import { checkSubscription } from "@/lib/subscription"; import MobileSidebar from "./mobile-sidebar"; const Navbar = async () => { const apiLimitCount = await getApiLimitCount(); const isPro = await checkSubscription(); return (
); }; export default Navbar; ================================================ FILE: components/pro-modal.tsx ================================================ "use client"; import axios from "axios"; import { useState } from "react"; import useProModal from "@/hooks/use-pro-modal"; import { cn } from "@/lib/utils"; import { Check, Code, ImageIcon, MessageSquare, Music, VideoIcon, Zap } from "lucide-react"; import { toast } from "react-hot-toast"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { Card } from "./ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog"; export const ProModal = () => { const proModal = useProModal(); const [loading, setLoading] = useState(false); const tools = [ { label: "Conversation", icon: MessageSquare, color: "text-violet-500", bgColor: "bg-violet-500/10", }, { label: "Music Generation", icon: Music, color: "text-emerald-500", bgColor: "bg-emerald-500/10", }, { label: "Image Generation", icon: ImageIcon, color: "text-pink-700", bgColor: "bg-pink-700/10", }, { label: "Video Generation", icon: VideoIcon, color: "text-orange-700", bgColor: "bg-orange-700/10", }, { label: "Code Generation", icon: Code, color: "text-green-700", bgColor: "bg-green-700/10", }, ]; const onSubscribe = async () => { try { setLoading(true); const response = await axios.get("/api/stripe"); window.location.href = response.data.url; } catch (error) { console.log("[STRIPE_CLIENT_ERROR]", error); toast.error("Something went wrong."); } finally { setLoading(false); } }; return (
Upgrade to Prometheus Pro pro
{tools.map((tool) => (
{tool.label}
))}
); }; export default ProModal; ================================================ FILE: components/sidebar.tsx ================================================ "use client"; import { Code, ImageIcon, LayoutDashboard, MessageSquare, Music, Settings, VideoIcon } from "lucide-react"; import { Montserrat } from "next/font/google"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { FC } from "react"; import { cn } from "@/lib/utils"; import FreeCounter from "./free-counter"; const montserrat = Montserrat({ weight: "600", subsets: ["latin"] }); const routes = [ { label: "Dashboard", icon: LayoutDashboard, href: "/dashboard", color: "text-sky-500", }, { label: "Conversation", icon: MessageSquare, href: "/conversation", color: "text-violet-500", }, { label: "Image Generation", icon: ImageIcon, href: "/image", color: "text-pink-700", }, { label: "Video Generation", icon: VideoIcon, href: "/video", color: "text-orange-700", }, { label: "Music Generation", icon: Music, href: "/music", color: "text-emerald-500", }, { label: "Code Generation", icon: Code, href: "/code", color: "text-green-700", }, { label: "Settings", icon: Settings, href: "/settings", }, ]; interface SidebarProps { apiLimitCount: number; isPro: boolean; } const Sidebar: FC = ({ apiLimitCount = 0, isPro = false }) => { const pathname = usePathname(); return (
Logo

Prometheus

{routes.map((route) => (
{route.label}
))}
); }; export default Sidebar; ================================================ FILE: components/subscription-button.tsx ================================================ "use client"; import axios from "axios"; import { Zap } from "lucide-react"; import { FC, useState } from "react"; import { toast } from "react-hot-toast"; import { Button } from "./ui/button"; interface SubscriptionButtonProps { isPro: boolean; } export const SubscriptionButton: FC = ({ isPro = false }) => { const [loading, setLoading] = useState(false); const onClick = async () => { try { setLoading(true); const response = await axios.get("/api/stripe"); window.location.href = response.data.url; } catch (error) { console.log("[BILLING_ERROR]", error); toast.error("Something went wrong."); } finally { setLoading(false); } }; return ( ); }; ================================================ FILE: components/toaster-provider.tsx ================================================ "use client"; import { Toaster } from "react-hot-toast"; export const ToasterProvider = () => { return ; }; ================================================ FILE: components/ui/avatar.tsx ================================================ "use client" import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Avatar.displayName = AvatarPrimitive.Root.displayName const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarImage.displayName = AvatarPrimitive.Image.displayName const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName export { Avatar, AvatarImage, AvatarFallback } ================================================ FILE: components/ui/badge.tsx ================================================ import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "@/lib/utils"; const badgeVariants = cva( "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", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", outline: "text-foreground", premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0", }, }, defaultVariants: { variant: "default", }, } ); export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return
; } export { Badge, badgeVariants }; ================================================ FILE: components/ui/button.tsx ================================================ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "@/lib/utils"; const buttonVariants = cva( "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", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ; }); Button.displayName = "Button"; export { Button, buttonVariants }; ================================================ FILE: components/ui/card.tsx ================================================ import * as React from "react" import { cn } from "@/lib/utils" const Card = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) Card.displayName = "Card" const CardHeader = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardHeader.displayName = "CardHeader" const CardTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardTitle.displayName = "CardTitle" const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardFooter.displayName = "CardFooter" export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } ================================================ FILE: components/ui/dialog.tsx ================================================ "use client" import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react" import { cn } from "@/lib/utils" const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger const DialogPortal = ({ className, ...props }: DialogPrimitive.DialogPortalProps) => ( ) DialogPortal.displayName = DialogPrimitive.Portal.displayName const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} Close )) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
) DialogHeader.displayName = "DialogHeader" const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) DialogFooter.displayName = "DialogFooter" const DialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } ================================================ FILE: components/ui/form.tsx ================================================ import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot" import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext, } from "react-hook-form" import { cn } from "@/lib/utils" import { Label } from "@/components/ui/label" const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath > = { name: TName } const FormFieldContext = React.createContext( {} as FormFieldContextValue ) const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath >({ ...props }: ControllerProps) => { return ( ) } const useFormField = () => { const fieldContext = React.useContext(FormFieldContext) const itemContext = React.useContext(FormItemContext) const { getFieldState, formState } = useFormContext() const fieldState = getFieldState(fieldContext.name, formState) if (!fieldContext) { throw new Error("useFormField should be used within ") } const { id } = itemContext return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, } } type FormItemContextValue = { id: string } const FormItemContext = React.createContext( {} as FormItemContextValue ) const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { const id = React.useId() return (
) }) FormItem.displayName = "FormItem" const FormLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { const { error, formItemId } = useFormField() return (