├── .env.example ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── merge.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── actions └── getToolByLabel.ts ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (dashboard) │ ├── (routes) │ │ ├── code │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── conversation │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── image │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── music │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ └── video │ │ │ ├── constants.ts │ │ │ └── page.tsx │ └── layout.tsx ├── (landing) │ ├── layout.tsx │ └── page.tsx ├── README.md ├── api │ ├── README.md │ ├── 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 └── not-found.tsx ├── components.json ├── components ├── README.md ├── avatar │ ├── BotAvatar.tsx │ └── UserAvatar.tsx ├── buttons │ └── SubscriptionButton.tsx ├── empty │ └── Empty.tsx ├── heading │ └── Heading.tsx ├── loader │ └── Loader.tsx ├── modals │ └── ProModal.tsx ├── navbar │ └── Navbar.tsx ├── pages │ └── landing │ │ ├── LandingHero.tsx │ │ └── LandingNavbar.tsx ├── sidebar │ ├── FreeCounter.tsx │ ├── MobileSidebar.tsx │ └── Sidebar.tsx ├── toast │ └── toast-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 ├── constants ├── README.md └── constants.ts ├── docker ├── README.md ├── docker-compose.yml ├── mysql │ └── Dockerfile └── next │ ├── Dockerfile │ └── README.md ├── hooks ├── README.md └── useProModal.ts ├── lib ├── README.md ├── api-limit.ts ├── prismadb.ts ├── stripe.ts ├── subscriptions.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── prisma ├── README.md └── schema.prisma ├── providers ├── ModalProvider.tsx └── README.md ├── public ├── empty.png ├── logo.png ├── next.svg └── vercel.svg ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Simpler for Prisma to use `.env` insted of `.env.local` 2 | 3 | #^ Clerk Auth 4 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY='' 5 | CLERK_SECRET_KEY='' 6 | 7 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 8 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 9 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard 10 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard 11 | 12 | #^ OpenAI 13 | OPENAI_API_KEY='' 14 | 15 | #^ Replicate AI 16 | REPLICATE_API_TOKEN='' 17 | 18 | #^ DATABASE 19 | # Used my Prisma to connect to the database 20 | DATABASE_URL="mysql://user:password@localhost:3306/test" 21 | # Used for the Docker container 22 | MYSQL_ROOT_PASSWORD=password 23 | MYSQL_DATABASE=test 24 | MYSQL_USER=user 25 | MYSQL_PASSWORD=password 26 | 27 | #^ STRIPE 28 | STRIPE_API_KEY='' 29 | STRIPE_WEBHOOK_SECRET='' 30 | NEXT_PUBLIC_APP_URL='' -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: 'BUG: ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | 13 | 14 | ## Reproducing the bug** 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected behavior 22 | 23 | 24 | 25 | ## Screenshots 26 | 27 | 28 | 29 | ## Platforms 30 | ### Desktop (please complete the following information): 31 | - **OS:** 32 | - **Browser:** 33 | - **Version:** 34 | 35 | ### Smartphone (please complete the following information):** 36 | - **Device:** 37 | - **OS:** 38 | - **Browser:** 39 | - **Version:** 40 | 41 | ## Additional context 42 | 43 | 44 | 45 | ## Severity: 46 | - [ ] **High** (*completely break the functionality and usability of the site*) 47 | - [ ] **Medium** (*significant disruption or degrade user experience*) 48 | - [ ] **Low** (*only cause minor disruption keeping the site functional*) 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: 'FEATURE: ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | 12 | 13 | 14 | ## Describe the solution you'd like 15 | 16 | 17 | 18 | ## Describe alternatives you've considered 19 | 20 | 21 | 22 | ## Screenshots 23 | 24 | 25 | 26 | ## Importance 27 | - [ ] **High** 28 | - [ ] **Medium** 29 | - [ ] **Low** 30 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Merging 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | 12 | env: 13 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} 14 | CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} 15 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: ${{ secrets.NEXT_PUBLIC_CLERK_SIGN_IN_URL }} 16 | NEXT_PUBLIC_CLERK_SIGN_UP_URL: ${{ secrets.NEXT_PUBLIC_CLERK_SIGN_UP_URL }} 17 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL: ${{ secrets.NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL }} 18 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL: ${{ secrets.NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL }} 19 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 20 | REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} 21 | DATABASE_URL: ${{ secrets.DATABASE_URL }} 22 | STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }} 23 | STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }} 24 | NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL }} 25 | 26 | jobs: 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | strategy: 31 | matrix: 32 | node-version: [18.17.0, 20.9.0] 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | 37 | - name: Detect package manager 38 | id: detect-package-manager 39 | run: | 40 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 41 | echo "manager=yarn" >> $GITHUB_OUTPUT 42 | echo "command=install" >> $GITHUB_OUTPUT 43 | echo "runner=yarn" >> $GITHUB_OUTPUT 44 | exit 0 45 | elif [ -f "${{ github.workspace }}/package.json" ]; then 46 | echo "manager=npm" >> $GITHUB_OUTPUT 47 | echo "command=ci" >> $GITHUB_OUTPUT 48 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 49 | exit 0 50 | else 51 | echo "Unable to determine package manager" 52 | exit 1 53 | fi 54 | 55 | - name: Setup Node 56 | uses: actions/setup-node@v3 57 | with: 58 | node-version: ${{ matrix.node-version }} 59 | cache: ${{ steps.detect-package-manager.outputs.manager }} 60 | 61 | - name: Install dependencies 62 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 63 | 64 | - name: Run Linter 65 | run: ${{ steps.detect-package-manager.outputs.runner }} eslint '**/*.{js,ts,tsx}' 66 | 67 | trunk_check: 68 | name: Trunk Check Runner 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - name: Checkout 73 | uses: actions/checkout@v3 74 | 75 | - name: Trunk Check 76 | uses: trunk-io/trunk-action@v1 77 | 78 | build: 79 | name: Build 80 | needs: lint 81 | runs-on: ubuntu-latest 82 | strategy: 83 | matrix: 84 | node-version: [18.17.0, 20.9.0] 85 | steps: 86 | - name: Checkout 87 | uses: actions/checkout@v3 88 | 89 | - name: Detect package manager 90 | id: detect-package-manager 91 | run: | 92 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 93 | echo "manager=yarn" >> $GITHUB_OUTPUT 94 | echo "command=install" >> $GITHUB_OUTPUT 95 | echo "runner=yarn" >> $GITHUB_OUTPUT 96 | exit 0 97 | elif [ -f "${{ github.workspace }}/package.json" ]; then 98 | echo "manager=npm" >> $GITHUB_OUTPUT 99 | echo "command=ci" >> $GITHUB_OUTPUT 100 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 101 | exit 0 102 | else 103 | echo "Unable to determine package manager" 104 | exit 1 105 | fi 106 | 107 | - name: Setup Node 108 | uses: actions/setup-node@v3 109 | with: 110 | node-version: ${{ matrix.node-version }} 111 | cache: ${{ steps.detect-package-manager.outputs.manager }} 112 | 113 | - name: Install dependencies 114 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 115 | 116 | - name: Build 117 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 118 | -------------------------------------------------------------------------------- /.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | .env 37 | .env.production 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // Use IntelliSense to learn about possible attributes. 2 | // Hover to view descriptions of existing attributes. 3 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 4 | { 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Next.js: debug server-side with Database", 9 | "type": "node-terminal", 10 | "request": "launch", 11 | "command": "yarn concurrently \"yarn dev\" \"docker-compose --env-file .env -f docker/docker-compose.yml up db\"" 12 | }, 13 | { 14 | "name": "Database: Run Only", 15 | "type": "node-terminal", 16 | "request": "launch", 17 | "command": "docker-compose --env-file .env -f docker/docker-compose.yml up db" 18 | }, 19 | { 20 | "name": "Next.js: debug server-side", 21 | "type": "node-terminal", 22 | "request": "launch", 23 | "command": "yarn dev" 24 | }, 25 | { 26 | "name": "Next.js: debug server-side with Stripe", 27 | "type": "node-terminal", 28 | "request": "launch", 29 | "command": "yarn dev && stripe listen --forward-to localhost:3000/api/webhook" 30 | }, 31 | { 32 | "name": "Next.js: debug client-side", 33 | "type": "msedge", 34 | "request": "launch", 35 | "url": "http://localhost:3000" 36 | }, 37 | { 38 | "name": "Next.js: debug full stack", 39 | "type": "node-terminal", 40 | "request": "launch", 41 | "command": "yarn dev", 42 | "serverReadyAction": { 43 | "pattern": "started server on .+, url: (https?://.+)", 44 | "uriFormat": "%s", 45 | "action": "debugWithEdge" 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.autoSave": "off", 4 | "testing.automaticallyOpenPeekView": "never" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maruf Bepary 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 | ![cover](https://github.com/mbeps/magician-ai/assets/58662575/7d656288-3c1b-4c7c-887f-f2136e5a732b) 2 | 3 | --- 4 | 5 | Introducing Magician AI, a highly interactive platform that leverages the power of OpenAI's GPT-3.5 for text generation and DALL-E for image generation. As well as Replicate AI for song and video generation. With our platform, you can communicate with the AI, ask it to write code, describe images or videos to be generated, and more. Our interface is user-friendly and designed to provide an exciting, creative experience. Built to handle subscriptions and payments using Stripe, users get limited initial uses and can subsequently subscribe for continued access. 6 | 7 | # **Requirements** 8 | To run the project, you need: 9 | - Node 18 LTS 10 | - Next.JS 13+ 11 | 12 | # **Features** 13 | Our platform offers several unique and engaging features to explore: 14 | 15 | ## **Authentication and Account Management** 16 | Our system ensures seamless and secure user experiences: 17 | - Users can sign up using email and password 18 | - Users can sign up using third-party authentication providers such as Google and GitHub 19 | - Users can log in using email and password 20 | - Users can log out 21 | - Users can manage their subscriptions and payments 22 | 23 | ## **Conversations** 24 | Users can have enriching conversations with the AI: 25 | - Ask the AI to generate text based on given prompts 26 | - Discuss various topics in a conversational manner with the AI 27 | 28 | ## **Code Generation** 29 | Users can leverage AI for programming: 30 | - Ask the AI to write code based on specific requirements 31 | - The AI provides a generated code snippet along with an explanation 32 | 33 | ## **Image Generation** 34 | The AI creates images from user descriptions: 35 | - Users describe an image 36 | - The AI generates a number of images matching that description at a specified resolution 37 | 38 | ## **Video Generation** 39 | Experience the AI's creativity with video generation: 40 | - Users describe a video 41 | - The AI generates a video based on the description provided 42 | 43 | Please note that after a limited number of uses, users must subscribe to continue accessing these features. Payment and subscription management is handled securely using Stripe. 44 | 45 | # **Tech Stack** 46 | 47 | The Magician AI project utilizes a robust set of modern technologies to deliver a high-quality user experience: 48 | 49 | ## **Frontend** 50 | 51 | - **[Next.js](https://nextjs.org/)**: A React-based framework offering tools and conventions for server-side rendered (SSR) and statically generated web applications. 52 | 53 | - **[Tailwind CSS](https://tailwindcss.com/)**: A utility-first CSS framework promoting highly customizable and responsive design. 54 | 55 | - **[Shadcn UI](https://ui.shadcn.com/)**: A collection of reusable, accessible, and customizable components built with Radix UI and Tailwind CSS. Shadcn UI offers an easy start for developers, irrespective of their experience with component libraries. 56 | 57 | ## **Backend** 58 | 59 | - **[Node.js](https://nodejs.org/en/)**: A JavaScript runtime environment that executes JavaScript code outside of a web browser. 60 | 61 | - **[Prisma](https://www.prisma.io/)**: An open-source ORM that provides a type-safe client for efficient, bug-free queries. 62 | 63 | - **[Axios](https://axios-http.com/)**: A promise-based HTTP client used for making HTTP requests. 64 | 65 | - **[Clerk Auth](https://clerk.com/)**: A user-friendly authentication and user management platform. Clerk provides multiple authentication strategies and a comprehensive user management system. It is secure, scalable, and easy to use, with customizable UI components. 66 | 67 | - **[Stripe](https://stripe.com/)**: An online payment processing platform used in this project for handling payments and subscriptions. 68 | 69 | - **[Zod](https://github.com/colinhacks/zod)**: A TypeScript-first schema declaration and validation library used for type-safe REST APIs. 70 | 71 | - **[MySQL](https://www.mysql.com/)**: A popular open-source relational database management system. 72 | 73 | ## **AI and Media Generation** 74 | 75 | - **[OpenAI](https://openai.com/)**: Utilized for generating text and images. OpenAI’s GPT-3.5 is used for text generation, and DALL-E for image generation. 76 | 77 | - **[Replicate AI](https://replicate.com/)**: Used for generating music and videos based on user inputs. 78 | 79 | Each technology in this stack plays a crucial role in delivering a seamless and dynamic user experience. 80 | 81 | # **Running Application Locally** 82 | 83 | ## 1. **Clone the Project Locally** 84 | Open your terminal and use the following command to clone the project: 85 | ```sh 86 | git clone https://github.com/mbeps/magician-ai.git 87 | ``` 88 | 89 | ## 2. **Install Dependencies** 90 | Navigate to the project's root directory and install the required dependencies using the following command: 91 | ```sh 92 | yarn install 93 | ``` 94 | 95 | ## 3. **Set Up Environment Variables** 96 | Create a copy of the `.env.example` file and rename it to `.env.local`. Populate the `.env.local` with the necessary secrets. 97 | 98 | Here are instructions for getting some of these secrets: 99 | 100 | **Clerk Auth** 101 | 1. Create an account on Clerk's website. 102 | 2. Create a new application. 103 | 3. In your application dashboard, go to the settings section. 104 | 4. You will find the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` in this section. 105 | 5. Add these keys to your environment variables in `.env.local`. 106 | 107 | You also need to add the following URLs for Clerk Auth: 108 | ``` 109 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 110 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 111 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard 112 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard 113 | ``` 114 | 115 | For `OPENAI_API_KEY`, `REPLICATE_API_TOKEN`, `DATABASE_URL`, `STRIPE_API_KEY`, and `NEXT_PUBLIC_APP_URL`, refer to the respective service's documentation or settings page to get these values. 116 | 117 | # 4. **Running Database (Docker)** 118 | This step is necessary if you with to use the Docker image that has been provided. You can also use an alternative cloud service for MySQL. Make sure to change the connection string on the `.env` file if you wish to do so. 119 | 120 | Run the following command from the root of the project to start your MySQL container: 121 | ```sh 122 | docker-compose --env-file .env -f docker/docker-compose.yml up db 123 | ``` 124 | 125 | ## 5. **Set Up Prisma** 126 | To set up Prisma and push schema to the database, use the following commands: 127 | 128 | Generate Prisma Client: 129 | ```sh 130 | yarn prisma generate 131 | ``` 132 | 133 | Push Prisma schema to the database: 134 | ```sh 135 | yarn prisma db push 136 | ``` 137 | 138 | ## 5. **Set Up Stripe Webhook** 139 | Run the Stripe CLI and make it listen to the webhook: 140 | ```sh 141 | stripe listen --forward-to localhost:3000/api/webhook 142 | ``` 143 | This will output your `STRIPE_WEBHOOK_SECRET`. Add this to your environment variables in `.env.local`. 144 | 145 | ## 6. **Run Project** 146 | Once you've set up the environment variables, Prisma, and Stripe, use the following commands to run the project: 147 | 148 | In one terminal, run the Next.js server: 149 | ```sh 150 | yarn dev 151 | ``` 152 | 153 | In another terminal, start the Stripe listener: 154 | ```sh 155 | stripe listen --forward-to localhost:3000/api/webhook 156 | ``` 157 | 158 | This should run the project on `localhost:3000`. 159 | 160 | **Note:** Both the frontend Next.js server and Stripe CLI need to be running concurrently for the application to function properly. 161 | -------------------------------------------------------------------------------- /actions/getToolByLabel.ts: -------------------------------------------------------------------------------- 1 | import { Tool, tools } from "@/constants/constants"; 2 | 3 | /** 4 | * Gets a tool object by its label. 5 | * @param label (string): the label of the tool 6 | * @returns (Tool | null): the tool object if found, null otherwise 7 | */ 8 | export function getToolByLabel(label: string): Tool | null { 9 | return tools.find((tool) => tool.label === label) || null; 10 | } 11 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | //^ Name and route are specified in the documentation for Clerk 4 | 5 | /** 6 | * Displays the sign in page. 7 | * The component rendered is from Clerk. 8 | * The route is specified in the documentation for Clerk. 9 | * @returns (JSX.Element): sign in page 10 | */ 11 | export default function Page() { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | //^ Name and route are specified in the documentation for Clerk 4 | 5 | /** 6 | * Displays the sign up page. 7 | * The component rendered is from Clerk. 8 | * The route is specified in the documentation for Clerk. 9 | * @returns (JSX.Element): sign up page 10 | */ 11 | export default function Page() { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Specifies the layout for all pages in the auth folder 3 | * @param {children} (React.ReactNode): children components 4 | * @returns (JSX.Element): auth layout for all pages in the auth folder 5 | */ 6 | const AuthLayout = ({ children }: { children: React.ReactNode }) => { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export default AuthLayout; 15 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | /** 4 | * The form schema for the code route. 5 | * Forces type safety on the form. 6 | */ 7 | export const formSchema = z.object({ 8 | prompt: z.string().min(1, { 9 | message: "Prompt is required.", 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { BotAvatar } from "@/components/avatar/BotAvatar"; 4 | import { UserAvatar } from "@/components/avatar/UserAvatar"; 5 | import { Empty } from "@/components/empty/Empty"; 6 | import { Heading } from "@/components/heading/Heading"; 7 | import { Loader } from "@/components/loader/Loader"; 8 | import { Button } from "@/components/ui/button"; 9 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 10 | import { Input } from "@/components/ui/input"; 11 | import { cn } from "@/lib/utils"; 12 | import { zodResolver } from "@hookform/resolvers/zod"; 13 | import axios from "axios"; 14 | import { Code } from "lucide-react"; 15 | import { useRouter } from "next/navigation"; 16 | import { ChatCompletionRequestMessage } from "openai"; 17 | import React, { useState } from "react"; 18 | import { useForm } from "react-hook-form"; 19 | import ReactMarkdown from "react-markdown"; 20 | import * as z from "zod"; 21 | import { formSchema } from "./constants"; 22 | import { toast } from "react-hot-toast"; 23 | import { useProModal } from "@/hooks/useProModal"; 24 | import { getToolByLabel } from "@/actions/getToolByLabel"; 25 | import { Tool } from "@/constants/constants"; 26 | 27 | type CodeProps = {}; 28 | 29 | /** 30 | * Code page allows users to generate code. 31 | * It uses the OpenAI API to generate code from a prompt. 32 | * It returns a code snippet with explanation. 33 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 34 | */ 35 | const CodePage: React.FC = () => { 36 | const proModal = useProModal(); // modal for non-subscribers 37 | const router = useRouter(); 38 | const [messages, setMessages] = useState([]); // messages from the bot 39 | 40 | /** 41 | * Form for the prompt for the code generation. 42 | * Zod used for validation. 43 | */ 44 | const form = useForm>({ 45 | resolver: zodResolver(formSchema), 46 | defaultValues: { 47 | prompt: "", 48 | }, 49 | }); 50 | 51 | const tool: Tool | null = getToolByLabel("Code Generation"); 52 | 53 | if (!tool) { 54 | return null; 55 | } 56 | 57 | const isLoading = form.formState.isSubmitting; 58 | 59 | /** 60 | * Submit the prompt to the API to generate code. 61 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 62 | * @param values (string) prompt for the code generation 63 | */ 64 | const onSubmit = async (values: z.infer) => { 65 | try { 66 | const userMessage: ChatCompletionRequestMessage = { 67 | role: "user", // user message 68 | content: values.prompt, // prompt for the code generation 69 | }; 70 | const newMessages = [...messages, userMessage]; // add user message to the messages 71 | 72 | /** 73 | * Send the messages to the API to generate code. 74 | * Store the response in the messages. 75 | */ 76 | const response = await axios.post("/api/conversation", { 77 | messages: newMessages, 78 | }); 79 | setMessages((current) => [...current, userMessage, response.data]); // add the response to the messages 80 | 81 | form.reset(); // clear input 82 | } catch (error: any) { 83 | // if the user is not subscribed and there are no remaining free tries, it will show a modal 84 | if (error.response.status === 403) { 85 | proModal.onOpen(); 86 | } else { 87 | console.log(error); 88 | toast.error("Could not generate code"); 89 | } 90 | } finally { 91 | router.refresh(); 92 | } 93 | }; 94 | 95 | return ( 96 |
97 | 104 |
105 |
106 |
107 | 123 | ( 126 | 127 | 128 | 134 | 135 | 136 | )} 137 | /> 138 | 146 | 147 | 148 |
149 |
150 | {isLoading && ( 151 |
152 | 153 |
154 | )} 155 | {messages.length === 0 && !isLoading && ( 156 | 157 | )} 158 |
159 | {messages.map((message) => ( 160 |
169 | {message.role === "user" ? : } 170 | ( 173 |
174 |
175 |                       
176 | ), 177 | code: ({ node, ...props }) => ( 178 | 179 | ), 180 | }} 181 | className="text-sm overflow-hidden leading-7" 182 | > 183 | {message.content || ""} 184 |
185 |
186 | ))} 187 |
188 |
189 |
190 |
191 | ); 192 | }; 193 | export default CodePage; 194 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | /** 4 | * The form schema for the code route. 5 | * Forces type safety on the form. 6 | */ 7 | export const formSchema = z.object({ 8 | prompt: z.string().min(1, { 9 | message: "Prompt is required.", 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { BotAvatar } from "@/components/avatar/BotAvatar"; 4 | import { UserAvatar } from "@/components/avatar/UserAvatar"; 5 | import { Empty } from "@/components/empty/Empty"; 6 | import { Heading } from "@/components/heading/Heading"; 7 | import { Loader } from "@/components/loader/Loader"; 8 | import { Button } from "@/components/ui/button"; 9 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 10 | import { Input } from "@/components/ui/input"; 11 | import { cn } from "@/lib/utils"; 12 | import { zodResolver } from "@hookform/resolvers/zod"; 13 | import axios from "axios"; 14 | import { MessageSquare } from "lucide-react"; 15 | import { useRouter } from "next/navigation"; 16 | import { ChatCompletionRequestMessage } from "openai"; 17 | import React, { useState } from "react"; 18 | import { useForm } from "react-hook-form"; 19 | import * as z from "zod"; 20 | import { formSchema } from "./constants"; 21 | import { toast } from "react-hot-toast"; 22 | import { useProModal } from "@/hooks/useProModal"; 23 | import { getToolByLabel } from "@/actions/getToolByLabel"; 24 | import { Tool } from "@/constants/constants"; 25 | 26 | type ConversationProps = {}; 27 | 28 | /** 29 | * Code page allows users to have conversations and generate text. 30 | * It uses the OpenAI API to generate text from a prompt. 31 | * It returns a code snippet with explanation. 32 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 33 | */ 34 | const ConversationPage: React.FC = () => { 35 | const router = useRouter(); 36 | const proModal = useProModal(); 37 | const [messages, setMessages] = useState([]); 38 | 39 | /** 40 | * Form for the prompt for the code generation. 41 | * Zod used for validation. 42 | */ 43 | const form = useForm>({ 44 | resolver: zodResolver(formSchema), 45 | defaultValues: { 46 | prompt: "", 47 | }, 48 | }); 49 | 50 | const tool: Tool | null = getToolByLabel("Conversation"); 51 | 52 | if (!tool) { 53 | return null; 54 | } 55 | 56 | const isLoading = form.formState.isSubmitting; 57 | 58 | /** 59 | * Submit the prompt to the API to generate a response. 60 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 61 | * @param values (string) prompt for the code generation 62 | */ 63 | const onSubmit = async (values: z.infer) => { 64 | try { 65 | /** 66 | * Message to be sent to the API. 67 | * It contains the prompt and the role (user or bot). 68 | */ 69 | const userMessage: ChatCompletionRequestMessage = { 70 | role: "user", 71 | content: values.prompt, 72 | }; 73 | const newMessages = [...messages, userMessage]; 74 | 75 | /** 76 | * Send the messages to the API. 77 | * Stores the response. 78 | */ 79 | const response = await axios.post("/api/conversation", { 80 | messages: newMessages, 81 | }); 82 | setMessages((current) => [...current, userMessage, response.data]); 83 | 84 | form.reset(); // clear input 85 | } catch (error: any) { 86 | // if the user is not subscribed and there are no remaining free tries, it will show a modal 87 | if (error.response.status === 403) { 88 | proModal.onOpen(); 89 | } else { 90 | console.log(error); 91 | toast.error("Could not answer your question"); 92 | } 93 | } finally { 94 | router.refresh(); 95 | } 96 | }; 97 | 98 | return ( 99 |
100 | 107 |
108 |
109 |
110 | 127 | ( 130 | 131 | 132 | 138 | 139 | 140 | )} 141 | /> 142 | 150 | 151 | 152 |
153 |
154 | {isLoading && ( 155 |
156 | 157 |
158 | )} 159 | {messages.length === 0 && !isLoading && ( 160 | 161 | )} 162 |
163 | {messages.map((message) => ( 164 |
173 | {message.role === "user" ? : } 174 |

{message.content}

175 |
176 | ))} 177 |
178 |
179 |
180 |
181 | ); 182 | }; 183 | export default ConversationPage; 184 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ArrowRight } from "lucide-react"; 4 | import { useRouter } from "next/navigation"; 5 | import { Card } from "@/components/ui/card"; 6 | import { cn } from "@/lib/utils"; 7 | import { tools } from "@/constants/constants"; 8 | 9 | /** 10 | * Dashboard page where users can select the tool they want to use. 11 | * This route is protected and is not accessible while user is not authenticated. 12 | */ 13 | export default function DashboardPage() { 14 | const router = useRouter(); 15 | 16 | return ( 17 |
18 |
19 |

20 | Explore the power of AI 21 |

22 |

23 | Chat with the smartest AI - Experience the power of AI 24 |

25 |
26 |
27 | {tools.map((tool) => ( 28 | router.push(tool.href)} 30 | key={tool.href} 31 | className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer" 32 | > 33 |
34 |
35 | 36 |
37 |
{tool.label}
38 |
39 | 40 |
41 | ))} 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | /** 4 | * The form schema for the code route. 5 | * Forces type safety on the form. 6 | */ 7 | export const formSchema = z.object({ 8 | prompt: z.string().min(1, { 9 | message: "Photo prompt is required", 10 | }), 11 | amount: z.string().min(1), 12 | resolution: z.string().min(1), 13 | }); 14 | 15 | /** 16 | * The options for the amount of photos to generate. 17 | * This is a constant because it is used in multiple places. 18 | * It is also used in the form schema. 19 | */ 20 | export const amountOptions = Array.from({ length: 5 }, (_, i) => ({ 21 | value: `${i + 1}`, 22 | label: `${i + 1} Photo${i > 0 ? "s" : ""}`, 23 | })); 24 | 25 | /** 26 | * Set of resolution options for the image route. 27 | */ 28 | export const resolutionOptions = [ 29 | { 30 | value: "256x256", 31 | label: "256x256", 32 | }, 33 | { 34 | value: "512x512", 35 | label: "512x512", 36 | }, 37 | { 38 | value: "1024x1024", 39 | label: "1024x1024", 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/empty/Empty"; 4 | import { Heading } from "@/components/heading/Heading"; 5 | import { Loader } from "@/components/loader/Loader"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Card, CardFooter } from "@/components/ui/card"; 8 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 9 | import { Input } from "@/components/ui/input"; 10 | import { 11 | Select, 12 | SelectContent, 13 | SelectItem, 14 | SelectTrigger, 15 | SelectValue, 16 | } from "@/components/ui/select"; 17 | import { zodResolver } from "@hookform/resolvers/zod"; 18 | import axios from "axios"; 19 | import { Download, ImageIcon } from "lucide-react"; 20 | import Image from "next/image"; 21 | import { useRouter } from "next/navigation"; 22 | import React, { useState } from "react"; 23 | import { useForm } from "react-hook-form"; 24 | import { toast } from "react-hot-toast"; 25 | import * as z from "zod"; 26 | import { amountOptions, formSchema, resolutionOptions } from "./constants"; 27 | import { useProModal } from "@/hooks/useProModal"; 28 | import { getToolByLabel } from "@/actions/getToolByLabel"; 29 | import { Tool } from "@/constants/constants"; 30 | 31 | type ImageProps = {}; 32 | 33 | /** 34 | * Page where users can generate images from a prompt. 35 | * It uses the OpenAI API to generate images from a prompt. 36 | * User can specify the amount of images to generate and the resolution. 37 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 38 | * @returns (JSX.Element): Image page allows users to generate images. 39 | */ 40 | const ImagePage: React.FC = () => { 41 | const router = useRouter(); 42 | const proModal = useProModal(); 43 | const [images, setImages] = useState([]); 44 | 45 | /** 46 | * Form for the prompt for the image generation. 47 | * Zod used for validation. 48 | */ 49 | const form = useForm>({ 50 | resolver: zodResolver(formSchema), 51 | defaultValues: { 52 | prompt: "", 53 | amount: "1", 54 | resolution: "512x512", 55 | }, 56 | }); 57 | 58 | const isLoading = form.formState.isSubmitting; 59 | 60 | const tool: Tool | null = getToolByLabel("Image Generation"); 61 | 62 | if (!tool) { 63 | return null; 64 | } 65 | 66 | /** 67 | * Submit the prompt to the API to generate images. 68 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 69 | * @param values (string) prompt for the image generation 70 | */ 71 | const onSubmit = async (values: z.infer) => { 72 | try { 73 | setImages([]); // empty the images array if there are any images from previous results 74 | 75 | const response = await axios.post("/api/image", values); // send the prompt to the API 76 | 77 | // get the urls of the generated images 78 | const urls: string[] = response.data.map( 79 | (image: { url: string }) => image.url 80 | ); 81 | 82 | setImages(urls); 83 | } catch (error: any) { 84 | // if the user is not subscribed and there are no remaining free tries, it will show a modal 85 | if (error.response.status === 403) { 86 | proModal.onOpen(); 87 | } else { 88 | console.log(error); 89 | toast.error("Could not generate image"); 90 | } 91 | } finally { 92 | router.refresh(); 93 | } 94 | }; 95 | 96 | return ( 97 |
98 | 105 |
106 |
107 | 124 | ( 127 | 128 | 129 | 135 | 136 | 137 | )} 138 | /> 139 | ( 143 | 144 | 163 | 164 | )} 165 | /> 166 | ( 170 | 171 | 190 | 191 | )} 192 | /> 193 | 201 | 202 | 203 | {isLoading && ( 204 |
205 | 206 |
207 | )} 208 | {images.length === 0 && !isLoading && ( 209 | 210 | )} 211 |
212 | {images.map((image) => ( 213 | 214 |
215 | Generated 221 |
222 | 223 | 231 | 232 |
233 | ))} 234 |
235 |
236 |
237 | ); 238 | }; 239 | export default ImagePage; 240 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | /** 4 | * The form schema for the code route. 5 | * Forces type safety on the form. 6 | */ 7 | export const formSchema = z.object({ 8 | prompt: z.string().min(1, { 9 | message: "Prompt is required.", 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/empty/Empty"; 4 | import { Heading } from "@/components/heading/Heading"; 5 | import { Loader } from "@/components/loader/Loader"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 8 | import { Input } from "@/components/ui/input"; 9 | import { zodResolver } from "@hookform/resolvers/zod"; 10 | import axios from "axios"; 11 | import { Music } from "lucide-react"; 12 | import { useRouter } from "next/navigation"; 13 | import React, { useState } from "react"; 14 | import { useForm } from "react-hook-form"; 15 | import * as z from "zod"; 16 | import { formSchema } from "./constants"; 17 | import { toast } from "react-hot-toast"; 18 | import { useProModal } from "@/hooks/useProModal"; 19 | import { getToolByLabel } from "@/actions/getToolByLabel"; 20 | import { Tool } from "@/constants/constants"; 21 | 22 | type MusicProps = {}; 23 | 24 | /** 25 | * Music page allows users to generate music from a prompt. 26 | * It uses the Replicate AI API to generate music from a prompt. 27 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 28 | * @returns (JSX.Element): Music page allows users to generate music. 29 | */ 30 | const MusicPage: React.FC = () => { 31 | const router = useRouter(); 32 | const proModal = useProModal(); 33 | const [music, setMusic] = useState(); 34 | 35 | /** 36 | * Form for the prompt for the music generation. 37 | * Zod used for validation. 38 | */ 39 | const form = useForm>({ 40 | resolver: zodResolver(formSchema), 41 | defaultValues: { 42 | prompt: "", 43 | }, 44 | }); 45 | 46 | const tool: Tool | null = getToolByLabel("Music Generation"); 47 | 48 | if (!tool) { 49 | return null; 50 | } 51 | 52 | const isLoading = form.formState.isSubmitting; 53 | 54 | console.log("music", tool); 55 | 56 | /** 57 | * Submit the prompt to the API to generate music. 58 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 59 | * @param values (string) prompt for the music generation 60 | */ 61 | const onSubmit = async (values: z.infer) => { 62 | try { 63 | setMusic(undefined); // reset music 64 | 65 | const response = await axios.post("/api/music", values); // call API to generate music 66 | 67 | setMusic(response.data.audio); // store music 68 | form.reset(); // reset form to empty 69 | } catch (error: any) { 70 | // if the user is not subscribed and there are no remaining free tries, it will show a modal 71 | if (error.response.status === 403) { 72 | proModal.onOpen(); 73 | } else { 74 | console.log(error); 75 | toast.error("Could not generate music"); 76 | } 77 | } finally { 78 | router.refresh(); 79 | } 80 | }; 81 | 82 | return ( 83 |
84 | 91 |
92 |
93 | 110 | ( 113 | 114 | 115 | 121 | 122 | 123 | )} 124 | /> 125 | 133 | 134 | 135 | {isLoading && ( 136 |
137 | 138 |
139 | )} 140 | {!music && !isLoading && ( 141 | 142 | )} 143 | {music && ( 144 | 147 | )} 148 |
149 |
150 | ); 151 | }; 152 | 153 | export default MusicPage; 154 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { SubscriptionButton } from "@/components/buttons/SubscriptionButton"; 2 | import { Heading } from "@/components/heading/Heading"; 3 | import { checkSubscription } from "@/lib/subscriptions"; 4 | import { Settings } from "lucide-react"; 5 | 6 | /** 7 | * Settings page which allows users to manage their account settings. 8 | * @returns (JSX.Element): Settings page allows users to manage their account settings. 9 | */ 10 | const SettingsPage = async () => { 11 | const isPro = await checkSubscription(); // check if the user is subscribed 12 | 13 | return ( 14 |
15 | 22 |
23 |
24 | {isPro 25 | ? "You are currently on a Pro plan." 26 | : "You are currently on a free plan."} 27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default SettingsPage; 35 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | /** 4 | * The form schema for the code route. 5 | * Forces type safety on the form. 6 | */ 7 | export const formSchema = z.object({ 8 | prompt: z.string().min(1, { 9 | message: "Prompt is required.", 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/empty/Empty"; 4 | import { Heading } from "@/components/heading/Heading"; 5 | import { Loader } from "@/components/loader/Loader"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 8 | import { Input } from "@/components/ui/input"; 9 | import { useProModal } from "@/hooks/useProModal"; 10 | import { zodResolver } from "@hookform/resolvers/zod"; 11 | import axios from "axios"; 12 | import { VideoIcon } from "lucide-react"; 13 | import { useRouter } from "next/navigation"; 14 | import React, { useState } from "react"; 15 | import { useForm } from "react-hook-form"; 16 | import * as z from "zod"; 17 | import { formSchema } from "./constants"; 18 | import toast from "react-hot-toast"; 19 | import { getToolByLabel } from "@/actions/getToolByLabel"; 20 | import { Tool } from "@/constants/constants"; 21 | 22 | type VideoProps = {}; 23 | 24 | /** 25 | * Video page allows users to generate video from a prompt. 26 | * It uses the Replicate AI API to generate video from a prompt. 27 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 28 | */ 29 | const VideoPage: React.FC = () => { 30 | const router = useRouter(); 31 | const proModal = useProModal(); 32 | const [video, setVideo] = useState(); 33 | 34 | /** 35 | * Form for the prompt for the video generation. 36 | * Zod used for validation. 37 | */ 38 | const form = useForm>({ 39 | resolver: zodResolver(formSchema), 40 | defaultValues: { 41 | prompt: "", 42 | }, 43 | }); 44 | 45 | const tool: Tool | null = getToolByLabel("Video Generation"); 46 | 47 | if (!tool) { 48 | return null; 49 | } 50 | 51 | const isLoading = form.formState.isSubmitting; 52 | 53 | /** 54 | * Submit the prompt to the API to generate video. 55 | * If the user is not subscribed and there are no remaining free tries, it will show a modal. 56 | * @param values (string) prompt for the video generation 57 | */ 58 | const onSubmit = async (values: z.infer) => { 59 | try { 60 | setVideo(undefined); // reset video 61 | 62 | const response = await axios.post("/api/video", values); // call API to generate video 63 | 64 | setVideo(response.data[0]); // store video 65 | form.reset(); // reset form to empty 66 | } catch (error: any) { 67 | // if the user is not subscribed and there are no remaining free tries, it will show a modal 68 | if (error.response.status === 403) { 69 | proModal.onOpen(); 70 | } else { 71 | console.log(error); 72 | toast.error("Could not generate video"); 73 | } 74 | } finally { 75 | router.refresh(); 76 | } 77 | }; 78 | 79 | return ( 80 |
81 | 88 |
89 |
90 | 106 | ( 109 | 110 | 111 | 117 | 118 | 119 | )} 120 | /> 121 | 129 | 130 | 131 | {isLoading && ( 132 |
133 | 134 |
135 | )} 136 | {!video && !isLoading && } 137 | {video && ( 138 | 144 | )} 145 |
146 |
147 | ); 148 | }; 149 | 150 | export default VideoPage; 151 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar/Navbar"; 2 | import { Sidebar } from "@/components/sidebar/Sidebar"; 3 | import { getApiLimitCount } from "@/lib/api-limit"; 4 | import { checkSubscription } from "@/lib/subscriptions"; 5 | 6 | /** 7 | * Specifies the layout for all pages in the dashboard folder. 8 | * It adds a sidebar and navbar to the page. 9 | * @param {children} (React.ReactNode): children components (pages from the dashboard folder) 10 | * @returns (JSX.Element): dashboard layout for all pages in the dashboard folder 11 | */ 12 | const DashboardLayout = async ({ children }: { children: React.ReactNode }) => { 13 | /** 14 | * Must be passed from a server component (layout) to the client component. 15 | * This is because the action check is in the server. 16 | * This cannot be called from a client component directly. 17 | */ 18 | const apiLimitCount = await getApiLimitCount(); 19 | const isPro = await checkSubscription(); 20 | 21 | return ( 22 |
23 |
27 | 28 |
29 |
30 | 31 | {children} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default DashboardLayout; 38 | -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Specifies the layout for all pages in the landing folder 3 | * @param {children} (React.ReactNode): children components for the landing page 4 | * @returns (JSX.Element): landing layout for all pages in the landing folder 5 | */ 6 | const LandingLayout = ({ children }: { children: React.ReactNode }) => { 7 | return ( 8 |
9 |
{children}
10 |
11 | ); 12 | }; 13 | 14 | export default LandingLayout; 15 | -------------------------------------------------------------------------------- /app/(landing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { LandingHero } from "@/components/pages/landing/LandingHero"; 2 | import { LandingNavbar } from "@/components/pages/landing/LandingNavbar"; 3 | 4 | /** 5 | * Landing page for the application. 6 | */ 7 | const LandingPage = () => { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default LandingPage; 17 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | 2 | # **`app` Directory** 3 | The app directory is a new feature introduced in Next.js 13 that allows you to create layouts, nested routes, and use server components by default . It works alongside the pages directory, which handles the file system-based routing. 4 | Inside the app directory, you can create folders that correspond to the routes in your application. Each folder should contain a page.jsx file that defines the UI for that route. For example, `app/profile/settings/page.jsx` will render the `/profile/settings` route. 5 | You can also create optional files inside each folder, such as: 6 | - `loading.jsx`: This file will wrap the page inside a React suspense boundary and show a loading component while the page is being fetched or rendered. 7 | - `error.jsx`: This file will wrap the page inside a React error boundary and show an error component if any error occurs inside the page. 8 | - `layout.jsx`: This file will define a layout component that can be used to wrap the page and provide common UI elements such as headers, footers, navigation, etc. 9 | 10 | The app directory also supports nested layouts, which means you can have different layouts for different sections of your app. For example, you can have a root layout for the entire app, and then a nested layout for a specific route group. 11 | 12 | The app directory leverages server components, which are a new feature of React that allows you to write components that run on the server and stream data to the client. This can improve performance, reduce bundle size, and enable new capabilities such as accessing databases or file systems directly from your components. 13 | 14 | # **Folders in the `app` Directory** 15 | These are some of the folders that you can create inside the app directory and their functionality: 16 | - `(site)`: This is a special folder that contains global files such as _document.jsx, _app.jsx, _error.jsx, etc. These files are similar to the ones in the pages directory, but they apply to the entire app instead of individual pages. 17 | - `actions`: This folder can contain files that define actions or mutations for your app state management. For example, you can use Zustand or Recoil to create global or local state atoms and selectors. 18 | - `api`: This folder can contain files that define API routes for your app. These are server-side functions that can handle requests from the client and return data or perform actions. For example, you can use Next.js API Routes or Next Connect to create RESTful or GraphQL endpoints. 19 | - `components`: This folder can contain files that define reusable UI components for your app. These are regular React components that can accept props and render elements. For example, you can use Chakra UI or Tailwind CSS to create styled components. 20 | - `context`: This folder can contain files that define React context providers for your app. These are components that can provide data or functionality to their descendants via the React context API. For example, you can use React Query or SWR to create data fetching and caching providers. 21 | - `hooks`: This folder can contain files that define custom hooks for your app. These are functions that can encapsulate logic and state and return values or functions to be used by other components. For example, you can use React Hook Form or Formik to create form validation hooks. 22 | - `libs`: This folder can contain files that define utility functions or libraries for your app. These are functions that can perform common tasks or calculations and return values or objects. For example, you can use Lodash or Date-Fns to create helper functions for arrays, objects, strings, dates, etc. 23 | - `types`: This folder can contain files that define type definitions or interfaces for your app. These are declarations that can describe the shape or structure of data or objects and help with type checking and code completion. For example, you can use TypeScript or PropTypes to create type annotations for your components, props, state, etc. 24 | 25 | # **Routes** 26 | The routes in the app directory are defined by the folders and files inside it. Each folder corresponds to a segment in the URL path, and each file corresponds to a component or function related to that route. 27 | 28 | Each route folder has its own components folder which is only accessible to itself. This means that any component placed inside this folder will not be available to other routes unless it is explicitly imported. 29 | 30 | To create dynamic routes in Next.js 13, you have to add brackets `[]` to a file or folder name. This indicates that the segment name will be dynamic and filled in at request time or prerendered at build time. 31 | -------------------------------------------------------------------------------- /app/api/README.md: -------------------------------------------------------------------------------- 1 | The `api` directory in the `app` folder of this Next.js 13 project contains files that define the API routes and server-side functionality of the application. These files handle incoming requests, process data, and generate appropriate responses. Here's a general description of the functionality of the `api` directory: 2 | 3 | 1. **Route Handlers**: The `api` directory contains various files that serve as route handlers for different API endpoints. These files define the behavior and logic associated with specific routes. 4 | 5 | 2. **Request Handling**: Each route handler file defines functions or middleware to handle incoming HTTP requests. These functions may parse request bodies, validate input, and perform necessary operations based on the request type (GET, POST, DELETE, etc.). 6 | 7 | 3. **Data Processing**: Inside the route handler functions, there may be code to process and manipulate data received from the client or retrieved from databases or external services. This processing step involves applying business logic, performing calculations, or transforming the data as required by the application. 8 | 9 | 4. **Database Operations**: In some cases, the route handlers may interact with a database to retrieve or store data. This could involve executing queries, creating or updating records, or performing data validation. 10 | 11 | 5. **Response Generation**: After processing the request and performing necessary operations, the route handlers generate appropriate responses. This includes constructing response bodies, setting response headers, and returning the response to the client. 12 | 13 | 6. **Error Handling**: The route handlers may include error handling logic to handle and respond to any errors that occur during request processing. This ensures that the client receives informative error messages or appropriate HTTP status codes when necessary. 14 | 15 | 7. **Authentication and Authorization**: Depending on the application's requirements, the route handlers may implement authentication and authorization mechanisms to protect certain routes or validate user access. This could involve validating session tokens, checking user permissions, or verifying user credentials. 16 | 17 | Overall, the `api` directory in the `app` folder of this Next.js 13 project serves as the backend of the application, handling incoming requests, processing data, and generating responses. It encapsulates the server-side functionality required for the application's API endpoints and serves as the bridge between the client-side and server-side components of the application. -------------------------------------------------------------------------------- /app/api/code/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscriptions"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; 6 | 7 | /** 8 | * Loads the OpenAI API key from the environment variables. 9 | */ 10 | const configuration = new Configuration({ 11 | apiKey: process.env.OPENAI_API_KEY, 12 | }); 13 | 14 | const openai = new OpenAIApi(configuration); // create an OpenAI API instance 15 | 16 | /** 17 | * Custom prompt for the conversation model. 18 | * This specifies the model and instructions on how to present the data. 19 | */ 20 | const instructionMessage: ChatCompletionRequestMessage = { 21 | role: "system", 22 | content: 23 | "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.", 24 | }; 25 | 26 | /** 27 | * POST /api/code 28 | * Uses the same OpenAI API as the conversation route but has a different prompt. 29 | * This allows it to be used for code generation. 30 | * The API route is protected by Clerk and requires a valid session. 31 | * The message is required. 32 | * The response is a JSON object with the generated text. 33 | * This API route is rate limited if there is no active subscription. 34 | * @param req (Request): The incoming request object which is the code description (JSON) 35 | * @returns (NextResponse): The response object (JSON) 36 | */ 37 | export async function POST(req: Request) { 38 | try { 39 | const { userId } = auth(); // get the user ID from the session (Clerk) 40 | const body = await req.json(); // get the request body 41 | const { messages } = body; // get the messages from the request body 42 | 43 | // if the user ID is not valid, return an unauthorized response 44 | if (!userId) { 45 | return new NextResponse("Unauthorized", { status: 401 }); 46 | } 47 | 48 | // if the OpenAI API key is not configured, return an internal error response 49 | if (!configuration.apiKey) { 50 | return new NextResponse("OpenAI API Key not configured.", { 51 | status: 500, 52 | }); 53 | } 54 | 55 | // if the messages are not valid, return a bad request response 56 | if (!messages) { 57 | return new NextResponse("Messages are required", { status: 400 }); 58 | } 59 | 60 | const freeTrial = await checkApiLimit(); // check if the user is on a free trial 61 | const isPro = await checkSubscription(); // check if the user is on a pro subscription 62 | 63 | // if the user is not on a free trial and not on a pro subscription, return a forbidden response 64 | if (!freeTrial && !isPro) { 65 | return new NextResponse( 66 | "Free trial has expired. Please upgrade to pro.", 67 | { status: 403 } 68 | ); 69 | } 70 | 71 | // generate the response from OpenAI 72 | const response = await openai.createChatCompletion({ 73 | model: "gpt-3.5-turbo", // the model to use 74 | messages: [instructionMessage, ...messages], // add the instruction message to the messages 75 | }); 76 | 77 | // if the user is not on a pro subscription, increment the API limit 78 | if (!isPro) { 79 | await incrementApiLimit(); 80 | } 81 | 82 | return NextResponse.json(response.data.choices[0].message); // return the response from OpenAI 83 | } catch (error) { 84 | console.log("[CODE_ERROR]", error); 85 | return new NextResponse("Internal Error", { status: 500 }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/api/conversation/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscriptions"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import { Configuration, OpenAIApi } from "openai"; 6 | 7 | /** 8 | * Loads the OpenAI API key from the environment variables. 9 | */ 10 | const configuration = new Configuration({ 11 | apiKey: process.env.OPENAI_API_KEY, 12 | }); 13 | 14 | const openai = new OpenAIApi(configuration); // create an OpenAI API instance 15 | 16 | /** 17 | * POST /api/conversation 18 | * API route for conversations and text generation with OpenAI's GPT-3.5 model. 19 | * This is for generic conversations. 20 | * The API route is protected by Clerk and requires a valid session. 21 | * The message is required. 22 | * The response is a JSON object with the generated text. 23 | * This API route is rate limited if there is no active subscription. 24 | * @param req (Request): The incoming request object which is the message (JSON) 25 | * @returns (NextResponse): The response object (JSON) 26 | */ 27 | export async function POST(req: Request) { 28 | try { 29 | const { userId } = auth(); // get the user ID from the session (Clerk) 30 | const body = await req.json(); // get the request body 31 | const { messages } = body; // get the messages from the request body 32 | 33 | // if the user ID is not valid, return an unauthorized response 34 | if (!userId) { 35 | return new NextResponse("Unauthorized", { status: 401 }); 36 | } 37 | 38 | // if the OpenAI API key is not configured, return an internal error response 39 | if (!configuration.apiKey) { 40 | return new NextResponse("OpenAI API Key not configured.", { 41 | status: 500, 42 | }); 43 | } 44 | 45 | // if the messages are not valid, return a bad request response 46 | if (!messages) { 47 | return new NextResponse("Messages are required", { status: 400 }); 48 | } 49 | 50 | const freeTrial = await checkApiLimit(); // check if the user is on a free trial 51 | const isPro = await checkSubscription(); // check if the user is on a pro subscription 52 | 53 | // if the user is not on a free trial and not on a pro subscription, return a forbidden response 54 | if (!freeTrial && !isPro) { 55 | return new NextResponse( 56 | "Free trial has expired. Please upgrade to pro.", 57 | { status: 403 } 58 | ); 59 | } 60 | 61 | // generate the response from OpenAI 62 | const response = await openai.createChatCompletion({ 63 | model: "gpt-3.5-turbo", // the model to use 64 | messages, // the messages to model 65 | }); 66 | 67 | // if the user is not on a pro subscription, increment the API limit 68 | if (!isPro) { 69 | await incrementApiLimit(); 70 | } 71 | 72 | return NextResponse.json(response.data.choices[0].message); // response from OpenAI 73 | } catch (error) { 74 | console.log("[CONVERSATION_ERROR]", error); 75 | return new NextResponse("Internal Error", { status: 500 }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/api/image/route.ts: -------------------------------------------------------------------------------- 1 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 2 | import { checkSubscription } from "@/lib/subscriptions"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import { Configuration, OpenAIApi } from "openai"; 6 | 7 | /** 8 | * Loads the OpenAI API key from the environment variables. 9 | */ 10 | const configuration = new Configuration({ 11 | apiKey: process.env.OPENAI_API_KEY, 12 | }); 13 | 14 | const openai = new OpenAIApi(configuration); // create an OpenAI API instance 15 | 16 | /** 17 | * POST /api/image 18 | * API route for image generation with OpenAI's DALL-E model. 19 | * This can generate a single image or multiple images. 20 | * The images are of specified resolutions. 21 | * The API route is protected by Clerk and requires a valid session. 22 | * The prompt, amount, and resolution are required. 23 | * @param req (Request): The incoming request object 24 | * @returns (NextResponse): The response object (JSON) 25 | */ 26 | export async function POST(req: Request) { 27 | try { 28 | const { userId } = auth(); // get the user ID from the session (Clerk) 29 | const body = await req.json(); // get the request body 30 | // get the prompt, amount, and resolution from the request body, defaulting to 1 and 512x512 31 | const { prompt, amount = 1, resolution = "512x512" } = body; 32 | 33 | // if the user ID is not valid, return an unauthorized response 34 | if (!userId) { 35 | return new NextResponse("Unauthorized", { status: 401 }); 36 | } 37 | 38 | // if the OpenAI API key is not configured, return an internal error response 39 | if (!configuration.apiKey) { 40 | return new NextResponse("OpenAI API Key not configured.", { 41 | status: 500, 42 | }); 43 | } 44 | 45 | // if the prompt is not valid, return a bad request response 46 | if (!prompt) { 47 | return new NextResponse("Prompt is required", { status: 400 }); 48 | } 49 | 50 | // if the amount of images is not valid, return a bad request response 51 | if (!amount) { 52 | return new NextResponse("Amount is required", { status: 400 }); 53 | } 54 | 55 | // if the resolution for images is not valid, return a bad request response 56 | if (!resolution) { 57 | return new NextResponse("Resolution is required", { status: 400 }); 58 | } 59 | 60 | const freeTrial = await checkApiLimit(); // check if the user is on a free trial 61 | const isPro = await checkSubscription(); // check if the user is on a pro subscription 62 | 63 | // if the user is not on a free trial and not on a pro subscription, return a forbidden response 64 | if (!freeTrial && !isPro) { 65 | return new NextResponse( 66 | "Free trial has expired. Please upgrade to pro.", 67 | { status: 403 } 68 | ); 69 | } 70 | 71 | // generate the images with the prompt, amount, and resolution 72 | const response = await openai.createImage({ 73 | prompt, // description of the image 74 | n: parseInt(amount, 10), // amount of images to generate 75 | size: resolution, // resolution of the images 76 | }); 77 | 78 | // if the user is not on a pro subscription, increment the API limit 79 | if (!isPro) { 80 | await incrementApiLimit(); 81 | } 82 | 83 | return NextResponse.json(response.data.data); // return the response data 84 | } catch (error) { 85 | console.log("[IMAGE_ERROR]", error); 86 | return new NextResponse("Internal Error", { status: 500 }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/api/music/route.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 5 | import { checkSubscription } from "@/lib/subscriptions"; 6 | 7 | /** 8 | * Loads the Replicate API key from the environment variables. 9 | */ 10 | const replicate = new Replicate({ 11 | auth: process.env.REPLICATE_API_TOKEN!, 12 | }); 13 | 14 | /** 15 | * POST /api/music 16 | * API route for music generation with Replicate's Riffusion model. 17 | * This can generate a single song from a prompt. 18 | * The API route is protected by Clerk and requires a valid session. 19 | * The prompt is required. 20 | * @param req (Request): The incoming request object which is the description of the song (JSON) 21 | * @returns (NextResponse): The response object which is the generated song (JSON) 22 | */ 23 | export async function POST(req: Request) { 24 | try { 25 | const { userId } = auth(); // get the user ID from the session (Clerk) 26 | const body = await req.json(); // get the request body 27 | const { prompt } = body; // get the prompt from the request body 28 | 29 | // if the user ID is not valid, return an unauthorized response 30 | if (!userId) { 31 | return new NextResponse("Unauthorized", { status: 401 }); 32 | } 33 | 34 | // if the Replicate API key is not configured, return an internal error response 35 | if (!prompt) { 36 | return new NextResponse("Prompt is required", { status: 400 }); 37 | } 38 | 39 | const freeTrial = await checkApiLimit(); // check if the user is on a free trial 40 | const isPro = await checkSubscription(); // check if the user is on a pro subscription 41 | 42 | // if the user is not on a free trial and not on a pro subscription, return a forbidden response 43 | if (!freeTrial && !isPro) { 44 | return new NextResponse( 45 | "Free trial has expired. Please upgrade to pro.", 46 | { status: 403 } 47 | ); 48 | } 49 | 50 | // generate the response from Replicate 51 | const response = await replicate.run( 52 | // model used 53 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", 54 | { 55 | input: { 56 | prompt_a: prompt, // prompt for the first song 57 | }, 58 | } 59 | ); 60 | 61 | // if the user is not on a pro subscription, increment the API limit 62 | if (!isPro) { 63 | await incrementApiLimit(); 64 | } 65 | 66 | return NextResponse.json(response); // return the response from Replicate 67 | } catch (error) { 68 | console.log("[MUSIC_ERROR]", error); 69 | return new NextResponse("Internal Error", { status: 500 }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/api/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, currentUser } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import prismadb from "@/lib/prismadb"; 4 | import { stripe } from "@/lib/stripe"; 5 | import { absoluteUrl } from "@/lib/utils"; 6 | 7 | /** 8 | * Settings URL for the app where the user can manage their subscription. 9 | */ 10 | const settingsUrl = absoluteUrl("/settings"); 11 | 12 | /** 13 | * GET /api/stripe 14 | * API route for Stripe billing portal. 15 | * This is used to manage subscriptions. 16 | * The API route is protected by Clerk and requires a valid session. 17 | * 18 | * If the user is subscribed, a Stripe billing portal session is created. 19 | * This is where the user can manage their subscription. 20 | * 21 | * If the user is not subscribed, a Stripe checkout session is created. 22 | * This is where the user can subscribe. 23 | * 24 | * @returns (NextResponse): The response object (JSON) 25 | */ 26 | export async function GET() { 27 | try { 28 | const { userId } = auth(); // get the user ID from the session (Clerk) 29 | const user = await currentUser(); // get the current user 30 | 31 | // if the user ID is not valid or the user is not valid, return an unauthorized response 32 | if (!userId || !user) { 33 | return new NextResponse("Unauthorized", { status: 401 }); 34 | } 35 | 36 | // get subscription details for the current user 37 | const userSubscription = await prismadb.userSubscription.findUnique({ 38 | where: { 39 | userId, 40 | }, 41 | }); 42 | 43 | // if the user is subscribed, create a Stripe billing portal session to manage the subscription 44 | if (userSubscription && userSubscription.stripeCustomerId) { 45 | const stripeSession = await stripe.billingPortal.sessions.create({ 46 | customer: userSubscription.stripeCustomerId, // customer ID 47 | return_url: settingsUrl, // return URL 48 | }); 49 | 50 | return new NextResponse(JSON.stringify({ url: stripeSession.url })); 51 | } 52 | 53 | // if user is not subscribed, create a Stripe checkout session to subscribe the user 54 | const stripeSession = await stripe.checkout.sessions.create({ 55 | success_url: settingsUrl, // redirect URL after successful payment 56 | cancel_url: settingsUrl, // redirect URL after cancelled payment 57 | payment_method_types: ["card"], // payment method types 58 | mode: "subscription", // subscription mode 59 | billing_address_collection: "auto", // billing address collection 60 | customer_email: user.emailAddresses[0].emailAddress, // customer email 61 | line_items: [ 62 | { 63 | price_data: { 64 | currency: "GBP", 65 | product_data: { 66 | name: "Magician Plus", 67 | description: "Unlimited AI Generations", 68 | }, 69 | unit_amount: 999, // £9.99 70 | recurring: { 71 | interval: "month", 72 | }, 73 | }, 74 | quantity: 1, 75 | }, 76 | ], 77 | metadata: { 78 | userId, // used to check and keep track of users who are subscribed 79 | }, 80 | }); 81 | 82 | return new NextResponse(JSON.stringify({ url: stripeSession.url })); // return the Stripe checkout session URL 83 | } catch (error) { 84 | console.log("[STRIPE_ERROR]", error); 85 | return new NextResponse("Internal Error", { status: 500 }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/api/video/route.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit"; 5 | import { checkSubscription } from "@/lib/subscriptions"; 6 | 7 | /** 8 | * Loads the Replicate API key from the environment variables. 9 | */ 10 | const replicate = new Replicate({ 11 | auth: process.env.REPLICATE_API_TOKEN!, 12 | }); 13 | 14 | /** 15 | * POST /api/video 16 | * API route for video generation with Replicate's Zeroscope model. 17 | * This can generate a single video from a prompt. 18 | * The API route is protected by Clerk and requires a valid session. 19 | * The prompt is required. 20 | * @param req (Request): The incoming request object which is the description of the song (JSON) 21 | * @returns (NextResponse): The response object which is the generated song (JSON) 22 | */ 23 | export async function POST(req: Request) { 24 | try { 25 | const { userId } = auth(); // get the user ID from the session (Clerk) 26 | const body = await req.json(); // get the request body 27 | const { prompt } = body; // get the prompt from the request body 28 | 29 | // if the user ID is not valid, return an unauthorized response 30 | if (!userId) { 31 | return new NextResponse("Unauthorized", { status: 401 }); 32 | } 33 | 34 | // if the Replicate API key is not configured, return an internal error response 35 | if (!prompt) { 36 | return new NextResponse("Prompt is required", { status: 400 }); 37 | } 38 | 39 | const freeTrial = await checkApiLimit(); // check if the user is on a free trial 40 | const isPro = await checkSubscription(); // check if the user is on a pro subscription 41 | 42 | // if the user is not on a free trial and not on a pro subscription, return a forbidden response 43 | if (!freeTrial && !isPro) { 44 | return new NextResponse( 45 | "Free trial has expired. Please upgrade to pro.", 46 | { status: 403 } 47 | ); 48 | } 49 | const response = await replicate.run( 50 | "anotherjesse/zeroscope-v2-xl:71996d331e8ede8ef7bd76eba9fae076d31792e4ddf4ad057779b443d6aea62f", 51 | { 52 | input: { 53 | prompt, // description of the video 54 | }, 55 | } 56 | ); 57 | 58 | // if the user is not on a pro subscription, increment the API limit 59 | if (!isPro) { 60 | await incrementApiLimit(); 61 | } 62 | 63 | return NextResponse.json(response); // return the response from Replicate 64 | } catch (error) { 65 | console.log("[VIDEO_ERROR]", error); 66 | return new NextResponse("Internal Error", { status: 500 }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { headers } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | import prismadb from "@/lib/prismadb"; 5 | import { stripe } from "@/lib/stripe"; 6 | 7 | /** 8 | * Webhook listener for Stripe. 9 | * This is used to listen for events from Stripe. 10 | * The webhook is protected by a secret key. 11 | * This webhook is used to update the subscription details for the user. 12 | * It is a public API route. 13 | * @param req (Request): The incoming request object (JSON) 14 | * @returns (NextResponse): The response object (JSON) 15 | */ 16 | export async function POST(req: Request) { 17 | const body = await req.text(); // get the request body 18 | const signature = headers().get("Stripe-Signature") as string; // get the Stripe signature 19 | 20 | let event: Stripe.Event; // create an event object 21 | 22 | // verify the signature 23 | try { 24 | event = stripe.webhooks.constructEvent( 25 | body, 26 | signature, 27 | process.env.STRIPE_WEBHOOK_SECRET! 28 | ); 29 | } catch (error: any) { 30 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); 31 | } 32 | 33 | const session = event.data.object as Stripe.Checkout.Session; // get the session object 34 | 35 | // new user subscribes 36 | if (event.type === "checkout.session.completed") { 37 | const subscription = await stripe.subscriptions.retrieve( 38 | session.subscription as string // get the subscription object for the session 39 | ); 40 | 41 | // if the user ID is not valid, return a bad request response 42 | if (!session?.metadata?.userId) { 43 | return new NextResponse("User id is required", { status: 400 }); 44 | } 45 | 46 | // user subscribes directly created new account 47 | await prismadb.userSubscription.create({ 48 | data: { 49 | userId: session?.metadata?.userId, 50 | stripeSubscriptionId: subscription.id, 51 | stripeCustomerId: subscription.customer as string, 52 | stripePriceId: subscription.items.data[0].price.id, 53 | stripeCurrentPeriodEnd: new Date( 54 | subscription.current_period_end * 1000 55 | ), 56 | }, 57 | }); 58 | } 59 | 60 | // existing user subscribes 61 | if (event.type === "invoice.payment_succeeded") { 62 | const subscription = await stripe.subscriptions.retrieve( 63 | session.subscription as string // get the subscription object for the session 64 | ); 65 | 66 | // update the subscription details for the user 67 | await prismadb.userSubscription.update({ 68 | where: { 69 | stripeSubscriptionId: subscription.id, 70 | }, 71 | data: { 72 | stripePriceId: subscription.items.data[0].price.id, 73 | stripeCurrentPeriodEnd: new Date( 74 | subscription.current_period_end * 1000 75 | ), 76 | }, 77 | }); 78 | } 79 | 80 | return new NextResponse(null, { status: 200 }); 81 | } 82 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbeps/magician-ai/57283c12cf3fc2cfa08f3d9d729be30b7fb8d30b/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: 0 0% 3.9%; 15 | 16 | --muted: 0 0% 96.1%; 17 | --muted-foreground: 0 0% 45.1%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 0 0% 3.9%; 21 | 22 | --card: 0 0% 100%; 23 | --card-foreground: 0 0% 3.9%; 24 | 25 | --border: 0 0% 89.8%; 26 | --input: 0 0% 89.8%; 27 | 28 | --primary: 0 0% 9%; 29 | --primary-foreground: 0 0% 98%; 30 | 31 | --secondary: 0 0% 96.1%; 32 | --secondary-foreground: 0 0% 9%; 33 | 34 | --accent: 0 0% 96.1%; 35 | --accent-foreground: 0 0% 9%; 36 | 37 | --destructive: 0 84.2% 60.2%; 38 | --destructive-foreground: 0 0% 98%; 39 | 40 | --ring: 0 0% 63.9%; 41 | 42 | --radius: 0.5rem; 43 | } 44 | 45 | .dark { 46 | --background: 0 0% 3.9%; 47 | --foreground: 0 0% 98%; 48 | 49 | --muted: 0 0% 14.9%; 50 | --muted-foreground: 0 0% 63.9%; 51 | 52 | --popover: 0 0% 3.9%; 53 | --popover-foreground: 0 0% 98%; 54 | 55 | --card: 0 0% 3.9%; 56 | --card-foreground: 0 0% 98%; 57 | 58 | --border: 0 0% 14.9%; 59 | --input: 0 0% 14.9%; 60 | 61 | --primary: 0 0% 98%; 62 | --primary-foreground: 0 0% 9%; 63 | 64 | --secondary: 0 0% 14.9%; 65 | --secondary-foreground: 0 0% 98%; 66 | 67 | --accent: 0 0% 14.9%; 68 | --accent-foreground: 0 0% 98%; 69 | 70 | --destructive: 0 62.8% 30.6%; 71 | --destructive-foreground: 0 85.7% 97.3%; 72 | 73 | --ring: 0 0% 14.9%; 74 | } 75 | } 76 | 77 | @layer base { 78 | * { 79 | @apply border-border; 80 | } 81 | 82 | body { 83 | @apply bg-background text-foreground; 84 | } 85 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { dark } from "@clerk/themes"; 6 | import { ToasterProvider } from "@/components/toast/toast-provider"; 7 | import { ModalProvider } from "@/providers/ModalProvider"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Magician", 13 | description: "An AI content generator", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 30 | 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ArrowRight } from "lucide-react"; 4 | import { useRouter } from "next/navigation"; 5 | import { Card } from "@/components/ui/card"; 6 | import { cn } from "@/lib/utils"; 7 | import { tools } from "@/constants/constants"; 8 | import { Button } from "@/components/ui/button"; 9 | import Link from "next/link"; 10 | 11 | /** 12 | * This route is protected and is not accessible while user is not authenticated. 13 | */ 14 | export default function DashboardPage() { 15 | const router = useRouter(); 16 | 17 | return ( 18 |
19 |
20 |

21 | 404: The page does not exist 22 |

23 |

24 | Use the links bellow to see some magic 25 |

26 |
27 | 28 | 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /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": "neutral", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | The `components` folder in the root of the `app` directory, as well as the `components` folders within each route, contain reusable UI components that are used throughout the application. These components serve various purposes and provide consistent visual elements and functionality across different parts of the application. Here's a general description of the functionality of the `components` folder: 2 | 3 | 1. **Reusable UI Components**: The `components` folder contains reusable UI components that can be used in multiple parts of the application. These components are designed to be modular and encapsulate specific functionality or visual elements. Examples of reusable UI components include buttons, forms, navigation bars, modals, alerts, and input fields. 4 | 5 | 2. **Consistent Styling**: The components in the `components` folder typically follow a consistent design and styling approach to maintain a cohesive visual identity across the application. They may utilize CSS-in-JS solutions like Styled Components or CSS modules to encapsulate styles within the component and avoid CSS class conflicts. 6 | 7 | 3. **Layout Components**: The `components` folder may include layout components that define the overall structure and organization of the application's pages or sections. These components provide a consistent layout for headers, footers, sidebars, and other structural elements. 8 | 9 | 4. **Higher-Order Components (HOCs)**: HOCs may be present in the `components` folder. These components wrap other components and enhance their functionality or behavior. HOCs can be used for tasks such as authentication, routing, or data fetching, providing a reusable and declarative way to apply certain behaviors to multiple components. 10 | 11 | 5. **Component Composition**: The components in the `components` folder are often composed together to build larger, more complex UI elements or page layouts. This promotes code reusability and modularity, allowing developers to create new UI elements by combining existing components. 12 | 13 | 6. **Custom Hooks**: The `components` folder may also include custom hooks that encapsulate reusable logic or state management for specific functionalities. These hooks can be used across different components to provide consistent behavior or data handling. 14 | 15 | 7. **Route-Specific Components**: In addition to the global `components` folder in the root of the `app` directory, each route may have its own `components` folder. These route-specific components are tailored to the unique requirements of that particular route and may contain components that are specific to that route's functionality or visual representation. 16 | -------------------------------------------------------------------------------- /components/avatar/BotAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 2 | 3 | /** 4 | * Avatar for the AI. 5 | * This is used in the chatbot. 6 | * @returns (JSX.Element): bot avatar component 7 | */ 8 | export const BotAvatar = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/avatar/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { useUser } from "@clerk/nextjs"; 2 | 3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 4 | 5 | /** 6 | * Avatar component that displays the user's avatar. 7 | * If the user does not have an avatar, it will display their initials. 8 | * @returns (JSX.Element): user avatar component 9 | */ 10 | export const UserAvatar = () => { 11 | const { user } = useUser(); 12 | 13 | return ( 14 | 15 | 16 | 17 | {user?.firstName?.charAt(0)} 18 | {user?.lastName?.charAt(0)} 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /components/buttons/SubscriptionButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useState } from "react"; 5 | import { Zap } from "lucide-react"; 6 | import { toast } from "react-hot-toast"; 7 | import { Button } from "@/components/ui/button"; 8 | 9 | /** 10 | * Button specifically used to manage subscriptions. 11 | * If the user is subscribed, it will allow them to manage their subscription. 12 | * If the user is not subscribed, it will allow them to upgrade. 13 | * The API call automatically redirects the user to the Stripe checkout page or subscription management page. 14 | * @param {isPro} (boolean): whether the user is subscribed 15 | * @returns (JSX.Element): subscription button component 16 | */ 17 | export const SubscriptionButton = ({ isPro = false }: { isPro: boolean }) => { 18 | const [loading, setLoading] = useState(false); 19 | 20 | /** 21 | * Function that is called when the user clicks the button. 22 | * This calls the Stripe API to redirect the user to the checkout page or subscription management page. 23 | */ 24 | const onClick = async () => { 25 | try { 26 | setLoading(true); 27 | 28 | const response = await axios.get("/api/stripe"); // calls the Stripe API 29 | 30 | window.location.href = response.data.url; // redirects the user to the Stripe checkout page or subscription management page 31 | } catch (error) { 32 | toast.error("Something went wrong"); 33 | } finally { 34 | setLoading(false); 35 | } 36 | }; 37 | 38 | return ( 39 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /components/empty/Empty.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface EmptyProps { 4 | label: string; 5 | } 6 | 7 | /** 8 | * Empty component which is displayed when there is no data to be displayed. 9 | * @param {label} (string): label to be displayed 10 | * @returns (JSX.Element): empty component 11 | */ 12 | export const Empty = ({ label }: EmptyProps) => { 13 | return ( 14 |
15 |
16 | Empty 17 |
18 |

{label}

19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /components/heading/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 | /** 14 | * Heading component used on top of a page. 15 | * @param {title} (string): heading title 16 | * @param {description} (string): heading description 17 | * @param {icon} (LucideIcon): icon component 18 | * @param {iconColor} (string): icon color 19 | * @param {bgColor} (string): background color 20 | * @returns (JSX.Element): heading component to be displayed on top of a page 21 | */ 22 | export const Heading = ({ 23 | title, 24 | description, 25 | icon: Icon, 26 | iconColor, 27 | bgColor, 28 | }: HeadingProps) => { 29 | return ( 30 |
31 |
32 | 33 |
34 |
35 |

{title}

36 |

{description}

37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | 4 | /** 5 | * Loading component which is displayed when the user is waiting for something. 6 | * @returns (JSX.Element): loader component 7 | */ 8 | export const Loader: React.FC = () => { 9 | return ( 10 |
11 |
12 | Logo 13 |
14 |

15 | The magician is working his magic... 16 |

17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /components/modals/ProModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useState } from "react"; 5 | import { Check, Zap } from "lucide-react"; 6 | import { toast } from "react-hot-toast"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogHeader, 12 | DialogTitle, 13 | DialogDescription, 14 | DialogFooter, 15 | } from "@/components/ui/dialog"; 16 | import { Badge } from "@/components/ui/badge"; 17 | import { Button } from "@/components/ui/button"; 18 | import { useProModal } from "@/hooks/useProModal"; 19 | import { tools } from "@/constants/constants"; 20 | import { Card } from "@/components/ui/card"; 21 | import { cn } from "@/lib/utils"; 22 | 23 | /** 24 | * Pro modal which is opened to allow the user to subscribe. 25 | * @returns (JSX.Element): pro modal component 26 | */ 27 | export const ProModal = () => { 28 | const proModal = useProModal(); // custom hook to open/close the modal 29 | const [loading, setLoading] = useState(false); // loading state 30 | 31 | /** 32 | * Function that is called when the user clicks the subscribe button. 33 | * Calls the Stripe API to get the checkout session url. 34 | */ 35 | const onSubscribe = async () => { 36 | try { 37 | setLoading(true); 38 | const response = await axios.get("/api/stripe"); // get checkout session url 39 | 40 | window.location.href = response.data.url; // redirect to checkout page 41 | } catch (error) { 42 | toast.error("Could not subscribe"); 43 | } finally { 44 | setLoading(false); 45 | } 46 | }; 47 | 48 | // TODO: do not render if user is subscribed 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 |
56 | Upgrade to Magician 57 | 58 | Plus 59 | 60 |
61 |
62 | 63 | {tools.map((tool) => ( 64 | 68 |
69 |
70 | 71 |
72 |
{tool.label}
73 |
74 | 75 |
76 | ))} 77 |
78 |
79 | 80 | 90 | 91 |
92 |
93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@clerk/nextjs"; 2 | import { MobileSidebar } from "../sidebar/MobileSidebar"; 3 | import { getApiLimitCount } from "@/lib/api-limit"; 4 | import { checkSubscription } from "@/lib/subscriptions"; 5 | 6 | /** 7 | * Navbar component which displays the mobile sidebar and the user button. 8 | */ 9 | const Navbar = async () => { 10 | const apiLimitCount = await getApiLimitCount(); // gets the number of free generations used 11 | const isPro = await checkSubscription(); // checks if the user is subscribed 12 | 13 | return ( 14 |
15 | 16 |
17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Navbar; 24 | -------------------------------------------------------------------------------- /components/pages/landing/LandingHero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TypewriterComponent from "typewriter-effect"; 4 | import Link from "next/link"; 5 | import { useAuth } from "@clerk/nextjs"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | 9 | /** 10 | * This component displays the landing hero. 11 | * @returns (JSX.Element): landing hero component 12 | */ 13 | export const LandingHero = () => { 14 | const { isSignedIn } = useAuth(); 15 | 16 | return ( 17 |
18 |
19 |

The Best AI Tool for

20 |
24 | 36 |
37 |
38 |
39 | Create content using AI 10x faster. 40 |
41 |
42 | 43 | 49 | 50 |
51 |
52 | No credit card required. 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /components/pages/landing/LandingNavbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Montserrat } from "next/font/google"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | import { useAuth } from "@clerk/nextjs"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "@/components/ui/button"; 10 | 11 | const font = Montserrat({ weight: "600", subsets: ["latin"] }); 12 | 13 | /** 14 | * Component that displays the landing navbar. 15 | * This navbar is only used in the landing page. 16 | * The navbar has a logo, a button to get started, and a button to login. 17 | */ 18 | export const LandingNavbar = () => { 19 | const { isSignedIn } = useAuth(); 20 | 21 | return ( 22 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/sidebar/FreeCounter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Zap } from "lucide-react"; 4 | import { useEffect, useState } from "react"; 5 | 6 | import { MAX_FREE_COUNTS } from "@/constants/constants"; 7 | import { Card, CardContent } from "@/components/ui/card"; 8 | import { Button } from "@/components/ui/button"; 9 | import { Progress } from "@/components/ui/progress"; 10 | import { useProModal } from "@/hooks/useProModal"; 11 | 12 | /** 13 | * Component that displays the free generation counter and a button to upgrade 14 | * If the user is subscribed, then the component is not rendered 15 | * @param {isPro} (boolean): whether the user is subscribed 16 | * @param {apiLimitCount} (number): number of free generations used 17 | * @returns (JSX.Element): free counter component (null if user is subscribed) 18 | */ 19 | export const FreeCounter = ({ 20 | isPro = false, 21 | apiLimitCount = 0, 22 | }: { 23 | isPro: boolean; 24 | apiLimitCount: number; 25 | }) => { 26 | const [mounted, setMounted] = useState(false); 27 | const proModal = useProModal(); 28 | 29 | useEffect(() => { 30 | setMounted(true); 31 | }, []); 32 | 33 | // prevent hydration errors `isMounted` 34 | if (!mounted || isPro) { 35 | return null; 36 | } 37 | 38 | return ( 39 |
40 | 41 | 42 |
43 |

44 | {apiLimitCount} / {MAX_FREE_COUNTS} Free Generations 45 |

46 | 50 |
51 | 59 |
60 |
61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /components/sidebar/MobileSidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Menu } from "lucide-react"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 7 | import { Sidebar } from "./Sidebar"; 8 | 9 | /** 10 | * Component that displays the mobile sidebar if the screen is small. 11 | * This displays the user icon and a hamburger menu to open the sidebar. 12 | * @param {apiLimitCount} (number): number of free generations used 13 | * @param {isPro} (boolean): whether the user is subscribed 14 | * @returns (JSX.Element): mobile sidebar component 15 | */ 16 | export const MobileSidebar = ({ 17 | apiLimitCount = 0, 18 | isPro = false, 19 | }: { 20 | apiLimitCount: number; 21 | isPro: boolean; 22 | }) => { 23 | const [isMounted, setIsMounted] = useState(false); 24 | 25 | //! Prevents hydration 26 | useEffect(() => { 27 | setIsMounted(true); 28 | }, []); 29 | 30 | if (!isMounted) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /components/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { Montserrat } from "next/font/google"; 6 | import { 7 | Code, 8 | ImageIcon, 9 | LayoutDashboard, 10 | MessageSquare, 11 | Music, 12 | Settings, 13 | VideoIcon, 14 | } from "lucide-react"; 15 | import { usePathname } from "next/navigation"; 16 | import { cn } from "@/lib/utils"; 17 | import { FreeCounter } from "./FreeCounter"; 18 | 19 | const poppins = Montserrat({ weight: "600", subsets: ["latin"] }); 20 | 21 | const routes = [ 22 | { 23 | label: "Dashboard", 24 | icon: LayoutDashboard, 25 | href: "/dashboard", 26 | color: "text-sky-500", 27 | }, 28 | { 29 | label: "Conversation", 30 | icon: MessageSquare, 31 | href: "/conversation", 32 | color: "text-violet-500", 33 | }, 34 | { 35 | label: "Image Generation", 36 | icon: ImageIcon, 37 | color: "text-pink-700", 38 | href: "/image", 39 | }, 40 | { 41 | label: "Video Generation", 42 | icon: VideoIcon, 43 | color: "text-orange-700", 44 | href: "/video", 45 | }, 46 | { 47 | label: "Music Generation", 48 | icon: Music, 49 | color: "text-emerald-500", 50 | href: "/music", 51 | }, 52 | { 53 | label: "Code Generation", 54 | icon: Code, 55 | color: "text-green-700", 56 | href: "/code", 57 | }, 58 | { 59 | label: "Settings", 60 | icon: Settings, 61 | href: "/settings", 62 | }, 63 | ]; 64 | 65 | /** 66 | * Sidebar component which allows the user to navigate to different pages. 67 | * On desktop, this is always rendered. On mobile, this is rendered when the user clicks the hamburger menu. 68 | * The sidebar also display the free generation counter and a button to upgrade if the user is not subscribed. 69 | * @param {apiLimitCount} (number): number of free generations used 70 | * @param {isPro} (boolean): whether the user is subscribed 71 | * @returns (JSX.Element): sidebar component 72 | */ 73 | export const Sidebar = ({ 74 | apiLimitCount = 0, 75 | isPro = false, 76 | }: { 77 | apiLimitCount: number; 78 | isPro: boolean; 79 | }) => { 80 | const pathname = usePathname(); 81 | 82 | return ( 83 |
84 |
85 | {/* Logo */} 86 | 87 |
88 | Logo 89 |
90 |

91 | Magician AI 92 |

93 | 94 | 95 | {/* Routes */} 96 |
97 | {routes.map((route) => ( 98 | 108 |
109 | 110 | {route.label} 111 |
112 | 113 | ))} 114 |
115 |
116 | 117 | {/* Counter */} 118 | 119 |
120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /components/toast/toast-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 * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | premium: 19 | "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ); 27 | 28 | export interface BadgeProps 29 | extends React.HTMLAttributes, 30 | VariantProps {} 31 | 32 | function Badge({ className, variant, ...props }: BadgeProps) { 33 | return ( 34 |
35 | ); 36 | } 37 | 38 | export { Badge, badgeVariants }; 39 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | premium: 22 | "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", 23 | }, 24 | size: { 25 | default: "h-10 px-4 py-2", 26 | sm: "h-9 rounded-lg px-3", 27 | lg: "h-11 rounded-lg px-8", 28 | icon: "h-10 w-10", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button"; 47 | return ( 48 | 53 | ); 54 | } 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /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 { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /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 = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /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 |