├── .eslintrc.json ├── .github ├── codeql-config.yml ├── dependabot.yml └── renovate.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── error.tsx │ └── layout.tsx ├── (dashboard) │ ├── (routes) │ │ ├── code │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── conversation │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── image │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── music │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ └── video │ │ │ ├── constants.ts │ │ │ └── page.tsx │ ├── error.tsx │ └── layout.tsx ├── (landing) │ ├── constants.ts │ ├── error.tsx │ ├── layout.tsx │ └── page.tsx ├── api │ ├── code │ │ └── route.ts │ ├── conversation │ │ └── route.ts │ ├── image │ │ └── route.ts │ ├── music │ │ └── route.ts │ ├── stripe │ │ └── route.ts │ ├── video │ │ └── route.ts │ └── webhook │ │ └── route.ts ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── bot-avatar.tsx ├── crisp-chat.tsx ├── crisp-provider.tsx ├── empty.tsx ├── free-counter.tsx ├── heading.tsx ├── landing-content.tsx ├── landing-hero.tsx ├── landing-navbar.tsx ├── loader.tsx ├── mobile-sidebar.tsx ├── modal-provider.tsx ├── navbar.tsx ├── pro-modal.tsx ├── sidebar.tsx ├── subscription-button.tsx ├── toaster-provider.tsx ├── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── progress.tsx │ ├── select.tsx │ └── sheet.tsx └── user-avatar.tsx ├── constants.ts ├── constants └── sidebar-constants.ts ├── hooks └── use-pro-modal.ts ├── lib ├── api-limit.ts ├── metadata.ts ├── prismadb.ts ├── stripe.ts ├── subscription.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── empty.png └── logo.png ├── tailwind.config.js ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "Advanced CodeQL Analysis" 2 | 3 | on: 4 | push: 5 | branches: [main, develop] 6 | pull_request: 7 | branches: [main, develop] 8 | schedule: 9 | - cron: "0 1 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | node: ["20", "21", "lts/*"] 20 | language: ["javascript"] 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node }} 30 | cache: "npm" 31 | 32 | - name: Install Dependencies 33 | run: npm ci 34 | 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v2.16.0 37 | with: 38 | languages: ${{ matrix.language }} 39 | config-file: ./.github/codeql/codeql-config.yml 40 | 41 | - name: Autobuild 42 | uses: github/codeql-action/autobuild@v2.16.0 43 | 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@v2.16.0 46 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "sunday" 8 | time: "16:00" 9 | open-pull-requests-limit: 30 10 | reviewers: 11 | - "RicardoGEsteves" 12 | assignees: 13 | - "RicardoGEsteves" 14 | groups: 15 | all-dependencies: 16 | applies-to: version-updates 17 | patterns: 18 | - "*" 19 | update-types: 20 | - "minor" 21 | - "patch" 22 | 23 | - package-ecosystem: "github-actions" 24 | directory: "/" 25 | schedule: 26 | interval: "weekly" 27 | day: "sunday" 28 | time: "16:00" 29 | groups: 30 | all-actions: 31 | patterns: [ "*" ] 32 | open-pull-requests-limit: 30 33 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | ":rebaseStalePrs", 6 | ":enableVulnerabilityAlertsWithLabel('security')", 7 | "group:recommended", 8 | "group:monorepos", 9 | "replacements:all", 10 | "workarounds:all", 11 | ":semanticPrefixFixDepsChoreOthers", 12 | ":separateMultipleMajorReleases" 13 | ], 14 | "vulnerabilityAlerts": { 15 | "labels": [ 16 | "security" 17 | ], 18 | "automerge": true, 19 | "assignees": [ 20 | "@RicardoGEsteves" 21 | ], 22 | "prCreation": "immediate" 23 | }, 24 | "digest": { 25 | "prBodyDefinitions": { 26 | "Change": "{{#if displayFrom}}`{{{displayFrom}}}` -> {{else}}{{#if currentValue}}`{{{currentValue}}}` -> {{/if}}{{/if}}{{#if displayTo}}`{{{displayTo}}}`{{else}}`{{{newValue}}}`{{/if}}" 27 | }, 28 | "automerge": true 29 | }, 30 | "prBodyDefinitions": { 31 | "Change": "[{{#if displayFrom}}`{{{displayFrom}}}` -> {{else}}{{#if currentValue}}`{{{currentValue}}}` -> {{/if}}{{/if}}{{#if displayTo}}`{{{displayTo}}}`{{else}}`{{{newValue}}}`{{/if}}]({{#if depName}}https://renovatebot.com/diffs/npm/{{replace '/' '%2f' depName}}/{{{currentVersion}}}/{{{newVersion}}}{{/if}})" 32 | }, 33 | "prHeader": "RicardoGEsteves {{{groupSlug}}} update, dependency {{depName}} to v{{newVersion}}", 34 | "assignees": [ 35 | "RicardoGEsteves" 36 | ], 37 | "labels": [ 38 | "dependencies", 39 | "major", 40 | "minor", 41 | "patch", 42 | "{{depName}}" 43 | ], 44 | "schedule": [ 45 | "every weekend" 46 | ], 47 | "prCreation": "not-pending", 48 | "configMigration": true, 49 | "platformCommit": "enabled", 50 | "packageRules": [ 51 | { 52 | "prBodyDefinitions": { 53 | "OpenSSF": "[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/{{sourceRepo}}/badge)](https://securityscorecards.dev/viewer/?uri=github.com/{{sourceRepo}})", 54 | "Sourcegraph": "[![code search for \"{{{depName}}}\"](https://sourcegraph.com/search/badge?q=repo:%5Egithub%5C.com/{{{repository}}}%24+case:yes+-file:package%28-lock%29%3F%5C.json+{{{depName}}}&label=matches)](https://sourcegraph.com/search?q=repo:%5Egithub%5C.com/{{{repository}}}%24+case:yes+-file:package%28-lock%29%3F%5C.json+{{{depName}}})" 55 | }, 56 | "prBodyColumns": [ 57 | "Package", 58 | "Type", 59 | "Update", 60 | "Change", 61 | "Pending", 62 | "OpenSSF", 63 | "New value", 64 | "Package file", 65 | "References" 66 | ], 67 | "matchSourceUrls": [ 68 | "https://github.com/{/,}**" 69 | ] 70 | }, 71 | { 72 | "matchPackageNames": [ 73 | "node" 74 | ], 75 | "enabled": false 76 | }, 77 | { 78 | "matchPackageNames": [ 79 | "npm" 80 | ], 81 | "enabled": false 82 | }, 83 | { 84 | "matchPackageNames": [ 85 | "pnpm" 86 | ], 87 | "enabled": false 88 | }, 89 | { 90 | "matchDepTypes": [ 91 | "peerDependencies" 92 | ], 93 | "enabled": false 94 | }, 95 | { 96 | "description": "Automatically merge minor and patch-level updates", 97 | "matchUpdateTypes": [ 98 | "minor", 99 | "patch", 100 | "pin", 101 | "digest" 102 | ], 103 | "autoApprove": true, 104 | "automerge": true, 105 | "automergeType": "pr", 106 | "automergeStrategy": "squash", 107 | "platformAutomerge": true, 108 | "assignAutomerge": true, 109 | "groupName": "all non-major dependencies", 110 | "groupSlug": "all-non-major", 111 | "addLabels": [ 112 | "{{{matchUpdateTypes}}}", 113 | "{{{groupSlug}}}" 114 | ], 115 | "matchPackageNames": [ 116 | "*" 117 | ] 118 | }, 119 | { 120 | "description": "Dependencies major updates", 121 | "matchUpdateTypes": [ 122 | "major" 123 | ], 124 | "dependencyDashboardApproval": true, 125 | "addLabels": [ 126 | "{{{matchUpdateTypes}}}", 127 | "{{{depName}}}-update-{{{newVersion}}}" 128 | ], 129 | "matchPackageNames": [ 130 | "*" 131 | ] 132 | } 133 | ], 134 | "group": { 135 | "branchTopic": "{{{groupSlug}}}", 136 | "commitMessageTopic": "{{{groupName}}}" 137 | }, 138 | "major": { 139 | "automerge": false 140 | }, 141 | "patch": { 142 | "automerge": true 143 | }, 144 | "minor": { 145 | "automerge": true 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ricardo Esteves 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 | # Omniscient AI Platform 2 | 3 | > **(UPDATE)** - with the latest dependencies updates, you will have to do some small config changes. 4 | 5 | Welcome to Omniscient, an advanced AI Platform offered as a SaaS (Software as a Service). Empower your projects with cutting-edge artificial intelligence capabilities across various domains. Built on a robust technology stack, Omniscient seamlessly integrates with Next.js 14, React, Typescript, and powerful APIs such as OpenAI and Replicate. This platform is designed to provide a comprehensive solution for code generation, conversation simulation, image creation, music composition, and video generation. 6 | 7 | ## Key Features 8 | 9 | - **Free and Pro Plans**: Choose between free and pro plans tailored to meet your specific needs. 10 | 11 | - **User Settings**: Manage your experience with ease. Take control of account details, services, and monitor resource usage effortlessly. 12 | 13 | - **Multifaceted AI Models**: 14 | - **Code Generation**: Automate code creation with advanced models. 15 | - **Conversation Simulation**: Engage in realistic conversation simulations for diverse applications. 16 | - **Image Creation**: Generate stunning visuals with AI-powered image generation. 17 | - **Music Composition**: Unlock creativity with AI-generated musical compositions. 18 | - **Video Generation**: Seamlessly create videos for your projects with AI assistance. 19 | 20 | ## Technology Stack 21 | 22 | - **Frontend**: Next.js 14, React, Typescript, Clerk, Tailwind, Shadcn-ui. 23 | - **Backend**: Prisma, Postgres, Supabase. 24 | - **AI Integration**: OpenAI API, Replicate API. 25 | - **State Management**: Zustand. 26 | - **Form Handling**: React Hook Form. 27 | - **API Communication**: Axios. 28 | - **Notifications**: React Hot Toast. 29 | - **Markdown Rendering**: React Markdown. 30 | - **Payment Processing**: Stripe. 31 | - **Customer Support**: Crisp. 32 | 33 | ## Getting Started 34 | 35 | 1. Clone the repository 36 | 2. Install dependencies: `npm install` 37 | 3. Configure your environment variables. 38 | 4. Run the application: `npm run dev` 39 | 40 | ### Store your keys on your .env or .env.local 41 | 42 | ```bash 43 | CLERK_SECRET_KEY 44 | 45 | NEXT_PUBLIC_CLERK_SIGN_IN_URL 46 | NEXT_PUBLIC_CLERK_SIGN_UP_URL 47 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL 48 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL 49 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 50 | 51 | OPENAI_API_KEY 52 | 53 | REPLICATE_API_TOKEN 54 | 55 | NEXT_PUBLIC_APP_URL 56 | 57 | DATABASE_URL 58 | DATABASE_PASS 59 | 60 | STRIPE_API_KEY 61 | STRIPE_WEBHOOK_SECRET 62 | 63 | CRISP_TOKEN_ID 64 | ``` 65 | 66 | ## For prisma 67 | 68 | ```bash 69 | # Generate prisma setup 70 | npx prisma init # than make your changes on prisma schema with your provider and connection string 71 | 72 | # Generate/Create tables 73 | npx prisma generate 74 | 75 | # Install prisma client 76 | npm i @prisma/client 77 | 78 | # Push schema to db 79 | npx prisma db push 80 | 81 | # Open prisma studio on localhost 82 | npx prisma studio 83 | 84 | # Reset database (You will lose all the data) 85 | npx prisma migrate reset 86 | ``` 87 | 88 | ## For stripe 89 | 90 | - Create the connection with the sample endpoint 91 | - Test in local environment 92 | - Download cli 93 | - $ stripe login (check documentation) 94 | - $ stripe listen --forward-to (localhost:3000/api/webhook) 95 | now you got the secret, copy it and add it to your .env `STRIPE_WEBHOOK_SECRET` 96 | - $ stripe trigger (trigger events with the cli) 97 | - Keep dev running , prisma and stripe cli bash's 98 | - Go to stipe website and search customer portal and activate 'Activate test link' 99 | 100 | ## Contributing 101 | 102 | Contributions are welcome to enhance Omniscient's capabilities. Whether it's fixing bugs, improving existing features, or proposing new ones, your input is valuable. 103 | 104 | ## Reporting Issues 105 | 106 | If you encounter any issues or have suggestions for improvement, please [create an issue](https://github.com/your-username/omniscient/issues) on our GitHub repository. 107 | 108 | ## License 109 | 110 | This project is licensed under the [MIT License](LICENSE). 111 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/empty"; 4 | 5 | const Error = () => { 6 | return ; 7 | }; 8 | 9 | export default Error; 10 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { Code } from "lucide-react"; 6 | import { useForm } from "react-hook-form"; 7 | import { useState } from "react"; 8 | import { useRouter } from "next/navigation"; 9 | import toast from "react-hot-toast"; 10 | import OpenAI from "openai"; 11 | import ReactMarkdown from "react-markdown"; 12 | 13 | import Heading from "@/components/heading"; 14 | import { BotAvatar } from "@/components/bot-avatar"; 15 | import { Button } from "@/components/ui/button"; 16 | import { Input } from "@/components/ui/input"; 17 | import { zodResolver } from "@hookform/resolvers/zod"; 18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 19 | import { cn } from "@/lib/utils"; 20 | import { Loader } from "@/components/loader"; 21 | import { UserAvatar } from "@/components/user-avatar"; 22 | import { Empty } from "@/components/empty"; 23 | import { useProModal } from "@/hooks/use-pro-modal"; 24 | 25 | import { formSchema } from "./constants"; 26 | 27 | const CodePage = () => { 28 | const router = useRouter(); 29 | const proModal = useProModal(); 30 | const [messages, setMessages] = useState< 31 | OpenAI.ChatCompletionUserMessageParam[] 32 | >([]); 33 | 34 | const form = useForm>({ 35 | resolver: zodResolver(formSchema), 36 | defaultValues: { 37 | prompt: "", 38 | }, 39 | }); 40 | 41 | const isLoading = form.formState.isSubmitting; 42 | 43 | const onSubmit = async (values: z.infer) => { 44 | try { 45 | const userMessage: OpenAI.ChatCompletionUserMessageParam = { 46 | role: "user", 47 | content: values.prompt, 48 | }; 49 | const newMessages = [...messages, userMessage]; 50 | 51 | const response = await axios.post("/api/code", { messages: newMessages }); 52 | setMessages((current) => [...current, userMessage, response.data]); 53 | 54 | form.reset(); 55 | } catch (error: any) { 56 | if (error?.response?.status === 403) { 57 | proModal.onOpen(); 58 | } else { 59 | toast.error("Something went wrong."); 60 | } 61 | } finally { 62 | router.refresh(); 63 | } 64 | }; 65 | 66 | return ( 67 |
68 | 75 |
76 |
77 |
78 | 93 | ( 96 | 97 | 98 | 104 | 105 | 106 | )} 107 | /> 108 | 116 | 117 | 118 |
119 |
120 | {isLoading && ( 121 |
122 | 123 |
124 | )} 125 | {messages.length === 0 && !isLoading && ( 126 | 127 | )} 128 |
129 | {messages.map((message) => ( 130 |
139 | {message.role === "user" ? : } 140 | ( 143 |
144 |
145 |                       
146 | ), 147 | code: ({ node, ...props }) => ( 148 | 149 | ), 150 | }} 151 | className="text-sm overflow-hidden leading-7" 152 | > 153 | {(message.content as string) || ""} 154 |
155 |
156 | ))} 157 |
158 |
159 |
160 |
161 | ); 162 | }; 163 | 164 | export default CodePage; 165 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/conversation/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import * as z from "zod"; 5 | import { useState } from "react"; 6 | import { useRouter } from "next/navigation"; 7 | import { MessageSquare } from "lucide-react"; 8 | import toast from "react-hot-toast"; 9 | import { useForm } from "react-hook-form"; 10 | 11 | import { zodResolver } from "@hookform/resolvers/zod"; 12 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 13 | import Heading from "@/components/heading"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Button } from "@/components/ui/button"; 16 | import OpenAI from "openai"; 17 | 18 | import { formSchema } from "./constants"; 19 | import { Empty } from "@/components/empty"; 20 | import { Loader } from "@/components/loader"; 21 | import { cn } from "@/lib/utils"; 22 | import { UserAvatar } from "@/components/user-avatar"; 23 | import { BotAvatar } from "@/components/bot-avatar"; 24 | import { useProModal } from "@/hooks/use-pro-modal"; 25 | 26 | export default function ConversationPage() { 27 | const router = useRouter(); 28 | const proModal = useProModal(); 29 | const [messages, setMessages] = useState< 30 | OpenAI.ChatCompletionUserMessageParam[] 31 | >([]); 32 | 33 | const form = useForm>({ 34 | resolver: zodResolver(formSchema), 35 | defaultValues: { 36 | prompt: "", 37 | }, 38 | }); 39 | 40 | const isLoading = form.formState.isSubmitting; 41 | 42 | const onSubmit = async (values: z.infer) => { 43 | try { 44 | const userMessage: OpenAI.ChatCompletionUserMessageParam = { 45 | role: "user", 46 | content: values.prompt, 47 | }; 48 | const newMessages = [...messages, userMessage]; 49 | 50 | const response = await axios.post("/api/conversation", { 51 | messages: newMessages, 52 | }); 53 | setMessages((current) => [...current, userMessage, response.data]); 54 | 55 | form.reset(); 56 | } catch (error: any) { 57 | if (error?.response?.status === 403) { 58 | proModal.onOpen(); 59 | } else { 60 | toast.error("Something went wrong."); 61 | } 62 | } finally { 63 | router.refresh(); 64 | } 65 | }; 66 | 67 | return ( 68 |
69 | 76 | 77 |
78 |
79 |
80 | 95 | ( 98 | 99 | 100 | 106 | 107 | 108 | )} 109 | /> 110 | 118 | 119 | 120 |
121 | 122 |
123 | {isLoading && ( 124 |
125 | 126 |
127 | )} 128 | {messages.length === 0 && !isLoading && ( 129 | 130 | )} 131 |
132 | {messages.map((message) => ( 133 |
142 | {message.role === "user" ? : } 143 | 144 |

{message.content as string}

145 |
146 | ))} 147 |
148 |
149 |
150 |
151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/dashboard/constants.ts: -------------------------------------------------------------------------------- 1 | import { Code, ImageIcon, MessageSquare, Music, VideoIcon } from "lucide-react"; 2 | 3 | export const tools = [ 4 | { 5 | label: "Conversation", 6 | icon: MessageSquare, 7 | href: "/conversation", 8 | color: "text-violet-500", 9 | bgColor: "bg-violet-500/10", 10 | }, 11 | { 12 | label: "Image Generation", 13 | icon: ImageIcon, 14 | color: "text-pink-700", 15 | bgColor: "bg-pink-700/10", 16 | href: "/image", 17 | }, 18 | { 19 | label: "Video Generation", 20 | icon: VideoIcon, 21 | color: "text-orange-700", 22 | bgColor: "bg-orange-700/10", 23 | href: "/video", 24 | }, 25 | { 26 | label: "Music Generation", 27 | icon: Music, 28 | href: "/music", 29 | color: "text-emerald-500", 30 | bgColor: "bg-emerald-500/10", 31 | }, 32 | { 33 | label: "Code Generation", 34 | icon: Code, 35 | color: "text-green-600", 36 | bgColor: "bg-green-600/10", 37 | href: "/code", 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { Card } from "@/components/ui/card"; 5 | import { cn } from "@/lib/utils"; 6 | import { ArrowRight } from "lucide-react"; 7 | 8 | import { tools } from "./constants"; 9 | 10 | export default function DashboardPage() { 11 | const router = useRouter(); 12 | return ( 13 |
14 |
15 |

16 | Omniscient, unleash Tomorrow's Potential, Today. 17 |

18 |

19 | Where Insight Meets Innovation. Empower Your Future with Omniscient. 20 |

21 |
22 |
23 | {tools.map((tool) => ( 24 | router.push(tool.href)} 26 | key={tool.href} 27 | className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer" 28 | > 29 |
30 |
31 | 32 |
33 |
{tool.label}
34 |
35 | 36 |
37 | ))} 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Photo prompt is required", 6 | }), 7 | amount: z.string().min(1), 8 | resolution: z.string().min(1), 9 | }); 10 | 11 | export const amountOptions = [ 12 | { 13 | value: "1", 14 | label: "1 Photo", 15 | }, 16 | { 17 | value: "2", 18 | label: "2 Photos", 19 | }, 20 | { 21 | value: "3", 22 | label: "3 Photos", 23 | }, 24 | { 25 | value: "4", 26 | label: "4 Photos", 27 | }, 28 | { 29 | value: "5", 30 | label: "5 Photos", 31 | }, 32 | ]; 33 | 34 | export const resolutionOptions = [ 35 | { 36 | value: "256x256", 37 | label: "256x256", 38 | }, 39 | { 40 | value: "512x512", 41 | label: "512x512", 42 | }, 43 | { 44 | value: "1024x1024", 45 | label: "1024x1024", 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import Image from "next/image"; 6 | import { useState } from "react"; 7 | import { zodResolver } from "@hookform/resolvers/zod"; 8 | import { Download, ImageIcon } from "lucide-react"; 9 | import { useForm } from "react-hook-form"; 10 | import { useRouter } from "next/navigation"; 11 | import toast from "react-hot-toast"; 12 | 13 | import Heading from "@/components/heading"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Card, CardFooter } from "@/components/ui/card"; 16 | import { Input } from "@/components/ui/input"; 17 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 18 | import { Loader } from "@/components/loader"; 19 | import { Empty } from "@/components/empty"; 20 | import { 21 | Select, 22 | SelectContent, 23 | SelectItem, 24 | SelectTrigger, 25 | SelectValue, 26 | } from "@/components/ui/select"; 27 | import { useProModal } from "@/hooks/use-pro-modal"; 28 | 29 | import { amountOptions, formSchema, resolutionOptions } from "./constants"; 30 | 31 | const PhotoPage = () => { 32 | const proModal = useProModal(); 33 | const router = useRouter(); 34 | const [photos, setPhotos] = useState([]); 35 | 36 | const form = useForm>({ 37 | resolver: zodResolver(formSchema), 38 | defaultValues: { 39 | prompt: "", 40 | amount: "1", 41 | resolution: "512x512", 42 | }, 43 | }); 44 | 45 | const isLoading = form.formState.isSubmitting; 46 | 47 | const onSubmit = async (values: z.infer) => { 48 | try { 49 | setPhotos([]); 50 | 51 | const response = await axios.post("/api/image", values); 52 | 53 | const urls = response.data.map((image: { url: string }) => image.url); 54 | 55 | setPhotos(urls); 56 | } catch (error: any) { 57 | if (error?.response?.status === 403) { 58 | proModal.onOpen(); 59 | } else { 60 | toast.error("Something went wrong."); 61 | } 62 | } finally { 63 | router.refresh(); 64 | } 65 | }; 66 | 67 | return ( 68 |
69 | 76 |
77 |
78 | 93 | ( 96 | 97 | 98 | 104 | 105 | 106 | )} 107 | /> 108 | ( 112 | 113 | 132 | 133 | )} 134 | /> 135 | ( 139 | 140 | 159 | 160 | )} 161 | /> 162 | 170 | 171 | 172 | {isLoading && ( 173 |
174 | 175 |
176 | )} 177 | {photos.length === 0 && !isLoading && ( 178 | 179 | )} 180 |
181 | {photos.map((src) => ( 182 | 183 |
184 | Generated 185 |
186 | 187 | 195 | 196 |
197 | ))} 198 |
199 |
200 |
201 | ); 202 | }; 203 | 204 | export default PhotoPage; 205 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { useState } from "react"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { useForm } from "react-hook-form"; 8 | import { useRouter } from "next/navigation"; 9 | import toast from "react-hot-toast"; 10 | import { Music, Send } from "lucide-react"; 11 | 12 | import Heading from "@/components/heading"; 13 | import { Button } from "@/components/ui/button"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 16 | import { Loader } from "@/components/loader"; 17 | import { Empty } from "@/components/empty"; 18 | import { useProModal } from "@/hooks/use-pro-modal"; 19 | 20 | import { formSchema } from "./constants"; 21 | 22 | const MusicPage = () => { 23 | const proModal = useProModal(); 24 | const router = useRouter(); 25 | const [music, setMusic] = useState(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(formSchema), 29 | defaultValues: { 30 | prompt: "", 31 | }, 32 | }); 33 | 34 | const isLoading = form.formState.isSubmitting; 35 | 36 | const onSubmit = async (values: z.infer) => { 37 | try { 38 | setMusic(undefined); 39 | 40 | const response = await axios.post("/api/music", values); 41 | 42 | setMusic(response.data.audio); 43 | form.reset(); 44 | } catch (error: any) { 45 | if (error?.response?.status === 403) { 46 | proModal.onOpen(); 47 | } else { 48 | toast.error("Something went wrong."); 49 | } 50 | } finally { 51 | router.refresh(); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 | 64 |
65 |
66 | 81 | ( 84 | 85 | 86 | 92 | 93 | 94 | )} 95 | /> 96 | 104 | 105 | 106 | {isLoading && ( 107 |
108 | 109 |
110 | )} 111 | {!music && !isLoading && } 112 | {music && ( 113 | 116 | )} 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default MusicPage; 123 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required.", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Settings } from "lucide-react"; 2 | 3 | import Heading from "@/components/heading"; 4 | import { SubscriptionButton } from "@/components/subscription-button"; 5 | import { checkSubscription } from "@/lib/subscription"; 6 | 7 | const SettingsPage = async () => { 8 | const isPro = await checkSubscription(); 9 | 10 | return ( 11 |
12 | 19 |
20 |
21 | {isPro 22 | ? "You are currently on a Pro plan." 23 | : "You are currently on a free plan."} 24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default SettingsPage; 32 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/constants.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { useState } from "react"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { useForm } from "react-hook-form"; 8 | 9 | import { FileAudio } from "lucide-react"; 10 | import { useRouter } from "next/navigation"; 11 | import toast from "react-hot-toast"; 12 | 13 | import Heading from "@/components/heading"; 14 | import { Button } from "@/components/ui/button"; 15 | import { Input } from "@/components/ui/input"; 16 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 17 | import { Loader } from "@/components/loader"; 18 | import { Empty } from "@/components/empty"; 19 | import { useProModal } from "@/hooks/use-pro-modal"; 20 | 21 | import { formSchema } from "./constants"; 22 | 23 | const VideoPage = () => { 24 | const router = useRouter(); 25 | const proModal = useProModal(); 26 | const [video, setVideo] = useState(); 27 | 28 | const form = useForm>({ 29 | resolver: zodResolver(formSchema), 30 | defaultValues: { 31 | prompt: "", 32 | }, 33 | }); 34 | 35 | const isLoading = form.formState.isSubmitting; 36 | 37 | const onSubmit = async (values: z.infer) => { 38 | try { 39 | setVideo(undefined); 40 | 41 | const response = await axios.post("/api/video", values); 42 | 43 | setVideo(response.data[0]); 44 | form.reset(); 45 | } catch (error: any) { 46 | if (error?.response?.status === 403) { 47 | proModal.onOpen(); 48 | } else { 49 | toast.error("Something went wrong."); 50 | } 51 | } finally { 52 | router.refresh(); 53 | } 54 | }; 55 | 56 | return ( 57 |
58 | 65 |
66 |
67 | 82 | ( 85 | 86 | 87 | 93 | 94 | 95 | )} 96 | /> 97 | 105 | 106 | 107 | {isLoading && ( 108 |
109 | 110 |
111 | )} 112 | {!video && !isLoading && } 113 | {video && ( 114 | 120 | )} 121 |
122 |
123 | ); 124 | }; 125 | 126 | export default VideoPage; 127 | -------------------------------------------------------------------------------- /app/(dashboard)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/empty"; 4 | 5 | const Error = () => { 6 | return ; 7 | }; 8 | 9 | export default Error; 10 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar"; 2 | import Sidebar from "@/components/sidebar"; 3 | 4 | import { checkSubscription } from "@/lib/subscription"; 5 | import { getApiLimitCount } from "@/lib/api-limit"; 6 | 7 | export default async function DashboardLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const apiLimitCount = await getApiLimitCount(); 13 | const isPro = await checkSubscription(); 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | 22 | {children} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/(landing)/constants.ts: -------------------------------------------------------------------------------- 1 | export const testimonials = [ 2 | { 3 | name: "Joel", 4 | avatar: "J", 5 | title: "Software Engineer", 6 | description: "This is the best application I've ever used!", 7 | }, 8 | { 9 | name: "Antonio", 10 | avatar: "A", 11 | title: "Designer", 12 | description: "I use this daily for generating new photos!", 13 | }, 14 | { 15 | name: "Mark", 16 | avatar: "M", 17 | title: "CEO", 18 | description: 19 | "This app has changed my life, cannot imagine working without it!", 20 | }, 21 | { 22 | name: "Mary", 23 | avatar: "M", 24 | title: "CFO", 25 | description: 26 | "The best in class, definitely worth the premium subscription!", 27 | }, 28 | { 29 | name: "Laura", 30 | avatar: "L", 31 | title: "CTO", 32 | description: "Awesome application, really well tough", 33 | }, 34 | { 35 | name: "Mike", 36 | avatar: "M", 37 | title: "Entrepreneur", 38 | description: 39 | "This app helped me develop and research the best business models, and how to implement it.", 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /app/(landing)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Empty } from "@/components/empty"; 4 | 5 | const Error = () => { 6 | return ; 7 | }; 8 | 9 | export default Error; 10 | -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | const LandingLayout = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
4 |
{children}
5 |
6 | ); 7 | }; 8 | 9 | export default LandingLayout; 10 | -------------------------------------------------------------------------------- /app/(landing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { LandingNavbar } from "@/components/landing-navbar"; 2 | import { LandingHero } from "@/components/landing-hero"; 3 | import { LandingContent } from "@/components/landing-content"; 4 | 5 | const LandingPage = () => { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default LandingPage; 16 | -------------------------------------------------------------------------------- /app/api/code/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | import { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY, // This is also the default, can be omitted 10 | }); 11 | 12 | const instructionMessage: OpenAI.ChatCompletionSystemMessageParam = { 13 | role: "system", 14 | content: 15 | "You are a genius code generator, you are omniscient. You know everything about software engineering. You must answer only in markdown code snippets. Use code comments for explanations.", 16 | }; 17 | 18 | export async function POST(req: Request) { 19 | try { 20 | const { userId } = auth(); 21 | const body = await req.json(); 22 | const { messages } = body; 23 | 24 | if (!userId) { 25 | return new NextResponse("Unauthorized", { status: 401 }); 26 | } 27 | 28 | if (!openai.apiKey) { 29 | return new NextResponse("OpenAI API Key not configured.", { 30 | status: 500, 31 | }); 32 | } 33 | 34 | if (!messages) { 35 | return new NextResponse("Messages are required", { status: 400 }); 36 | } 37 | 38 | const freeTrial = await checkApiLimit(); 39 | const isPro = await checkSubscription(); 40 | 41 | if (!freeTrial && !isPro) { 42 | return new NextResponse( 43 | "Free trial has expired. Please upgrade to pro.", 44 | { status: 403 } 45 | ); 46 | } 47 | 48 | const response = await openai.chat.completions.create({ 49 | model: "gpt-3.5-turbo", 50 | messages: [instructionMessage, ...messages], 51 | }); 52 | 53 | if (!isPro) { 54 | await incrementApiLimit(); 55 | } 56 | 57 | return NextResponse.json(response.choices[0].message); 58 | } catch (error: any) { 59 | if (error instanceof OpenAI.APIError) { 60 | console.error(error.status); // e.g. 401 61 | console.error(error.message); // e.g. The authentication token you passed was invalid... 62 | console.error(error.code); // e.g. 'invalid_api_key' 63 | console.error(error.type); // e.g. 'invalid_request_error' 64 | } else { 65 | // Non-API error 66 | console.log("[CODE_ERROR]", error); 67 | } 68 | 69 | return new NextResponse("Internal Error", { status: 500 }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/api/conversation/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | import { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY, // This is also the default, can be omitted 10 | }); 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const { userId } = auth(); 15 | const body = await req.json(); 16 | const { messages } = body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | 22 | if (!openai.apiKey) { 23 | return new NextResponse("OpenAI API Key not configured.", { 24 | status: 500, 25 | }); 26 | } 27 | 28 | if (!messages) { 29 | return new NextResponse("Messages are required", { status: 400 }); 30 | } 31 | 32 | const freeTrial = await checkApiLimit(); 33 | const isPro = await checkSubscription(); 34 | 35 | if (!freeTrial && !isPro) { 36 | return new NextResponse( 37 | "Free trial has expired. Please upgrade to pro.", 38 | { status: 403 } 39 | ); 40 | } 41 | 42 | const response = await openai.chat.completions.create({ 43 | model: "gpt-3.5-turbo", 44 | messages, 45 | }); 46 | 47 | if (!isPro) { 48 | await incrementApiLimit(); 49 | } 50 | 51 | return NextResponse.json(response.choices[0].message); 52 | } catch (error: any) { 53 | if (error instanceof OpenAI.APIError) { 54 | console.error(error.status); // e.g. 401 55 | console.error(error.message); // e.g. The authentication token you passed was invalid... 56 | console.error(error.code); // e.g. 'invalid_api_key' 57 | console.error(error.type); // e.g. 'invalid_request_error' 58 | } else { 59 | // Non-API error 60 | console.log("[CONVERSATION_ERROR]", error); 61 | } 62 | 63 | return new NextResponse("Internal Error", { status: 500 }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/api/image/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | import { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY, // This is also the default, can be omitted 10 | }); 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const { userId } = auth(); 15 | const body = await req.json(); 16 | const { prompt, amount = 1, resolution = "512x512" } = body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | 22 | if (!openai.apiKey) { 23 | return new NextResponse("OpenAI API Key not configured.", { 24 | status: 500, 25 | }); 26 | } 27 | 28 | if (!prompt) { 29 | return new NextResponse("Prompt is required", { status: 400 }); 30 | } 31 | 32 | if (!amount) { 33 | return new NextResponse("Amount is required", { status: 400 }); 34 | } 35 | 36 | if (!resolution) { 37 | return new NextResponse("Resolution is required", { status: 400 }); 38 | } 39 | 40 | const freeTrial = await checkApiLimit(); 41 | const isPro = await checkSubscription(); 42 | 43 | if (!freeTrial && !isPro) { 44 | return new NextResponse( 45 | "Free trial has expired. Please upgrade to pro.", 46 | { status: 403 } 47 | ); 48 | } 49 | 50 | const response = await openai.images.generate({ 51 | prompt, 52 | n: parseInt(amount, 10), 53 | size: resolution, 54 | }); 55 | 56 | if (!isPro) { 57 | await incrementApiLimit(); 58 | } 59 | 60 | return NextResponse.json(response.data); 61 | } catch (error: any) { 62 | if (error instanceof OpenAI.APIError) { 63 | console.error(error.status); // e.g. 401 64 | console.error(error.message); // e.g. The authentication token you passed was invalid... 65 | console.error(error.code); // e.g. 'invalid_api_key' 66 | console.error(error.type); // e.g. 'invalid_request_error' 67 | } else { 68 | // Non-API error 69 | console.log("[IMAGE_ERROR]", error); 70 | } 71 | 72 | return new NextResponse("Internal Error", { status: 500 }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/api/music/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import Replicate from "replicate"; 4 | 5 | import { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const replicate = new Replicate({ 9 | auth: process.env.REPLICATE_API_TOKEN!, 10 | }); 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const { userId } = auth(); 15 | const body = await req.json(); 16 | const { prompt } = body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | 22 | if (!prompt) { 23 | return new NextResponse("Prompt is required", { status: 400 }); 24 | } 25 | 26 | const freeTrial = await checkApiLimit(); 27 | const isPro = await checkSubscription(); 28 | 29 | if (!freeTrial && !isPro) { 30 | return new NextResponse( 31 | "Free trial has expired. Please upgrade to pro.", 32 | { status: 403 } 33 | ); 34 | } 35 | 36 | const response = await replicate.run( 37 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", 38 | { 39 | input: { 40 | prompt_a: prompt, 41 | }, 42 | } 43 | ); 44 | 45 | if (!isPro) { 46 | await incrementApiLimit(); 47 | } 48 | 49 | return NextResponse.json(response); 50 | } catch (error: any) { 51 | console.log("[MUSIC_ERROR]", error); 52 | return new NextResponse("Internal Error", { status: 500 }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/api/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, currentUser } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import { stripe } from "@/lib/stripe"; 6 | import { absoluteUrl } from "@/lib/utils"; 7 | 8 | const settingsUrl = absoluteUrl("/settings"); 9 | 10 | export async function GET() { 11 | try { 12 | const { userId } = auth(); 13 | const user = await currentUser(); 14 | 15 | if (!userId || !user) { 16 | return new NextResponse("Unauthorized", { status: 401 }); 17 | } 18 | 19 | const userSubscription = await prismadb.userSubscription.findUnique({ 20 | where: { 21 | userId, 22 | }, 23 | }); 24 | 25 | if (userSubscription && userSubscription.stripeCustomerId) { 26 | const stripeSession = await stripe.billingPortal.sessions.create({ 27 | customer: userSubscription.stripeCustomerId, 28 | return_url: settingsUrl, 29 | }); 30 | 31 | return new NextResponse(JSON.stringify({ url: stripeSession.url })); 32 | } 33 | 34 | const stripeSession = await stripe.checkout.sessions.create({ 35 | success_url: settingsUrl, 36 | cancel_url: settingsUrl, 37 | payment_method_types: ["card"], 38 | mode: "subscription", 39 | billing_address_collection: "auto", 40 | customer_email: user.emailAddresses[0].emailAddress, 41 | line_items: [ 42 | { 43 | price_data: { 44 | currency: "EUR", 45 | product_data: { 46 | name: "Omniscient Pro", 47 | description: "Unlimited AI Generations", 48 | }, 49 | unit_amount: 2000, 50 | recurring: { 51 | interval: "month", 52 | }, 53 | }, 54 | quantity: 1, 55 | }, 56 | ], 57 | metadata: { 58 | userId, 59 | }, 60 | }); 61 | 62 | return new NextResponse(JSON.stringify({ url: stripeSession.url })); 63 | } catch (error) { 64 | console.log("[STRIPE_ERROR]", error); 65 | return new NextResponse("Internal Error", { status: 500 }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/api/video/route.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const replicate = new Replicate({ 9 | auth: process.env.REPLICATE_API_TOKEN!, 10 | }); 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const { userId } = auth(); 15 | const body = await req.json(); 16 | const { prompt } = body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | 22 | if (!prompt) { 23 | return new NextResponse("Prompt is required", { status: 400 }); 24 | } 25 | 26 | const freeTrial = await checkApiLimit(); 27 | const isPro = await checkSubscription(); 28 | 29 | if (!freeTrial && !isPro) { 30 | return new NextResponse( 31 | "Free trial has expired. Please upgrade to pro.", 32 | { status: 403 } 33 | ); 34 | } 35 | 36 | const response = await replicate.run( 37 | "anotherjesse/zeroscope-v2-xl:71996d331e8ede8ef7bd76eba9fae076d31792e4ddf4ad057779b443d6aea62f", 38 | { 39 | input: { 40 | prompt, 41 | }, 42 | } 43 | ); 44 | 45 | if (!isPro) { 46 | await incrementApiLimit(); 47 | } 48 | 49 | return NextResponse.json(response); 50 | } catch (error) { 51 | console.log("[VIDEO_ERROR]", error); 52 | return new NextResponse("Internal Error", { status: 500 }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { headers } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import prismadb from "@/lib/prismadb"; 6 | import { stripe } from "@/lib/stripe"; 7 | 8 | export async function POST(req: Request) { 9 | const body = await req.text(); 10 | const signature = headers().get("Stripe-Signature") as string; 11 | 12 | let event: Stripe.Event; 13 | 14 | try { 15 | event = stripe.webhooks.constructEvent( 16 | body, 17 | signature, 18 | process.env.STRIPE_WEBHOOK_SECRET! 19 | ); 20 | } catch (error: any) { 21 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); 22 | } 23 | 24 | const session = event.data.object as Stripe.Checkout.Session; 25 | 26 | if (event.type === "checkout.session.completed") { 27 | const subscription = await stripe.subscriptions.retrieve( 28 | session.subscription as string 29 | ); 30 | 31 | if (!session?.metadata?.userId) { 32 | return new NextResponse("User id is required", { status: 400 }); 33 | } 34 | 35 | await prismadb.userSubscription.create({ 36 | data: { 37 | userId: session?.metadata?.userId, 38 | stripeSubscriptionId: subscription.id, 39 | stripeCustomerId: subscription.customer as string, 40 | stripePriceId: subscription.items.data[0].price.id, 41 | stripeCurrentPeriodEnd: new Date( 42 | subscription.current_period_end * 1000 43 | ), 44 | }, 45 | }); 46 | } 47 | 48 | if (event.type === "invoice.payment_succeeded") { 49 | const subscription = await stripe.subscriptions.retrieve( 50 | session.subscription as string 51 | ); 52 | 53 | await prismadb.userSubscription.update({ 54 | where: { 55 | stripeSubscriptionId: subscription.id, 56 | }, 57 | data: { 58 | stripePriceId: subscription.items.data[0].price.id, 59 | stripeCurrentPeriodEnd: new Date( 60 | subscription.current_period_end * 1000 61 | ), 62 | }, 63 | }); 64 | } 65 | 66 | return new NextResponse(null, { status: 200 }); 67 | } 68 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoGEsteves/omniscient/b6ffe3afeaf89f61b697832365eb98202b43ddbd/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 222.2 84% 4.9%; 21 | 22 | /* --primary: 222.2 47.4% 11.2%; */ 23 | --primary: 248 90% 66%; 24 | --primary-foreground: 210 40% 98%; 25 | 26 | --secondary: 210 40% 96.1%; 27 | --secondary-foreground: 222.2 47.4% 11.2%; 28 | 29 | --muted: 210 40% 96.1%; 30 | --muted-foreground: 215.4 16.3% 46.9%; 31 | 32 | --accent: 210 40% 96.1%; 33 | --accent-foreground: 222.2 47.4% 11.2%; 34 | 35 | --destructive: 0 84.2% 60.2%; 36 | --destructive-foreground: 210 40% 98%; 37 | 38 | --border: 214.3 31.8% 91.4%; 39 | --input: 214.3 31.8% 91.4%; 40 | --ring: 222.2 84% 4.9%; 41 | 42 | --radius: 0.5rem; 43 | } 44 | 45 | .dark { 46 | --background: 222.2 84% 4.9%; 47 | --foreground: 210 40% 98%; 48 | 49 | --card: 222.2 84% 4.9%; 50 | --card-foreground: 210 40% 98%; 51 | 52 | --popover: 222.2 84% 4.9%; 53 | --popover-foreground: 210 40% 98%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --secondary: 217.2 32.6% 17.5%; 59 | --secondary-foreground: 210 40% 98%; 60 | 61 | --muted: 217.2 32.6% 17.5%; 62 | --muted-foreground: 215 20.2% 65.1%; 63 | 64 | --accent: 217.2 32.6% 17.5%; 65 | --accent-foreground: 210 40% 98%; 66 | 67 | --destructive: 0 62.8% 30.6%; 68 | --destructive-foreground: 210 40% 98%; 69 | 70 | --border: 217.2 32.6% 17.5%; 71 | --input: 217.2 32.6% 17.5%; 72 | --ring: 212.7 26.8% 83.9%; 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import { ClerkProvider } from "@clerk/nextjs"; 3 | import { constructMetadata } from "@/lib/metadata"; 4 | 5 | import { ToasterProvider } from "@/components/toaster-provider"; 6 | import { ModalProvider } from "@/components/modal-provider"; 7 | import { CrispProvider } from "@/components/crisp-provider"; 8 | 9 | import "./globals.css"; 10 | 11 | const inter = Inter({ subsets: ["latin"] }); 12 | 13 | export const metadata = constructMetadata(); 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {children} 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /components/bot-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 2 | 3 | export const BotAvatar = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /components/crisp-chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { Crisp } from "crisp-sdk-web"; 5 | 6 | export const CrispChat = () => { 7 | useEffect(() => { 8 | Crisp.configure(`${process.env.CRISP_TOKEN_ID}`); 9 | }, []); 10 | 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /components/crisp-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CrispChat } from "@/components/crisp-chat"; 4 | 5 | export const CrispProvider = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /components/empty.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface IEmptyProps { 4 | label: string; 5 | } 6 | 7 | export const Empty = ({ label }: IEmptyProps) => { 8 | return ( 9 |
10 |
11 | Empty 12 |
13 |

{label}

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

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

42 | 46 |
47 | 55 |
56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { LucideIcon } from "lucide-react"; 3 | 4 | interface IHeadingProps { 5 | title: string; 6 | description: string; 7 | icon: LucideIcon; 8 | iconColor?: string; 9 | bgColor?: string; 10 | } 11 | 12 | export default function Heading({ 13 | title, 14 | description, 15 | icon: Icon, 16 | iconColor, 17 | bgColor, 18 | }: IHeadingProps) { 19 | return ( 20 |
21 |
22 | 23 |
24 | 25 |
26 |

{title}

27 |

{description}

28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/landing-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | 5 | import { testimonials } from "@/app/(landing)/constants"; 6 | 7 | export const LandingContent = () => { 8 | return ( 9 |
10 |

11 | Testimonials 12 |

13 |
14 | {testimonials.map((item) => ( 15 | 19 | 20 | 21 |
22 |

{item.name}

23 |

{item.title}

24 |
25 |
26 | 27 | {item.description} 28 | 29 |
30 |
31 | ))} 32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /components/landing-hero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TypewriterComponent from "typewriter-effect"; 4 | import Link from "next/link"; 5 | import { useAuth } from "@clerk/nextjs"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | 9 | export const LandingHero = () => { 10 | const { isSignedIn } = useAuth(); 11 | 12 | return ( 13 |
14 |
15 |

The Best AI Platform for

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

Omniscient is thinking...

10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Menu } from "lucide-react"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 7 | import Sidebar from "@/components/sidebar"; 8 | 9 | interface IMobileSidebarProps { 10 | apiLimitCount: number; 11 | isPro: boolean; 12 | } 13 | 14 | export default function MobileSidebar({ 15 | apiLimitCount = 0, 16 | isPro = false, 17 | }: IMobileSidebarProps) { 18 | return ( 19 | 20 | 30 | 31 | 35 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import { ProModal } from "@/components/pro-modal"; 6 | 7 | export const ModalProvider = () => { 8 | const [isMounted, setIsMounted] = useState(false); 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) { 15 | return null; 16 | } 17 | 18 | return ( 19 | <> 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@clerk/nextjs"; 2 | 3 | import MobileSidebar from "./mobile-sidebar"; 4 | import { getApiLimitCount } from "@/lib/api-limit"; 5 | import { checkSubscription } from "@/lib/subscription"; 6 | 7 | export default async function Navbar() { 8 | const apiLimitCount = await getApiLimitCount(); 9 | const isPro = await checkSubscription(); 10 | 11 | return ( 12 |
13 | 14 | 15 |
16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/pro-modal.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/use-pro-modal"; 19 | import { tools } from "@/app/(dashboard)/(routes)/dashboard/constants"; 20 | import { Card } from "@/components/ui/card"; 21 | import { cn } from "@/lib/utils"; 22 | 23 | export const ProModal = () => { 24 | const proModal = useProModal(); 25 | const [loading, setLoading] = useState(false); 26 | 27 | const onSubscribe = async () => { 28 | try { 29 | setLoading(true); 30 | const response = await axios.get("/api/stripe"); 31 | 32 | window.location.href = response.data.url; 33 | } catch (error) { 34 | toast.error("Something went wrong"); 35 | } finally { 36 | setLoading(false); 37 | } 38 | }; 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 |
46 | Upgrade to Omniscient 47 | 48 | pro 49 | 50 |
51 |
52 | 53 | {tools.map((tool) => ( 54 | 58 |
59 |
60 | 61 |
62 |
{tool.label}
63 |
64 | 65 |
66 | ))} 67 |
68 |
69 | 70 | 80 | 81 |
82 |
83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { Montserrat } from "next/font/google"; 6 | import { usePathname } from "next/navigation"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { routes } from "@/constants/sidebar-constants"; 10 | import { FreeCounter } from "./free-counter"; 11 | 12 | const poppins = Montserrat({ weight: "600", subsets: ["latin"] }); 13 | 14 | interface ISidebarProps { 15 | apiLimitCount: number; 16 | isPro: boolean; 17 | } 18 | 19 | export default function Sidebar({ 20 | apiLimitCount = 0, 21 | isPro = false, 22 | }: ISidebarProps) { 23 | const pathname = usePathname(); 24 | 25 | return ( 26 |
27 |
28 | 29 |
30 | Logo 31 |
32 | 33 |

34 | Omniscient 35 |

36 | 37 | 38 |
39 | {routes.map((route) => ( 40 | 50 |
51 | 52 | {route.label} 53 |
54 | 55 | ))} 56 |
57 |
58 | 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /components/subscription-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useState } from "react"; 5 | import { Zap } from "lucide-react"; 6 | import { toast } from "react-hot-toast"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | 10 | export const SubscriptionButton = ({ isPro = false }: { isPro: boolean }) => { 11 | const [loading, setLoading] = useState(false); 12 | 13 | const onClick = async () => { 14 | try { 15 | setLoading(true); 16 | 17 | const response = await axios.get("/api/stripe"); 18 | 19 | window.location.href = response.data.url; 20 | } catch (error) { 21 | toast.error("Something went wrong"); 22 | } finally { 23 | setLoading(false); 24 | } 25 | }; 26 | 27 | return ( 28 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /components/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "react-hot-toast"; 4 | 5 | export const ToasterProvider = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * 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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | premium: 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-md px-3", 27 | lg: "h-11 rounded-md 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 { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = 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 |