├── .gitignore
├── LICENSE
├── README.md
├── app
├── (auth)
│ ├── (routes)
│ │ ├── sign-in
│ │ │ └── [[...sign-in]]
│ │ │ │ └── page.tsx
│ │ └── sign-up
│ │ │ └── [[...sign-up]]
│ │ │ └── page.tsx
│ ├── error.tsx
│ └── layout.tsx
├── (dashboard)
│ ├── (routes)
│ │ ├── code
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── conversation
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ ├── image
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── music
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── settings
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ └── video
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ ├── error.tsx
│ └── layout.tsx
├── (landing)
│ ├── error.tsx
│ ├── layout.tsx
│ └── page.tsx
├── api
│ ├── code
│ │ └── route.ts
│ ├── conversation
│ │ └── route.ts
│ ├── image
│ │ └── route.ts
│ ├── music
│ │ └── route.ts
│ ├── stripe
│ │ └── route.ts
│ ├── video
│ │ └── route.ts
│ └── webhook
│ │ └── route.ts
├── favicon.ico
├── globals.css
└── layout.tsx
├── components.json
├── components
├── bot-avatar.tsx
├── crisp-chat.tsx
├── crisp-provider.tsx
├── free-counter.tsx
├── heading.tsx
├── landing-content.tsx
├── landing-hero.tsx
├── landing-navbar.tsx
├── loader.tsx
├── mobile-sidebar.tsx
├── modal-provider.tsx
├── navbar.tsx
├── pro-modal.tsx
├── query-provider.tsx
├── sidebar.tsx
├── subscription-button.tsx
├── theme-provider.tsx
├── toaster-provider.tsx
├── tool-card.tsx
├── ui
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── empty.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── progress.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ └── sheet.tsx
└── user-avatar.tsx
├── constants.ts
├── hooks
├── use-messages.tsx
└── use-pro-modal.ts
├── lib
├── api-limit.ts
├── prismadb.ts
├── stripe.ts
├── subscription.ts
└── utils.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── empty.png
├── logo.png
├── next.svg
└── vercel.svg
├── tailwind.config.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules
3 | .next
4 | .env
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Aarav Mishra
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Genius-SaaS
2 |
3 |
4 |

5 |
6 |
7 |
8 |
9 | 
10 | 
11 | 
12 |
13 |
14 |
15 | ## Introduction
16 |
17 | [Genius](https://genius-saas.vercel.app) is a cutting-edge web application built using Next.js, React, and other modern [technologies](#technologies). It is designed to showcase the implementation of Artificial Intelligence (AI) features in a Software-as-a-Service (SaaS) application. This repository serves as a foundation for creating AI-powered SaaS products, with an emphasis on the user experience and seamless integration of AI capabilities.
18 |
19 | ## Table of Contents
20 |
21 | - [Features](#features)
22 | - [Getting Started](#getting-started)
23 | - [Prerequisites](#prerequisites)
24 | - [Installation](#installation)
25 | - [Usage](#usage)
26 | - [Configuration](#configuration)
27 | - [AI Integration](#ai-integration)
28 | - [Technologies](#technologies)
29 | - [Contributing](#contributing)
30 | - [License](#license)
31 |
32 | ## Features
33 |
34 | - **AI-driven Recommendations**: The application leverages AI algorithms to provide personalized recommendations to users based on their preferences and behavior.
35 |
36 | - **Dynamic Content Generation**: AI-powered content generation allows for creating dynamic and relevant content on the fly, optimizing user engagement.
37 |
38 | - **User Behavior Prediction**: The app uses AI models to predict user behavior, aiding in user retention and improving customer satisfaction.
39 |
40 | ## Getting Started
41 |
42 | Follow the instructions below to set up the project locally on your machine.
43 |
44 | ### Prerequisites
45 |
46 | - Node.js (v14 or higher)
47 | - npm or yarn
48 |
49 | ### Installation
50 |
51 | 1. Clone the repository to your local machine:
52 |
53 | 2. Navigate to the project directory:
54 |
55 | 3. Install the dependencies:
56 |
57 | ```bash
58 | npm install
59 | ```
60 |
61 | or
62 |
63 | ```bash
64 | yarn install
65 | ```
66 |
67 | ## Usage
68 |
69 | Create a `.env.local` file in the root directory of the project and add the following environment variables:
70 |
71 | ```bash
72 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_api_key
73 | CLERK_SECRET_KEY=your_secret_key
74 |
75 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
76 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
77 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
78 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
79 |
80 | OPENAI_API_KEY=your_api_key
81 |
82 | REPLICATE_API_TOKEN=your_api_token
83 |
84 | DATABASE_URL=your_database_connection_string
85 |
86 | STRIPE_API_KEY=your_stripe_api_key
87 |
88 | NEXT_PUBLIC_APP_URL=http://localhost:3000
89 |
90 | CRISP_WEBSITE_ID=your_crisp_website_id
91 |
92 | ```
93 |
94 | Start the development server:
95 |
96 | ```bash
97 | npm run dev
98 | ```
99 |
100 | or
101 |
102 | ```bash
103 | yarn dev
104 | ```
105 |
106 | ## Screenshots
107 |
108 |
109 |
110 | ## Configuration
111 |
112 | The application uses [Clerk](https://clerk.dev/) for authentication and user management. You can sign up for a free Clerk account [here](https://dashboard.clerk.dev/).
113 |
114 | The app also uses [OpenAI](https://openai.com/) for AI-powered content generation. You can sign up for a free OpenAI account [here](https://beta.openai.com/).
115 |
116 | The app uses [Replicate](https://replicate.ai/) for AI model deployment. You can sign up for a free Replicate account [here](https://replicate.ai/).
117 |
118 | The app uses [Stripe](https://stripe.com/) for payment processing. You can sign up for a free Stripe account [here](https://dashboard.stripe.com/register).
119 |
120 | The app uses [Crisp](https://crisp.chat/) for customer support. You can sign up for a free Crisp account [here](https://crisp.chat/en/).
121 |
122 | ## AI Integration
123 |
124 | The application uses AI models to provide personalized recommendations to users based on their preferences and behavior. It also uses AI-powered content generation to create dynamic and relevant content on the fly, optimizing user engagement. The app uses AI models to predict user behavior, aiding in user retention and improving customer satisfaction.
125 |
126 | ## Technologies
127 |
128 | - [Next.js](https://nextjs.org/)
129 | - [React](https://reactjs.org/)
130 | - [Tailwind CSS](https://tailwindcss.com/)
131 | - [Clerk](https://clerk.dev/)
132 | - [OpenAI](https://openai.com/)
133 | - [Replicate](https://replicate.ai/)
134 | - [Stripe](https://stripe.com/)
135 | - [Crisp](https://crisp.chat/)
136 | - [Prisma](https://www.prisma.io/)
137 |
138 | ## Contributing
139 |
140 | Contributions are welcome! Feel free to open an issue or submit a pull request if you have any questions or suggestions.
141 |
142 | ## License
143 |
144 | Distributed under the MIT License. See `LICENSE` for more information.
145 |
146 | ## Bonus
147 |
148 | I hope this project provides a valuable resource for developers interested in exploring the potential of AI in SaaS applications. Happy coding! 🚀
149 |
--------------------------------------------------------------------------------
/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
--------------------------------------------------------------------------------
/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/app/(auth)/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Empty } from "@/components/ui/empty";
4 |
5 | const Error = () => {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default Error;
12 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | const AuthLayout = ({
2 | children
3 | }: {
4 | children: React.ReactNode;
5 | }) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
13 | export default AuthLayout;
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/code/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Prompt is required."
6 | }),
7 | });
8 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/code/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import { Code } from "lucide-react";
5 | import { useRouter } from "next/navigation";
6 | import { ChatCompletionRequestMessage } from "openai";
7 | import { useEffect, useState } from "react";
8 | import { useForm } from "react-hook-form";
9 | import { toast } from "react-hot-toast";
10 | import ReactMarkdown from "react-markdown";
11 | import * as z from "zod";
12 |
13 | import { BotAvatar } from "@/components/bot-avatar";
14 | import { Heading } from "@/components/heading";
15 | import { Loader } from "@/components/loader";
16 | import { Button } from "@/components/ui/button";
17 | import { Empty } from "@/components/ui/empty";
18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
19 | import { Input } from "@/components/ui/input";
20 | import { UserAvatar } from "@/components/user-avatar";
21 | import { useProModal } from "@/hooks/use-pro-modal";
22 | import { cn } from "@/lib/utils";
23 | import { zodResolver } from "@hookform/resolvers/zod";
24 |
25 | import { Message } from "@prisma/client";
26 | import dayjs from "dayjs";
27 | import { formSchema } from "./constants";
28 |
29 | const CodePage = () => {
30 | const router = useRouter();
31 | const proModal = useProModal();
32 | const [messages, setMessages] = useState([]);
33 |
34 | const form = useForm>({
35 | resolver: zodResolver(formSchema),
36 | defaultValues: {
37 | prompt: "",
38 | },
39 | });
40 |
41 | const isLoading = form.formState.isSubmitting;
42 |
43 | const onSubmit = async (values: z.infer) => {
44 | try {
45 | const prompt: ChatCompletionRequestMessage = { role: "user", content: values.prompt };
46 | const newMessages = [prompt];
47 |
48 | const response = await axios.post("/api/code", { messages: newMessages });
49 | await axios.put("/api/code", { botMessage: response.data.content, userMessage: values.prompt });
50 |
51 | const userMessage = {
52 | createdAt: new Date(),
53 | id: Math.random().toString(),
54 | updatedAt: new Date(),
55 | content: values.prompt,
56 | role: "user",
57 | type: "code",
58 | } as Message;
59 |
60 | const botMessage = {
61 | createdAt: new Date(),
62 | id: Math.random().toString(),
63 | updatedAt: new Date(),
64 | content: response.data.content as string,
65 | role: "assistant",
66 | type: "code",
67 | } as Message;
68 |
69 | setMessages((current) => [botMessage, userMessage, ...current]);
70 |
71 | form.reset();
72 | } catch (error: any) {
73 | if (error?.response?.status === 403) {
74 | proModal.onOpen();
75 | } else {
76 | toast.error("Something went wrong.");
77 | }
78 | } finally {
79 | router.refresh();
80 | }
81 | };
82 |
83 | const fethMessages = async () => {
84 | try {
85 | const response = await axios.get("/api/code");
86 | setMessages(response.data);
87 | } catch (error: any) {
88 | if (error?.response?.status === 403) {
89 | return;
90 | } else {
91 | if (process.env.NODE_ENV === "development") {
92 | console.log(error);
93 | }
94 | }
95 | }
96 | };
97 |
98 | useEffect(() => {
99 | fethMessages();
100 | }, []);
101 |
102 | return (
103 |
104 |
111 |
112 |
113 |
148 |
149 |
150 |
151 | {isLoading && (
152 |
153 |
154 |
155 | )}
156 | {messages.length === 0 && !isLoading &&
}
157 |
158 | {messages.map((message) => (
159 |
166 | {message.role === "user" ?
:
}
167 |
(
170 |
173 | ),
174 | code: ({ node, ...props }) =>
,
175 | }}
176 | className="text-sm overflow-hidden leading-7"
177 | >
178 | {message.content || ""}
179 |
180 | {message?.createdAt && (
181 |
182 | {dayjs(message.createdAt).format("MMM D, h:mm A")}
183 |
184 | )}
185 |
186 | ))}
187 |
188 |
189 |
190 |
191 | );
192 | };
193 |
194 | export default CodePage;
195 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/conversation/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Prompt is required."
6 | }),
7 | });
8 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/conversation/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import dayjs from "dayjs";
5 | import { MessageSquare } from "lucide-react";
6 | import { useRouter } from "next/navigation";
7 | import { ChatCompletionRequestMessage } from "openai";
8 | import { useEffect, useState } from "react";
9 | import { useForm } from "react-hook-form";
10 | import { toast } from "react-hot-toast";
11 | import * as z from "zod";
12 |
13 | import { BotAvatar } from "@/components/bot-avatar";
14 | import { Heading } from "@/components/heading";
15 | import { Loader } from "@/components/loader";
16 | import { Button } from "@/components/ui/button";
17 | import { Empty } from "@/components/ui/empty";
18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
19 | import { Input } from "@/components/ui/input";
20 | import { UserAvatar } from "@/components/user-avatar";
21 | import { useProModal } from "@/hooks/use-pro-modal";
22 | import { cn } from "@/lib/utils";
23 | import { zodResolver } from "@hookform/resolvers/zod";
24 |
25 | import { Message } from "@prisma/client";
26 | import { formSchema } from "./constants";
27 |
28 | const ConversationPage = () => {
29 | const router = useRouter();
30 | const proModal = useProModal();
31 | const [messages, setMessages] = useState>([]);
32 |
33 | const form = useForm>({
34 | resolver: zodResolver(formSchema),
35 | defaultValues: {
36 | prompt: "",
37 | },
38 | });
39 |
40 | const isLoading = form.formState.isSubmitting;
41 |
42 | const onSubmit = async (values: z.infer) => {
43 | try {
44 | const prompt: ChatCompletionRequestMessage = { role: "user", content: values.prompt };
45 | const newMessages = [prompt];
46 |
47 | const response = await axios.post("/api/conversation", { messages: newMessages });
48 | // send user message and bot response to the server
49 | await axios.put("/api/conversation", { botMessage: response.data.content, userMessage: values.prompt });
50 |
51 | const userMessage = {
52 | createdAt: new Date(),
53 | id: Math.random().toString(),
54 | updatedAt: new Date(),
55 | content: values.prompt,
56 | role: "user",
57 | type: "conversation",
58 | } as Message;
59 |
60 | const botMessage = {
61 | createdAt: new Date(),
62 | id: Math.random().toString(),
63 | updatedAt: new Date(),
64 | content: response.data.content as string,
65 | role: "assistant",
66 | type: "conversation",
67 | } as Message;
68 |
69 | setMessages((messages) => [botMessage, userMessage, ...messages] as Message[]);
70 |
71 | form.reset();
72 | } catch (error: any) {
73 | if (error?.response?.status === 403) {
74 | proModal.onOpen();
75 | } else {
76 | toast.error("Something went wrong.");
77 | console.log(error);
78 | }
79 | } finally {
80 | router.refresh();
81 | }
82 | };
83 |
84 | const fetchConversation = async () => {
85 | try {
86 | const response = await axios.get("/api/conversation");
87 | console.log(response.data);
88 | setMessages(response.data);
89 | } catch (error: any) {
90 | if (error?.response?.status === 404) {
91 | return;
92 | } else {
93 | if (process.env.NODE_ENV === "development") {
94 | console.log(error);
95 | }
96 | }
97 | } finally {
98 | router.refresh();
99 | }
100 | };
101 |
102 | useEffect(() => {
103 | fetchConversation();
104 | }, []);
105 |
106 | return (
107 |
108 |
115 |
116 |
117 |
152 |
153 |
154 |
155 | {isLoading && (
156 |
157 |
158 |
159 | )}
160 | {messages.length === 0 && !isLoading &&
}
161 |
162 | {messages.map((message) => (
163 |
170 | {message.role === "user" ?
:
}
171 |
{message.content}
172 | {message?.createdAt && (
173 |
174 | {dayjs(message.createdAt).format("MMM D, h:mm A")}
175 |
176 | )}
177 |
178 | ))}
179 |
180 |
181 |
182 |
183 | );
184 | };
185 |
186 | export default ConversationPage;
187 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ArrowRight } from "lucide-react";
4 | import { useRouter } from "next/navigation";
5 |
6 | import { Card } from "@/components/ui/card";
7 | import { cn } from "@/lib/utils";
8 |
9 | import { tools } from "@/constants";
10 |
11 | export default function HomePage() {
12 | const router = useRouter();
13 |
14 | return (
15 |
16 |
17 |
18 | Explore the power of AI
19 |
20 |
21 | Chat with the smartest AI - Experience the power of AI
22 |
23 |
24 |
25 | {tools.map((tool) => (
26 |
router.push(tool.href)} key={tool.href} className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer">
27 |
28 |
29 |
30 |
31 |
32 | {tool.label}
33 |
34 |
35 |
36 |
37 | ))}
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/image/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Photo prompt is required"
6 | }),
7 | amount: z.string().min(1),
8 | resolution: z.string().min(1),
9 | });
10 |
11 | export const amountOptions = [
12 | {
13 | value: "1",
14 | label: "1 Photo"
15 | },
16 | {
17 | value: "2",
18 | label: "2 Photos"
19 | },
20 | {
21 | value: "3",
22 | label: "3 Photos"
23 | },
24 | {
25 | value: "4",
26 | label: "4 Photos"
27 | },
28 | {
29 | value: "5",
30 | label: "5 Photos"
31 | }
32 | ];
33 |
34 | export const resolutionOptions = [
35 | {
36 | value: "256x256",
37 | label: "256x256",
38 | },
39 | {
40 | value: "512x512",
41 | label: "512x512",
42 | },
43 | {
44 | value: "1024x1024",
45 | label: "1024x1024",
46 | },
47 | ];
48 |
49 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/image/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import axios from "axios";
5 | import { Download, ImageIcon } from "lucide-react";
6 | import Image from "next/image";
7 | import { useRouter } from "next/navigation";
8 | import { useState } from "react";
9 | import { useForm } from "react-hook-form";
10 | import { toast } from "react-hot-toast";
11 | import * as z from "zod";
12 |
13 | import { Heading } from "@/components/heading";
14 | import { Loader } from "@/components/loader";
15 | import { Button } from "@/components/ui/button";
16 | import { Card, CardFooter } from "@/components/ui/card";
17 | import { Empty } from "@/components/ui/empty";
18 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
19 | import { Input } from "@/components/ui/input";
20 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
21 | import { useProModal } from "@/hooks/use-pro-modal";
22 |
23 | import { amountOptions, formSchema, resolutionOptions } from "./constants";
24 |
25 | const PhotoPage = () => {
26 | const proModal = useProModal();
27 | const router = useRouter();
28 | const [photos, setPhotos] = useState([]);
29 |
30 | const form = useForm>({
31 | resolver: zodResolver(formSchema),
32 | defaultValues: {
33 | prompt: "",
34 | amount: "1",
35 | resolution: "512x512",
36 | },
37 | });
38 |
39 | const isLoading = form.formState.isSubmitting;
40 |
41 | const onSubmit = async (values: z.infer) => {
42 | try {
43 | setPhotos([]);
44 |
45 | const response = await axios.post("/api/image", values);
46 |
47 | const urls = response.data.map((image: { url: string }) => image.url);
48 |
49 | setPhotos(urls);
50 | } catch (error: any) {
51 | if (error?.response?.status === 403) {
52 | proModal.onOpen();
53 | } else {
54 | toast.error("Something went wrong.");
55 | }
56 | } finally {
57 | router.refresh();
58 | }
59 | };
60 |
61 | return (
62 |
63 |
70 |
71 |
160 |
161 | {isLoading && (
162 |
163 |
164 |
165 | )}
166 | {photos.length === 0 && !isLoading &&
}
167 |
168 | {photos.map((src) => (
169 |
170 |
171 |
172 |
173 |
174 |
178 |
179 |
180 | ))}
181 |
182 |
183 |
184 | );
185 | };
186 |
187 | export default PhotoPage;
188 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/music/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Music prompt is required"
6 | }),
7 | });
8 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/music/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import axios from "axios";
5 | import { Music } from "lucide-react";
6 | import { useRouter } from "next/navigation";
7 | import { useState } from "react";
8 | import { useForm } from "react-hook-form";
9 | import { toast } from "react-hot-toast";
10 | import * as z from "zod";
11 |
12 | import { Heading } from "@/components/heading";
13 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
14 | import { Input } from "@/components/ui/input";
15 | import { useProModal } from "@/hooks/use-pro-modal";
16 |
17 | import { Loader } from "@/components/loader";
18 | import { Button } from "@/components/ui/button";
19 | import { Empty } from "@/components/ui/empty";
20 | import { formSchema } from "./constants";
21 |
22 | const MusicPage = () => {
23 | const proModal = useProModal();
24 | const router = useRouter();
25 | const [music, setMusic] = useState();
26 |
27 | const form = useForm>({
28 | resolver: zodResolver(formSchema),
29 | defaultValues: {
30 | prompt: "",
31 | },
32 | });
33 |
34 | const isLoading = form.formState.isSubmitting;
35 |
36 | const onSubmit = async (values: z.infer) => {
37 | try {
38 | setMusic(undefined);
39 |
40 | const response = await axios.post("/api/music", values);
41 | console.log(response);
42 |
43 | setMusic(response.data.audio);
44 | form.reset();
45 | } catch (error: any) {
46 | if (error?.response?.status === 403) {
47 | proModal.onOpen();
48 | } else {
49 | toast.error("Something went wrong.");
50 | }
51 | } finally {
52 | router.refresh();
53 | }
54 | };
55 |
56 | return (
57 |
58 |
65 |
66 |
101 |
102 | {isLoading && (
103 |
104 |
105 |
106 | )}
107 | {!music && !isLoading &&
}
108 | {music && (
109 |
112 | )}
113 |
114 |
115 | );
116 | };
117 |
118 | export default MusicPage;
119 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/settings/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Prompt is required."
6 | }),
7 | });
8 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { Settings } from "lucide-react";
2 |
3 | import { Heading } from "@/components/heading";
4 | import { SubscriptionButton } from "@/components/subscription-button";
5 | import { checkSubscription } from "@/lib/subscription";
6 |
7 | const SettingsPage = async () => {
8 | const isPro = await checkSubscription();
9 |
10 | return (
11 |
12 |
19 |
20 |
21 | {isPro ? "You are currently on a Pro plan." : "You are currently on a free plan."}
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default SettingsPage;
30 |
31 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/video/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Prompt is required."
6 | }),
7 | });
8 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/video/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import axios from "axios";
5 | import { FileAudio } from "lucide-react";
6 | import { useRouter } from "next/navigation";
7 | import { useState } from "react";
8 | import { useForm } from "react-hook-form";
9 | import { toast } from "react-hot-toast";
10 | import * as z from "zod";
11 |
12 | import { Heading } from "@/components/heading";
13 | import { Loader } from "@/components/loader";
14 | import { Button } from "@/components/ui/button";
15 | import { Empty } from "@/components/ui/empty";
16 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
17 | import { Input } from "@/components/ui/input";
18 | import { useProModal } from "@/hooks/use-pro-modal";
19 |
20 | import { formSchema } from "./constants";
21 |
22 | const VideoPage = () => {
23 | const router = useRouter();
24 | const proModal = useProModal();
25 | const [video, setVideo] = useState();
26 |
27 | const form = useForm>({
28 | resolver: zodResolver(formSchema),
29 | defaultValues: {
30 | prompt: "",
31 | },
32 | });
33 |
34 | const isLoading = form.formState.isSubmitting;
35 |
36 | const onSubmit = async (values: z.infer) => {
37 | try {
38 | setVideo(undefined);
39 |
40 | const response = await axios.post("/api/video", values);
41 |
42 | setVideo(response.data[0]);
43 | form.reset();
44 | } catch (error: any) {
45 | if (error?.response?.status === 403) {
46 | proModal.onOpen();
47 | } else {
48 | toast.error("Something went wrong.");
49 | }
50 | } finally {
51 | router.refresh();
52 | }
53 | };
54 |
55 | return (
56 |
57 |
64 |
65 |
100 |
101 | {isLoading && (
102 |
103 |
104 |
105 | )}
106 | {!video && !isLoading &&
}
107 | {video && (
108 |
111 | )}
112 |
113 |
114 | );
115 | };
116 |
117 | export default VideoPage;
118 |
--------------------------------------------------------------------------------
/app/(dashboard)/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Empty } from "@/components/ui/empty";
4 |
5 | const Error = () => {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default Error;
12 |
--------------------------------------------------------------------------------
/app/(dashboard)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from "@/components/navbar";
2 | import { Sidebar } from "@/components/sidebar";
3 | import { checkSubscription } from "@/lib/subscription";
4 | import { getApiLimitCount } from "@/lib/api-limit";
5 |
6 | const DashboardLayout = async ({
7 | children,
8 | }: {
9 | children: React.ReactNode
10 | }) => {
11 | const apiLimitCount = await getApiLimitCount();
12 | const isPro = await checkSubscription();
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
27 | export default DashboardLayout;
28 |
--------------------------------------------------------------------------------
/app/(landing)/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Empty } from "@/components/ui/empty";
4 |
5 | const Error = () => {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default Error;
12 |
--------------------------------------------------------------------------------
/app/(landing)/layout.tsx:
--------------------------------------------------------------------------------
1 | const LandingLayout = ({
2 | children
3 | }: {
4 | children: React.ReactNode;
5 | }) => {
6 | return (
7 |
8 |
9 | {children}
10 |
11 |
12 | );
13 | }
14 |
15 | export default LandingLayout;
--------------------------------------------------------------------------------
/app/(landing)/page.tsx:
--------------------------------------------------------------------------------
1 | import { LandingContent } from "@/components/landing-content";
2 | import { LandingHero } from "@/components/landing-hero";
3 | import { LandingNavbar } from "@/components/landing-navbar";
4 |
5 | const LandingPage = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default LandingPage;
16 |
--------------------------------------------------------------------------------
/app/api/code/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 | import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
4 |
5 | import prismadb from "@/lib/prismadb";
6 |
7 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit";
8 | import { checkSubscription } from "@/lib/subscription";
9 |
10 | const configuration = new Configuration({
11 | apiKey: process.env.OPENAI_API_KEY,
12 | });
13 |
14 | const openai = new OpenAIApi(configuration);
15 |
16 | const instructionMessage: ChatCompletionRequestMessage = {
17 | role: "system",
18 | content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.",
19 | };
20 |
21 | export async function POST(req: Request) {
22 | try {
23 | const { userId } = auth();
24 | const body = await req.json();
25 | const { messages } = body;
26 |
27 | if (!userId) {
28 | return new NextResponse("Unauthorized", { status: 401 });
29 | }
30 |
31 | if (!configuration.apiKey) {
32 | return new NextResponse("OpenAI API Key not configured.", { status: 500 });
33 | }
34 |
35 | if (!messages) {
36 | return new NextResponse("Messages are required", { status: 400 });
37 | }
38 |
39 | const freeTrial = await checkApiLimit();
40 | const isPro = await checkSubscription();
41 |
42 | if (!freeTrial && !isPro) {
43 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
44 | }
45 |
46 | const response = await openai.createChatCompletion({
47 | model: "gpt-3.5-turbo",
48 | messages: [instructionMessage, ...messages],
49 | });
50 |
51 | if (!isPro) {
52 | await incrementApiLimit();
53 | }
54 |
55 | return NextResponse.json(response.data.choices[0].message);
56 | } catch (error) {
57 | console.log("[CODE_ERROR]", error);
58 | return new NextResponse("Internal Error", { status: 500 });
59 | }
60 | }
61 |
62 | export async function GET() {
63 | try {
64 | const { userId } = auth();
65 |
66 | if (!userId) {
67 | return new NextResponse("Unauthorized", { status: 401 });
68 | }
69 |
70 | const freeTrial = await checkApiLimit();
71 | const isPro = await checkSubscription();
72 |
73 | if (!freeTrial && !isPro) {
74 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 });
75 | }
76 |
77 | const messages = await prismadb.message.findMany({
78 | where: {
79 | userId,
80 | type: "code",
81 | },
82 | orderBy: {
83 | createdAt: "desc",
84 | },
85 | });
86 |
87 | return new NextResponse(JSON.stringify(messages));
88 | } catch (error) {
89 | console.log("[CODE_ERROR]", error);
90 | return new NextResponse("Internal Error", { status: 500 });
91 | }
92 | }
93 |
94 | export async function PUT(req: Request) {
95 | try {
96 | const { userId } = auth();
97 | const body = await req.json();
98 | const { userMessage, botMessage } = body;
99 |
100 | if (!userId) {
101 | return new NextResponse("Unauthorized", { status: 401 });
102 | }
103 |
104 | if (!userMessage || !botMessage) {
105 | return new NextResponse("Both user message and bot message is required.", { status: 400 });
106 | }
107 |
108 | const freeTrial = await checkApiLimit();
109 | const isPro = await checkSubscription();
110 |
111 | if (!freeTrial && !isPro) {
112 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 });
113 | }
114 |
115 | await prismadb.message.create({
116 | data: {
117 | userId,
118 | type: "code",
119 | content: userMessage,
120 | role: "user",
121 | },
122 | });
123 |
124 | await prismadb.message.create({
125 | data: {
126 | userId,
127 | type: "code",
128 | content: botMessage,
129 | role: "assistant",
130 | },
131 | });
132 |
133 | return new NextResponse(null, {
134 | status: 200,
135 | });
136 | } catch (error) {
137 | console.log("[CODE_ERROR]", error);
138 | return new NextResponse("Internal Error", { status: 500 });
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/app/api/conversation/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 | import { Configuration, OpenAIApi } from "openai";
4 |
5 | import prismadb from "@/lib/prismadb";
6 |
7 | import { checkApiLimit, incrementApiLimit } from "@/lib/api-limit";
8 | import { checkSubscription } from "@/lib/subscription";
9 |
10 | const configuration = new Configuration({
11 | apiKey: process.env.OPENAI_API_KEY,
12 | });
13 |
14 | const openai = new OpenAIApi(configuration);
15 |
16 | export async function POST(req: Request) {
17 | try {
18 | const { userId } = auth();
19 | const body = await req.json();
20 | const { messages } = body;
21 |
22 | if (!userId) {
23 | return new NextResponse("Unauthorized", { status: 401 });
24 | }
25 |
26 | if (!configuration.apiKey) {
27 | return new NextResponse("OpenAI API Key not configured.", { status: 500 });
28 | }
29 |
30 | if (!messages) {
31 | return new NextResponse("Messages are required", { status: 400 });
32 | }
33 |
34 | const freeTrial = await checkApiLimit();
35 | const isPro = await checkSubscription();
36 |
37 | if (!freeTrial && !isPro) {
38 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
39 | }
40 |
41 | const response = await openai.createChatCompletion({
42 | model: "gpt-3.5-turbo",
43 | messages,
44 | });
45 |
46 | if (!isPro) {
47 | await incrementApiLimit();
48 | }
49 |
50 | return NextResponse.json(response.data.choices[0].message);
51 | } catch (error) {
52 | console.log("[CONVERSATION_ERROR]", error);
53 | return new NextResponse("Internal Error", { status: 500 });
54 | }
55 | }
56 |
57 | export async function GET() {
58 | try {
59 | const { userId } = auth();
60 |
61 | if (!userId) {
62 | return new NextResponse("Unauthorized", { status: 401 });
63 | }
64 |
65 | const freeTrial = await checkApiLimit();
66 | const isPro = await checkSubscription();
67 |
68 | if (!freeTrial && !isPro) {
69 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 });
70 | }
71 |
72 | const messages = await prismadb.message.findMany({
73 | where: {
74 | userId,
75 | type: "conversation",
76 | },
77 | orderBy: {
78 | createdAt: "desc",
79 | },
80 | });
81 |
82 | return new NextResponse(JSON.stringify(messages));
83 | } catch (error) {
84 | console.log("[CODE_ERROR]", error);
85 | return new NextResponse("Internal Error", { status: 500 });
86 | }
87 | }
88 |
89 | export async function PUT(req: Request) {
90 | try {
91 | const { userId } = auth();
92 | const body = await req.json();
93 | const { userMessage, botMessage } = body;
94 |
95 | if (!userId) {
96 | return new NextResponse("Unauthorized", { status: 401 });
97 | }
98 |
99 | if (!userMessage || !botMessage) {
100 | return new NextResponse("Both user message and bot message is required.", { status: 400 });
101 | }
102 |
103 | const freeTrial = await checkApiLimit();
104 | const isPro = await checkSubscription();
105 |
106 | if (!freeTrial && !isPro) {
107 | return new NextResponse("No cloud storage for free trial. Please upgrade to pro.", { status: 404 });
108 | }
109 |
110 | await prismadb.message.create({
111 | data: {
112 | userId,
113 | type: "conversation",
114 | content: userMessage,
115 | role: "user",
116 | },
117 | });
118 |
119 | await prismadb.message.create({
120 | data: {
121 | userId,
122 | type: "conversation",
123 | content: botMessage,
124 | role: "assistant",
125 | },
126 | });
127 |
128 | return new NextResponse(null, {
129 | status: 200,
130 | });
131 | } catch (error) {
132 | console.log("[CODE_ERROR]", error);
133 | return new NextResponse("Internal Error", { status: 500 });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/api/image/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 | import { Configuration, OpenAIApi } from "openai";
4 |
5 | import { checkSubscription } from "@/lib/subscription";
6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
7 |
8 | const configuration = new Configuration({
9 | apiKey: process.env.OPENAI_API_KEY,
10 | });
11 |
12 | const openai = new OpenAIApi(configuration);
13 |
14 | export async function POST(
15 | req: Request
16 | ) {
17 | try {
18 | const { userId } = auth();
19 | const body = await req.json();
20 | const { prompt, amount = 1, resolution = "512x512" } = body;
21 |
22 | if (!userId) {
23 | return new NextResponse("Unauthorized", { status: 401 });
24 | }
25 |
26 | if (!configuration.apiKey) {
27 | return new NextResponse("OpenAI API Key not configured.", { status: 500 });
28 | }
29 |
30 | if (!prompt) {
31 | return new NextResponse("Prompt is required", { status: 400 });
32 | }
33 |
34 | if (!amount) {
35 | return new NextResponse("Amount is required", { status: 400 });
36 | }
37 |
38 | if (!resolution) {
39 | return new NextResponse("Resolution is required", { status: 400 });
40 | }
41 |
42 | const freeTrial = await checkApiLimit();
43 | const isPro = await checkSubscription();
44 |
45 | if (!freeTrial && !isPro) {
46 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
47 | }
48 |
49 | const response = await openai.createImage({
50 | prompt,
51 | n: parseInt(amount, 10),
52 | size: resolution,
53 | });
54 |
55 | if (!isPro) {
56 | await incrementApiLimit();
57 | }
58 |
59 | return NextResponse.json(response.data.data);
60 | } catch (error) {
61 | console.log('[IMAGE_ERROR]', error);
62 | return new NextResponse("Internal Error", { status: 500 });
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/app/api/music/route.ts:
--------------------------------------------------------------------------------
1 | import Replicate from "replicate";
2 | import { auth } from "@clerk/nextjs";
3 | import { NextResponse } from "next/server";
4 |
5 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
6 | import { checkSubscription } from "@/lib/subscription";
7 |
8 | const replicate = new Replicate({
9 | auth: process.env.REPLICATE_API_TOKEN!,
10 | });
11 |
12 | export async function POST(
13 | req: Request
14 | ) {
15 | try {
16 | const { userId } = auth();
17 | const body = await req.json();
18 | const { prompt } = body;
19 |
20 | if (!userId) {
21 | return new NextResponse("Unauthorized", { status: 401 });
22 | }
23 |
24 | if (!prompt) {
25 | return new NextResponse("Prompt is required", { status: 400 });
26 | }
27 |
28 | const freeTrial = await checkApiLimit();
29 | const isPro = await checkSubscription();
30 |
31 | if (!freeTrial && !isPro) {
32 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
33 | }
34 |
35 | const response = await replicate.run(
36 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05",
37 | {
38 | input: {
39 | prompt_a: prompt
40 | }
41 | }
42 | );
43 |
44 | if (!isPro) {
45 | await incrementApiLimit();
46 | }
47 |
48 | return NextResponse.json(response);
49 | } catch (error) {
50 | console.log('[MUSIC_ERROR]', error);
51 | return new NextResponse("Internal Error", { status: 500 });
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/app/api/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { auth, currentUser } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 |
4 | import prismadb from "@/lib/prismadb";
5 | import { stripe } from "@/lib/stripe";
6 | import { absoluteUrl } from "@/lib/utils";
7 |
8 | const settingsUrl = absoluteUrl("/settings");
9 |
10 | export async function GET() {
11 | try {
12 | const { userId } = auth();
13 | const user = await currentUser();
14 |
15 | if (!userId || !user) {
16 | return new NextResponse("Unauthorized", { status: 401 });
17 | }
18 |
19 | const userSubscription = await prismadb.userSubscription.findUnique({
20 | where: {
21 | userId,
22 | },
23 | });
24 |
25 | if (userSubscription && userSubscription.stripeCustomerId) {
26 | const stripeSession = await stripe.billingPortal.sessions.create({
27 | customer: userSubscription.stripeCustomerId,
28 | return_url: settingsUrl,
29 | });
30 |
31 | return new NextResponse(JSON.stringify({ url: stripeSession.url }));
32 | }
33 |
34 | const stripeSession = await stripe.checkout.sessions.create({
35 | success_url: settingsUrl,
36 | cancel_url: settingsUrl,
37 | payment_method_types: ["card"],
38 | mode: "subscription",
39 | billing_address_collection: "auto",
40 | customer_email: user.emailAddresses[0].emailAddress,
41 | line_items: [
42 | {
43 | price_data: {
44 | currency: "USD",
45 | product_data: {
46 | name: "Genius Pro",
47 | description: "Unlimited AI Generations",
48 | },
49 | unit_amount: 2000,
50 | recurring: {
51 | interval: "month",
52 | },
53 | },
54 | quantity: 1,
55 | },
56 | ],
57 | metadata: {
58 | userId,
59 | },
60 | });
61 |
62 | return new NextResponse(JSON.stringify({ url: stripeSession.url }));
63 | } catch (error) {
64 | console.log("[STRIPE_ERROR]", error);
65 | return new NextResponse("Internal Error", { status: 500 });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/api/video/route.ts:
--------------------------------------------------------------------------------
1 | import Replicate from "replicate";
2 | import { auth } from "@clerk/nextjs";
3 | import { NextResponse } from "next/server";
4 |
5 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
6 | import { checkSubscription } from "@/lib/subscription";
7 |
8 | const replicate = new Replicate({
9 | auth: process.env.REPLICATE_API_TOKEN!,
10 | });
11 |
12 | export async function POST(
13 | req: Request
14 | ) {
15 | try {
16 | const { userId } = auth();
17 | const body = await req.json();
18 | const { prompt } = body;
19 |
20 | if (!userId) {
21 | return new NextResponse("Unauthorized", { status: 401 });
22 | }
23 |
24 | if (!prompt) {
25 | return new NextResponse("Prompt is required", { status: 400 });
26 | }
27 |
28 | const freeTrial = await checkApiLimit();
29 | const isPro = await checkSubscription();
30 |
31 | if (!freeTrial && !isPro) {
32 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
33 | }
34 |
35 | const response = await replicate.run(
36 | "anotherjesse/zeroscope-v2-xl:71996d331e8ede8ef7bd76eba9fae076d31792e4ddf4ad057779b443d6aea62f",
37 | {
38 | input: {
39 | prompt,
40 | }
41 | }
42 | );
43 |
44 | if (!isPro) {
45 | await incrementApiLimit();
46 | }
47 |
48 | return NextResponse.json(response);
49 | } catch (error) {
50 | console.log('[VIDEO_ERROR]', error);
51 | return new NextResponse("Internal Error", { status: 500 });
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers";
2 | import { NextResponse } from "next/server";
3 | import Stripe from "stripe";
4 |
5 | import prismadb from "@/lib/prismadb";
6 | import { stripe } from "@/lib/stripe";
7 |
8 | export async function POST(req: Request) {
9 | const body = await req.text();
10 | const signature = headers().get("Stripe-Signature") as string;
11 |
12 | let event: Stripe.Event;
13 |
14 | try {
15 | event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
16 | } catch (error: any) {
17 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
18 | }
19 |
20 | const session = event.data.object as Stripe.Checkout.Session;
21 |
22 | if (event.type === "checkout.session.completed") {
23 | const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
24 |
25 | if (!session?.metadata?.userId) {
26 | return new NextResponse("User id is required", { status: 400 });
27 | }
28 |
29 | await prismadb.userSubscription.create({
30 | data: {
31 | userId: session?.metadata?.userId,
32 | stripeSubscriptionId: subscription.id,
33 | stripeCustomerId: subscription.customer as string,
34 | stripePriceId: subscription.items.data[0].price.id,
35 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
36 | },
37 | });
38 | }
39 |
40 | if (event.type === "invoice.payment_succeeded") {
41 | const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
42 |
43 | await prismadb.userSubscription.update({
44 | where: {
45 | stripeSubscriptionId: subscription.id,
46 | },
47 | data: {
48 | stripePriceId: subscription.items.data[0].price.id,
49 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
50 | },
51 | });
52 | }
53 |
54 | return new NextResponse(null, { status: 200 });
55 | }
56 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devaaravmishra/genius-saas/e205eccd6cef0bc13939e15d1447ab492f220b0d/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 20 14.3% 4.1%;
15 |
16 | --muted: 60 4.8% 95.9%;
17 | --muted-foreground: 25 5.3% 44.7%;
18 |
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 20 14.3% 4.1%;
21 |
22 | --card: 0 0% 100%;
23 | --card-foreground: 20 14.3% 4.1%;
24 |
25 | --border: 20 5.9% 90%;
26 | --input: 20 5.9% 90%;
27 |
28 | --primary: 248 90% 66%;
29 | --primary-foreground: 60 9.1% 97.8%;
30 |
31 | --secondary: 60 4.8% 95.9%;
32 | --secondary-foreground: 24 9.8% 10%;
33 |
34 | --accent: 60 4.8% 95.9%;
35 | --accent-foreground: 24 9.8% 10%;
36 |
37 | --destructive: 0 84.2% 60.2%;
38 | --destructive-foreground: 60 9.1% 97.8%;
39 |
40 | --ring: 248 90% 66%;
41 |
42 | --radius: 0.5rem;
43 | }
44 |
45 | .dark {
46 | --background: 20 14.3% 4.1%;
47 | --foreground: 60 9.1% 97.8%;
48 |
49 | --muted: 12 6.5% 15.1%;
50 | --muted-foreground: 24 5.4% 63.9%;
51 |
52 | --popover: 20 14.3% 4.1%;
53 | --popover-foreground: 60 9.1% 97.8%;
54 |
55 | --card: 20 14.3% 4.1%;
56 | --card-foreground: 60 9.1% 97.8%;
57 |
58 | --border: 12 6.5% 15.1%;
59 | --input: 12 6.5% 15.1%;
60 |
61 | --primary: 60 9.1% 97.8%;
62 | --primary-foreground: 24 9.8% 10%;
63 |
64 | --secondary: 12 6.5% 15.1%;
65 | --secondary-foreground: 60 9.1% 97.8%;
66 |
67 | --accent: 12 6.5% 15.1%;
68 | --accent-foreground: 60 9.1% 97.8%;
69 |
70 | --destructive: 0 62.8% 30.6%;
71 | --destructive-foreground: 0 85.7% 97.3%;
72 |
73 | --ring: 12 6.5% 15.1%;
74 | }
75 | }
76 |
77 | @layer base {
78 | * {
79 | @apply border-border;
80 | }
81 | body {
82 | @apply bg-background text-foreground;
83 | }
84 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkProvider } from "@clerk/nextjs";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 |
5 | import { CrispProvider } from "@/components/crisp-provider";
6 | import { ModalProvider } from "@/components/modal-provider";
7 | import { QueryProvider } from "@/components/query-provider";
8 | import { ToasterProvider } from "@/components/toaster-provider";
9 |
10 | import "./globals.css";
11 |
12 | const font = Inter({ subsets: ["latin"] });
13 |
14 | export const metadata: Metadata = {
15 | title: "Genius",
16 | description: "AI Platform",
17 | };
18 |
19 | export default async function RootLayout({ children }: { children: React.ReactNode }) {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/bot-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarImage } from "@/components/ui/avatar";
2 |
3 | export const BotAvatar = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/components/crisp-chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Crisp } from "crisp-sdk-web";
4 | import { useEffect } from "react";
5 |
6 | export const CrispChat = () => {
7 | useEffect(() => {
8 | Crisp.configure("afa20cf2-fe7d-446f-9e2d-81a1e6ab0299");
9 | }, []);
10 |
11 | return null;
12 | };
13 |
--------------------------------------------------------------------------------
/components/crisp-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CrispChat } from "@/components/crisp-chat";
4 |
5 | export const CrispProvider = () => {
6 | return
7 | };
8 |
--------------------------------------------------------------------------------
/components/free-counter.tsx:
--------------------------------------------------------------------------------
1 | import { Zap } from "lucide-react";
2 | import { useEffect, useState } from "react";
3 |
4 | import { MAX_FREE_COUNTS } from "@/constants";
5 | import { Card, CardContent } from "@/components/ui/card";
6 | import { Button } from "@/components/ui/button";
7 | import { Progress } from "@/components/ui/progress";
8 | import { useProModal } from "@/hooks/use-pro-modal";
9 |
10 | export const FreeCounter = ({
11 | isPro = false,
12 | apiLimitCount = 0,
13 | }: {
14 | isPro: boolean,
15 | apiLimitCount: number
16 | }) => {
17 | const [mounted, setMounted] = useState(false);
18 | const proModal = useProModal();
19 |
20 | useEffect(() => {
21 | setMounted(true);
22 | }, []);
23 |
24 | if (!mounted) {
25 | return null;
26 | }
27 |
28 |
29 | if (isPro) {
30 | return null;
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {apiLimitCount} / {MAX_FREE_COUNTS} Free Generations
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 | )
51 | }
--------------------------------------------------------------------------------
/components/heading.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from "lucide-react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | interface HeadingProps {
6 | title: string;
7 | description: string;
8 | icon: Icon;
9 | iconColor?: string;
10 | bgColor?: string;
11 | }
12 |
13 | export const Heading = ({
14 | title,
15 | description,
16 | icon: Icon,
17 | iconColor,
18 | bgColor,
19 | }: HeadingProps) => {
20 | return (
21 | <>
22 |
23 |
24 |
25 |
26 |
27 |
{title}
28 |
29 | {description}
30 |
31 |
32 |
33 | >
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/components/landing-content.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4 |
5 | const testimonials = [
6 | {
7 | name: "Joel",
8 | avatar: "J",
9 | title: "Software Engineer",
10 | description: "This is the best application I've ever used!",
11 | },
12 | {
13 | name: "Antonio",
14 | avatar: "A",
15 | title: "Designer",
16 | description: "I use this daily for generating new photos!",
17 | },
18 | {
19 | name: "Mark",
20 | avatar: "M",
21 | title: "CEO",
22 | description: "This app has changed my life, cannot imagine working without it!",
23 | },
24 | {
25 | name: "Mary",
26 | avatar: "M",
27 | title: "CFO",
28 | description: "The best in class, definitely worth the premium subscription!",
29 | },
30 | ];
31 |
32 | export const LandingContent = () => {
33 | return (
34 |
35 |
Testimonials
36 |
37 | {testimonials.map((item) => (
38 |
39 |
40 |
41 |
42 |
{item.name}
43 |
{item.title}
44 |
45 |
46 |
47 | {item.description}
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 | )
55 | }
--------------------------------------------------------------------------------
/components/landing-hero.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import TypewriterComponent from "typewriter-effect";
4 | import Link from "next/link";
5 | import { useAuth } from "@clerk/nextjs";
6 |
7 | import { Button } from "@/components/ui/button";
8 |
9 | export const LandingHero = () => {
10 | const { isSignedIn } = useAuth();
11 |
12 | return (
13 |
14 |
15 |
The Best AI Tool for
16 |
17 |
29 |
30 |
31 |
32 | Create content using AI 10x faster.
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 | No credit card required.
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/components/landing-navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Montserrat } from "next/font/google";
4 | import Image from "next/image"
5 | import Link from "next/link"
6 | import { useAuth } from "@clerk/nextjs";
7 |
8 | import { cn } from "@/lib/utils";
9 | import { Button } from "@/components/ui/button";
10 |
11 | const font = Montserrat({ weight: '600', subsets: ['latin'] });
12 |
13 | export const LandingNavbar = () => {
14 | const { isSignedIn } = useAuth();
15 |
16 | return (
17 |
34 | )
35 | }
--------------------------------------------------------------------------------
/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 |
3 | export const Loader = () => {
4 | return (
5 |
6 |
7 |
12 |
13 |
14 | Genius is thinking...
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/components/mobile-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { Menu } from "lucide-react";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
8 | import { Sidebar } from "@/components/sidebar";
9 |
10 | export const MobileSidebar = ({
11 | apiLimitCount = 0,
12 | isPro = false
13 | }: {
14 | apiLimitCount: number;
15 | isPro: boolean;
16 | }) => {
17 | const [isMounted, setIsMounted] = useState(false);
18 |
19 | useEffect(() => {
20 | setIsMounted(true);
21 | }, []);
22 |
23 | if (!isMounted) {
24 | return null;
25 | }
26 |
27 | return (
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/components/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import { ProModal } from "@/components/pro-modal";
6 |
7 | export const ModalProvider = () => {
8 | const [isMounted, setIsMounted] = useState(false);
9 |
10 | useEffect(() => {
11 | setIsMounted(true);
12 | }, []);
13 |
14 | if (!isMounted) {
15 | return null;
16 | }
17 |
18 | return (
19 | <>
20 |
21 | >
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { UserButton } from "@clerk/nextjs";
2 |
3 | import { MobileSidebar } from "@/components/mobile-sidebar";
4 | import { getApiLimitCount } from "@/lib/api-limit";
5 | import { checkSubscription } from "@/lib/subscription";
6 |
7 | const Navbar = async () => {
8 | const apiLimitCount = await getApiLimitCount();
9 | const isPro = await checkSubscription();
10 |
11 | return (
12 |
18 | );
19 | }
20 |
21 | export default Navbar;
--------------------------------------------------------------------------------
/components/pro-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import { Check, Cloud, Zap } from "lucide-react";
5 | import { useState } from "react";
6 | import { toast } from "react-hot-toast";
7 |
8 | import { Badge } from "@/components/ui/badge";
9 | import { Button } from "@/components/ui/button";
10 | import { Card } from "@/components/ui/card";
11 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
12 | import { tools } from "@/constants";
13 | import { useProModal } from "@/hooks/use-pro-modal";
14 | import { cn } from "@/lib/utils";
15 |
16 | export const ProModal = () => {
17 | const proModal = useProModal();
18 | const [loading, setLoading] = useState(false);
19 |
20 | const onSubscribe = async () => {
21 | try {
22 | setLoading(true);
23 | const response = await axios.get("/api/stripe");
24 |
25 | window.location.href = response.data.url;
26 | } catch (error) {
27 | toast.error("Something went wrong");
28 | } finally {
29 | setLoading(false);
30 | }
31 | };
32 |
33 | return (
34 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/components/query-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5 | import { useEffect, useState } from "react";
6 |
7 | export const QueryProvider = () => {
8 | const [isMounted, setIsMounted] = useState(false);
9 |
10 | useEffect(() => {
11 | setIsMounted(true);
12 | }, []);
13 |
14 | if (!isMounted) {
15 | return null;
16 | }
17 |
18 | const queryClient = new QueryClient();
19 | return (
20 | <>
21 |
22 |
23 |
24 | >
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import Image from "next/image";
5 | import { Montserrat } from 'next/font/google'
6 | import { Code, ImageIcon, LayoutDashboard, MessageSquare, Music, Settings, VideoIcon } from "lucide-react";
7 | import { usePathname } from "next/navigation";
8 |
9 | import { cn } from "@/lib/utils";
10 | import { FreeCounter } from "@/components/free-counter";
11 |
12 | const poppins = Montserrat ({ weight: '600', subsets: ['latin'] });
13 |
14 | const routes = [
15 | {
16 | label: 'Dashboard',
17 | icon: LayoutDashboard,
18 | href: '/dashboard',
19 | color: "text-sky-500"
20 | },
21 | {
22 | label: 'Conversation',
23 | icon: MessageSquare,
24 | href: '/conversation',
25 | color: "text-violet-500",
26 | },
27 | {
28 | label: 'Image Generation',
29 | icon: ImageIcon,
30 | color: "text-pink-700",
31 | href: '/image',
32 | },
33 | {
34 | label: 'Video Generation',
35 | icon: VideoIcon,
36 | color: "text-orange-700",
37 | href: '/video',
38 | },
39 | {
40 | label: 'Music Generation',
41 | icon: Music,
42 | color: "text-emerald-500",
43 | href: '/music',
44 | },
45 | {
46 | label: 'Code Generation',
47 | icon: Code,
48 | color: "text-green-700",
49 | href: '/code',
50 | },
51 | {
52 | label: 'Settings',
53 | icon: Settings,
54 | href: '/settings',
55 | },
56 | ];
57 |
58 | export const Sidebar = ({
59 | apiLimitCount = 0,
60 | isPro = false
61 | }: {
62 | apiLimitCount: number;
63 | isPro: boolean;
64 | }) => {
65 | const pathname = usePathname();
66 |
67 | return (
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | Genius
76 |
77 |
78 |
79 | {routes.map((route) => (
80 |
88 |
89 |
90 | {route.label}
91 |
92 |
93 | ))}
94 |
95 |
96 |
100 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/components/subscription-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import { useState } from "react";
5 | import { Zap } from "lucide-react";
6 | import { toast } from "react-hot-toast";
7 |
8 | import { Button } from "@/components/ui/button";
9 |
10 | export const SubscriptionButton = ({
11 | isPro = false
12 | }: {
13 | isPro: boolean;
14 | }) => {
15 | const [loading, setLoading] = useState(false);
16 |
17 | const onClick = async () => {
18 | try {
19 | setLoading(true);
20 |
21 | const response = await axios.get("/api/stripe");
22 |
23 | window.location.href = response.data.url;
24 | } catch (error) {
25 | toast.error("Something went wrong");
26 | } finally {
27 | setLoading(false);
28 | }
29 | };
30 |
31 | return (
32 |
36 | )
37 | };
38 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { type ThemeProviderProps } from "next-themes/dist/types"
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/components/toaster-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Toaster } from "react-hot-toast"
4 |
5 | export const ToasterProvider = () => {
6 | return
7 | };
8 |
--------------------------------------------------------------------------------
/components/tool-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 |
5 | import {
6 | Card,
7 | CardHeader,
8 | CardTitle,
9 | CardDescription,
10 | CardContent,
11 | } from "@/components/ui/card";
12 | import { Badge } from "@/components/ui/badge";
13 | import { useRouter } from "next/navigation";
14 |
15 | interface ToolCardProps {
16 | src: string,
17 | href: string,
18 | title: string,
19 | description: string,
20 | premium?: boolean;
21 | }
22 |
23 | export const ToolCard = ({
24 | src,
25 | href,
26 | title,
27 | description,
28 | premium
29 | }: ToolCardProps) => {
30 | const router = useRouter();
31 |
32 | const onClick = () => {
33 | router.push(href);
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 | {title}
44 |
45 | {description}
46 | {premium && (
47 |
48 | pro
49 |
50 | )}
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.25 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0"
19 | },
20 | },
21 | defaultVariants: {
22 | variant: "default",
23 | },
24 | }
25 | )
26 |
27 | export interface BadgeProps
28 | extends React.HTMLAttributes,
29 | VariantProps {}
30 |
31 | function Badge({ className, variant, ...props }: BadgeProps) {
32 | return (
33 |
34 | )
35 | }
36 |
37 | export { Badge, badgeVariants }
38 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0",
22 | },
23 | size: {
24 | default: "h-10 px-4 py-2",
25 | sm: "h-9 rounded-md px-3",
26 | lg: "h-11 rounded-md px-8",
27 | icon: "h-10 w-10",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | ...props
16 | }: DialogPrimitive.DialogPortalProps) => (
17 |
18 | )
19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
20 |
21 | const DialogOverlay = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
33 | ))
34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
35 |
36 | const DialogContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, children, ...props }, ref) => (
40 |
41 |
42 |
50 | {children}
51 |
52 |
53 | Close
54 |
55 |
56 |
57 | ))
58 | DialogContent.displayName = DialogPrimitive.Content.displayName
59 |
60 | const DialogHeader = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | DialogHeader.displayName = "DialogHeader"
73 |
74 | const DialogFooter = ({
75 | className,
76 | ...props
77 | }: React.HTMLAttributes) => (
78 |
85 | )
86 | DialogFooter.displayName = "DialogFooter"
87 |
88 | const DialogTitle = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
100 | ))
101 | DialogTitle.displayName = DialogPrimitive.Title.displayName
102 |
103 | const DialogDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | DialogDescription.displayName = DialogPrimitive.Description.displayName
114 |
115 | export {
116 | Dialog,
117 | DialogTrigger,
118 | DialogContent,
119 | DialogHeader,
120 | DialogFooter,
121 | DialogTitle,
122 | DialogDescription,
123 | }
124 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/empty.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 |
4 | interface EmptyProps {
5 | label: string;
6 | }
7 |
8 | export const Empty = ({
9 | label
10 | }: EmptyProps) => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | {label}
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ))
63 | SelectContent.displayName = SelectPrimitive.Content.displayName
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ))
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ))
98 | SelectItem.displayName = SelectPrimitive.Item.displayName
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator,
121 | }
122 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = ({
17 | className,
18 | ...props
19 | }: SheetPrimitive.DialogPortalProps) => (
20 |
21 | )
22 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
23 |
24 | const SheetOverlay = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, ...props }, ref) => (
28 |
36 | ))
37 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
38 |
39 | const sheetVariants = cva(
40 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
41 | {
42 | variants: {
43 | side: {
44 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
45 | bottom:
46 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
47 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
48 | right:
49 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
50 | },
51 | },
52 | defaultVariants: {
53 | side: "right",
54 | },
55 | }
56 | )
57 |
58 | interface SheetContentProps
59 | extends React.ComponentPropsWithoutRef,
60 | VariantProps {}
61 |
62 | const SheetContent = React.forwardRef<
63 | React.ElementRef,
64 | SheetContentProps
65 | >(({ side = "right", className, children, ...props }, ref) => (
66 |
67 |
68 |
73 | {children}
74 |
75 |
76 | Close
77 |
78 |
79 |
80 | ))
81 | SheetContent.displayName = SheetPrimitive.Content.displayName
82 |
83 | const SheetHeader = ({
84 | className,
85 | ...props
86 | }: React.HTMLAttributes) => (
87 |
94 | )
95 | SheetHeader.displayName = "SheetHeader"
96 |
97 | const SheetFooter = ({
98 | className,
99 | ...props
100 | }: React.HTMLAttributes) => (
101 |
108 | )
109 | SheetFooter.displayName = "SheetFooter"
110 |
111 | const SheetTitle = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
120 | ))
121 | SheetTitle.displayName = SheetPrimitive.Title.displayName
122 |
123 | const SheetDescription = React.forwardRef<
124 | React.ElementRef,
125 | React.ComponentPropsWithoutRef
126 | >(({ className, ...props }, ref) => (
127 |
132 | ))
133 | SheetDescription.displayName = SheetPrimitive.Description.displayName
134 |
135 | export {
136 | Sheet,
137 | SheetTrigger,
138 | SheetClose,
139 | SheetContent,
140 | SheetHeader,
141 | SheetFooter,
142 | SheetTitle,
143 | SheetDescription,
144 | }
145 |
--------------------------------------------------------------------------------
/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { useUser } from "@clerk/nextjs";
2 |
3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4 |
5 | export const UserAvatar = () => {
6 | const { user } = useUser();
7 |
8 | return (
9 |
10 |
11 |
12 | {user?.firstName?.charAt(0)}
13 | {user?.lastName?.charAt(0)}
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/constants.ts:
--------------------------------------------------------------------------------
1 | import { Code, ImageIcon, MessageSquare, Music, VideoIcon } from "lucide-react";
2 |
3 | export const MAX_FREE_COUNTS = 5;
4 |
5 | export const tools = [
6 | {
7 | label: "Conversation",
8 | icon: MessageSquare,
9 | href: "/conversation",
10 | color: "text-violet-500",
11 | bgColor: "bg-violet-500/10",
12 | },
13 | {
14 | label: "Music Generation",
15 | icon: Music,
16 | href: "/music",
17 | color: "text-emerald-500",
18 | bgColor: "bg-emerald-500/10",
19 | },
20 | {
21 | label: "Image Generation",
22 | icon: ImageIcon,
23 | color: "text-pink-700",
24 | bgColor: "bg-pink-700/10",
25 | href: "/image",
26 | },
27 | {
28 | label: "Video Generation",
29 | icon: VideoIcon,
30 | color: "text-orange-700",
31 | bgColor: "bg-orange-700/10",
32 | href: "/video",
33 | },
34 | {
35 | label: "Code Generation",
36 | icon: Code,
37 | color: "text-green-700",
38 | bgColor: "bg-green-700/10",
39 | href: "/code",
40 | },
41 | ];
42 |
--------------------------------------------------------------------------------
/hooks/use-messages.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useAuth } from "@clerk/nextjs";
4 | import { useQuery } from "@tanstack/react-query";
5 | const { userId } = useAuth();
6 |
7 | const useMessages = (endpoint: string) =>
8 | useQuery({
9 | queryKey: ["messages", userId],
10 | queryFn: async () => {
11 | const response = await fetch(`/api/${endpoint}`);
12 | const data = await response.json();
13 | return data;
14 | },
15 | });
16 |
17 | export default useMessages;
18 |
--------------------------------------------------------------------------------
/hooks/use-pro-modal.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | interface useProModalStore {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | export const useProModal = create((set) => ({
10 | isOpen: false,
11 | onOpen: () => set({ isOpen: true }),
12 | onClose: () => set({ isOpen: false }),
13 | }));
14 |
--------------------------------------------------------------------------------
/lib/api-limit.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs";
2 |
3 | import prismadb from "@/lib/prismadb";
4 | import { MAX_FREE_COUNTS } from "@/constants";
5 |
6 | export const incrementApiLimit = async () => {
7 | const { userId } = auth();
8 |
9 | if (!userId) {
10 | return;
11 | }
12 |
13 | const userApiLimit = await prismadb.userApiLimit.findUnique({
14 | where: { userId: userId },
15 | });
16 |
17 | if (userApiLimit) {
18 | await prismadb.userApiLimit.update({
19 | where: { userId: userId },
20 | data: { count: userApiLimit.count + 1 },
21 | });
22 | } else {
23 | await prismadb.userApiLimit.create({
24 | data: { userId: userId, count: 1 },
25 | });
26 | }
27 | };
28 |
29 | export const checkApiLimit = async () => {
30 | const { userId } = auth();
31 |
32 | if (!userId) {
33 | return false;
34 | }
35 |
36 | const userApiLimit = await prismadb.userApiLimit.findUnique({
37 | where: { userId: userId },
38 | });
39 |
40 | if (!userApiLimit || userApiLimit.count < MAX_FREE_COUNTS) {
41 | return true;
42 | } else {
43 | return false;
44 | }
45 | };
46 |
47 | export const getApiLimitCount = async () => {
48 | const { userId } = auth();
49 |
50 | if (!userId) {
51 | return 0;
52 | }
53 |
54 | const userApiLimit = await prismadb.userApiLimit.findUnique({
55 | where: {
56 | userId
57 | }
58 | });
59 |
60 | if (!userApiLimit) {
61 | return 0;
62 | }
63 |
64 | return userApiLimit.count;
65 | };
66 |
--------------------------------------------------------------------------------
/lib/prismadb.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client"
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined
5 | }
6 |
7 | const prismadb = globalThis.prisma || new PrismaClient()
8 | if (process.env.NODE_ENV !== "production") globalThis.prisma = prismadb
9 |
10 | export default prismadb;
11 |
--------------------------------------------------------------------------------
/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 |
3 | export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
4 | apiVersion: "2022-11-15",
5 | typescript: true,
6 | });
7 |
--------------------------------------------------------------------------------
/lib/subscription.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs";
2 |
3 | import prismadb from "@/lib/prismadb";
4 |
5 | const DAY_IN_MS = 86_400_000;
6 |
7 | export const checkSubscription = async () => {
8 | const { userId } = auth();
9 |
10 | if (!userId) {
11 | return false;
12 | }
13 |
14 | const userSubscription = await prismadb.userSubscription.findUnique({
15 | where: {
16 | userId: userId,
17 | },
18 | select: {
19 | stripeSubscriptionId: true,
20 | stripeCurrentPeriodEnd: true,
21 | stripeCustomerId: true,
22 | stripePriceId: true,
23 | },
24 | })
25 |
26 | if (!userSubscription) {
27 | return false;
28 | }
29 |
30 | const isValid =
31 | userSubscription.stripePriceId &&
32 | userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > Date.now()
33 |
34 | return !!isValid;
35 | };
36 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | export function absoluteUrl(path: string) {
9 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
10 | }
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | export default authMiddleware({
4 | publicRoutes: ["/", "/api/webhook"],
5 | });
6 |
7 | export const config = {
8 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
9 | };
10 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ["oaidalleapiprodscus.blob.core.windows.net"],
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "genius-saas",
3 | "version": "0.2.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^4.21.15",
14 | "@hookform/resolvers": "^3.1.1",
15 | "@prisma/client": "^5.0.0",
16 | "@radix-ui/react-avatar": "^1.0.3",
17 | "@radix-ui/react-dialog": "^1.0.4",
18 | "@radix-ui/react-dropdown-menu": "^2.0.5",
19 | "@radix-ui/react-label": "^2.0.2",
20 | "@radix-ui/react-progress": "^1.0.3",
21 | "@radix-ui/react-select": "^1.2.2",
22 | "@radix-ui/react-separator": "^1.0.3",
23 | "@radix-ui/react-slot": "^1.0.2",
24 | "@tanstack/react-query": "^4.32.0",
25 | "@types/node": "20.4.1",
26 | "@types/react": "18.2.14",
27 | "@types/react-dom": "18.2.6",
28 | "autoprefixer": "10.4.14",
29 | "axios": "^1.4.0",
30 | "class-variance-authority": "^0.6.1",
31 | "clsx": "^1.2.1",
32 | "cmdk": "^0.2.0",
33 | "crisp-sdk-web": "^1.0.19",
34 | "eslint": "8.44.0",
35 | "eslint-config-next": "13.4.9",
36 | "lucide-react": "^0.259.0",
37 | "next": "13.4.9",
38 | "next-themes": "^0.2.1",
39 | "openai": "^3.3.0",
40 | "postcss": "8.4.25",
41 | "react": "18.2.0",
42 | "react-dom": "18.2.0",
43 | "react-hook-form": "^7.45.1",
44 | "react-hot-toast": "^2.4.1",
45 | "react-markdown": "^8.0.7",
46 | "replicate": "^0.12.3",
47 | "stripe": "^12.13.0",
48 | "tailwind-merge": "^1.13.2",
49 | "tailwindcss": "3.3.2",
50 | "tailwindcss-animate": "^1.0.6",
51 | "typescript": "5.1.6",
52 | "typewriter-effect": "^2.20.1",
53 | "zod": "^3.21.4",
54 | "zustand": "^4.3.9"
55 | },
56 | "devDependencies": {
57 | "@tanstack/react-query-devtools": "^4.32.0",
58 | "dayjs": "^1.11.9",
59 | "prisma": "^5.0.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "mysql"
7 | url = env("DATABASE_URL")
8 | relationMode = "prisma"
9 | }
10 |
11 | model UserApiLimit {
12 | id String @id @default(cuid())
13 | userId String @unique
14 | count Int @default(0)
15 | createdAt DateTime @default(now())
16 | updatedAt DateTime @updatedAt
17 | }
18 |
19 | model UserSubscription {
20 | id String @id @default(cuid())
21 | userId String @unique
22 | stripeCustomerId String? @unique @map(name: "stripe_customer_id")
23 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
24 | stripePriceId String? @map(name: "stripe_price_id")
25 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
26 | Message Message[]
27 | }
28 |
29 | model Message {
30 | id String @id @default(cuid())
31 | userId String
32 | type String
33 | content String @db.Text
34 | role String @default("user")
35 | createdAt DateTime @default(now())
36 | updatedAt DateTime @updatedAt
37 | UserSubscription UserSubscription? @relation(fields: [userSubscriptionId], references: [id])
38 | userSubscriptionId String? @map(name: "user_subscription_id")
39 |
40 | @@index([userSubscriptionId])
41 | }
42 |
--------------------------------------------------------------------------------
/public/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devaaravmishra/genius-saas/e205eccd6cef0bc13939e15d1447ab492f220b0d/public/empty.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devaaravmishra/genius-saas/e205eccd6cef0bc13939e15d1447ab492f220b0d/public/logo.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------