├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── app
├── (auth)
│ ├── (routes)
│ │ ├── sign-in
│ │ │ └── [[...sing-in]]
│ │ │ │ └── page.tsx
│ │ └── sign-up
│ │ │ └── [[...sing-up]]
│ │ │ └── page.tsx
│ └── layout.tsx
├── (dashboard)
│ ├── (routes)
│ │ ├── code
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── conversation
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ ├── image
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── music
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ ├── settings
│ │ │ └── page.tsx
│ │ └── video
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ └── layout.tsx
├── (landing)
│ ├── layout.tsx
│ └── page.tsx
├── api
│ ├── code
│ │ └── route.ts
│ ├── conversation
│ │ └── route.ts
│ ├── image
│ │ └── route.ts
│ ├── music
│ │ └── route.ts
│ ├── stripe
│ │ └── route.ts
│ ├── video
│ │ └── route.ts
│ └── webhook
│ │ └── route.ts
├── favicon.ico
├── globals.css
└── layout.tsx
├── components.json
├── components
├── bot-avatar.tsx
├── crisp-chat.tsx
├── crisp-provider.tsx
├── empty.tsx
├── footer.tsx
├── free-counter.tsx
├── heading.tsx
├── landing-content.tsx
├── landing-hero.tsx
├── landing-navbar.tsx
├── loader.tsx
├── mobile-sidebar.tsx
├── modal-provider.tsx
├── navbar.tsx
├── pro-modal.tsx
├── sidebar.tsx
├── subscription-button.tsx
├── toaster-provider.tsx
├── ui
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── progress.tsx
│ ├── select.tsx
│ └── sheet.tsx
└── user-avatar.tsx
├── constants.ts
├── hooks
└── use-pro-modal.tsx
├── lib
├── api-limit.ts
├── prismadb.ts
├── stripe.ts
├── subscription.ts
└── utils.ts
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── empty.png
├── logo-old.png
├── logo.png
├── next.svg
└── vercel.svg
├── tailwind.config.js
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Sammy Leths
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 |
Witty AI SaaS
2 |
3 | Witty AI Software As A Service is an AI tool that can be used to create content 10x faster. This tool allows automatic generation of contents such as Images, Videos, Music and Code. It can also generate meaningful conversational responses via chat.
4 |
5 | Witty AI Software As A Service (SAAS) is a modern responsive, simple saas platform suitable for use by digital marketing agencies and individuals. With this project, you can easily generate marketing assets such as Images, Videos, Music, Q&A Conversations and Code Snippets. Some of the features built into this project include:
6 |
7 |
8 | Clerk User Authentication
9 | Social Login
10 | Image Generation Tool (Open AI)
11 | Video Generation Tool (Replicate AI)
12 | Conversation Generation Tool (Open AI)
13 | Music Generation Tool (Replicate AI)
14 | Code Generation Tool (Open AI)
15 | Stripe monthly subscription
16 | Free tier
17 | Interactive modals
18 |
19 |
20 | This project was developed using React, NextJS, TypeScript, Tailwind CSS, Prisma, MySql, Axios, NPM.
21 |
22 | Screenshots
23 |
24 | 
25 |
26 | Links
27 |
28 |
31 |
32 | Tech Stack
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Helpful Resources
50 |
51 |
52 |
53 | REACT : The library for web and native user interfaces.
54 |
55 |
56 | NEXTJS : The React Framework for the Web
57 |
58 |
59 | TYPESCRIPT : A strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
60 |
61 |
62 | TAILWIND CSS : A utility-first CSS framework packed with classes that can be composed to build any design, directly in your markup.
63 |
64 |
65 | PPRISMA : Next-generation Node.js and TypeScript ORM.
66 |
67 |
68 | OPENAI : AI-powered language model capable of generating human-like text based on context and past conversations.
69 |
70 |
71 | REPLICATE AI : run machine learning models with a few lines of code, without needing to understand how machine learning works.
72 |
73 |
74 | MYSQL : Open-source relational database management system.
75 |
76 | HTML5:
77 |
78 | MDN : Mozilla Developer Network - HTML (HyperText Markup Language)
79 | W3SCHOOL : HTML Introduction
80 |
81 |
82 | CSS3:
83 |
84 | MDN : Mozilla Developer Network - CSS (Cascading Style Sheets)
85 | W3SCHOOL : CSS Introduction
86 |
87 |
88 | JAVASCRIPT:
89 |
90 | MDN : Mozilla Developer Network - JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions
91 | W3SCHOOL : JavaScript Introduction
92 |
93 |
94 |
95 | AXIOS : A promise based HTTP client for the browser and node.js
96 |
97 |
98 | NPM : A package manager for the JavaScript programming language.
99 |
100 |
101 | MUGSHOTBOT : Automatic beautiful link previews
102 |
103 |
104 |
105 | Author's Links
106 |
107 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
121 |
122 | ## Getting Started
123 |
124 | First, run the development server:
125 |
126 | ```bash
127 | npm run dev
128 | # or
129 | yarn dev
130 | # or
131 | pnpm dev
132 | ```
133 |
134 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
135 |
136 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
137 |
138 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
139 |
140 | ## Learn More
141 |
142 | To learn more about Next.js, take a look at the following resources:
143 |
144 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
145 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
146 |
147 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
148 |
149 | ## Deploy on Vercel
150 |
151 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
152 |
153 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
154 |
--------------------------------------------------------------------------------
/app/(auth)/(routes)/sign-in/[[...sing-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from '@clerk/nextjs';
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(auth)/(routes)/sign-up/[[...sing-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from '@clerk/nextjs';
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | const AuthLayout = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 | {children}
4 | );
5 | };
6 |
7 | export default AuthLayout;
8 |
--------------------------------------------------------------------------------
/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 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { Code, Divide } from "lucide-react";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { useRouter } from "next/navigation";
7 | import { ChatCompletionRequestMessage } from "openai";
8 | import axios from "axios";
9 |
10 | import Heading from "@/components/heading";
11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
12 | import { Input } from "@/components/ui/input";
13 |
14 | import { formSchema } from "./constants";
15 | import { Button } from "@/components/ui/button";
16 | import { useState } from "react";
17 | import Empty from "@/components/empty";
18 | import Loader from "@/components/loader";
19 | import { cn } from "@/lib/utils";
20 | import UserAvatar from "@/components/user-avatar";
21 | import BotAvatar from "@/components/bot-avatar";
22 | import ReactMarkdown from "react-markdown";
23 | import { useProModal } from "@/hooks/use-pro-modal";
24 | import toast from "react-hot-toast";
25 |
26 | const CodePage = () => {
27 | const proModal = useProModal();
28 | const router = useRouter();
29 | const [messages, setMessages] = useState([]);
30 |
31 | const form = useForm>({
32 | resolver: zodResolver(formSchema),
33 | defaultValues: {
34 | prompt: "",
35 | },
36 | });
37 |
38 | const isLoading = form.formState.isSubmitting;
39 |
40 | const onSubmit = async (values: z.infer) => {
41 | try {
42 | const userMessage: ChatCompletionRequestMessage = {
43 | role: "user",
44 | content: values.prompt,
45 | };
46 | const newMessages = [...messages, userMessage];
47 |
48 | const response = await axios.post("/api/code", {
49 | messages: newMessages,
50 | });
51 |
52 | setMessages((current) => [...current, userMessage, response.data]);
53 | form.reset();
54 | } catch (error: any) {
55 | if (error?.response?.status === 403) {
56 | proModal.onOpen();
57 | } else {
58 | toast.error("Something went wrong");
59 | }
60 | } finally {
61 | router.refresh();
62 | }
63 | };
64 |
65 | return (
66 |
67 |
74 |
75 |
76 |
104 |
105 |
106 |
107 | {isLoading && (
108 |
109 |
110 |
111 | )}
112 | {messages.length === 0 && !isLoading && (
113 |
114 |
115 |
116 | )}
117 |
118 | {messages.map((message) => (
119 |
128 | {message.role === "user" ?
:
}
129 |
(
132 |
135 | ),
136 | code: ({ node, ...props }) => (
137 |
138 | ),
139 | }}
140 | className="text-sm overflow-hidden leading-7"
141 | >
142 | {message.content || ""}
143 |
144 |
145 | ))}
146 |
147 |
148 |
149 |
150 | );
151 | };
152 |
153 | export default CodePage;
154 |
--------------------------------------------------------------------------------
/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 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { MessageSquare } from "lucide-react";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { useRouter } from "next/navigation";
7 | import { ChatCompletionRequestMessage } from "openai";
8 | import axios from "axios";
9 |
10 | import Heading from "@/components/heading";
11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
12 | import { Input } from "@/components/ui/input";
13 |
14 | import { formSchema } from "./constants";
15 | import { Button } from "@/components/ui/button";
16 | import { useState } from "react";
17 | import Empty from "@/components/empty";
18 | import Loader from "@/components/loader";
19 | import { cn } from "@/lib/utils";
20 | import UserAvatar from "@/components/user-avatar";
21 | import BotAvatar from "@/components/bot-avatar";
22 | import { useProModal } from "@/hooks/use-pro-modal";
23 | import toast from "react-hot-toast";
24 |
25 | const ConversationPage = () => {
26 | const proModal = useProModal();
27 | const router = useRouter();
28 | const [messages, setMessages] = useState([]);
29 |
30 | const form = useForm>({
31 | resolver: zodResolver(formSchema),
32 | defaultValues: {
33 | prompt: "",
34 | },
35 | });
36 |
37 | const isLoading = form.formState.isSubmitting;
38 |
39 | const onSubmit = async (values: z.infer) => {
40 | try {
41 | const userMessage: ChatCompletionRequestMessage = {
42 | role: "user",
43 | content: values.prompt,
44 | };
45 | const newMessages = [...messages, userMessage];
46 |
47 | const response = await axios.post("/api/conversation", {
48 | messages: newMessages,
49 | });
50 |
51 | setMessages((current) => [...current, userMessage, response.data]);
52 | form.reset();
53 | } catch (error: any) {
54 | if (error?.response?.status === 403) {
55 | proModal.onOpen();
56 | } else {
57 | toast.error("Something went wrong");
58 | }
59 | } finally {
60 | router.refresh();
61 | }
62 | };
63 |
64 | return (
65 |
66 |
73 |
74 |
75 |
103 |
104 |
105 |
106 | {isLoading && (
107 |
108 |
109 |
110 | )}
111 | {messages.length === 0 && !isLoading && (
112 |
113 |
114 |
115 | )}
116 |
117 | {messages.map((message) => (
118 |
127 | {message.role === "user" ?
:
}
128 |
{message.content}
129 |
130 | ))}
131 |
132 |
133 |
134 |
135 | );
136 | };
137 |
138 | export default ConversationPage;
139 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter } from "next/navigation";
3 |
4 | import { Card } from "@/components/ui/card";
5 | import { cn } from "@/lib/utils";
6 | import {
7 | ArrowRight,
8 | Code,
9 | ImageIcon,
10 | MessageSquare,
11 | Music,
12 | VideoIcon,
13 | } from "lucide-react";
14 |
15 | const tools = [
16 | {
17 | label: "Conversation",
18 | icon: MessageSquare,
19 | color: "text-yellow-500",
20 | bgColor: "bg-yellow-500/10",
21 | href: "/conversation",
22 | },
23 | {
24 | label: "Music Generation",
25 | icon: Music,
26 | color: "text-fuchsia-500",
27 | bgColor: "bg-fuchsia-500/10",
28 | href: "/music",
29 | },
30 | {
31 | label: "Image Generation",
32 | icon: ImageIcon,
33 | color: "text-teal-700",
34 | bgColor: "bg-teal-700/10",
35 | href: "/image",
36 | },
37 | {
38 | label: "Video Generation",
39 | icon: VideoIcon,
40 | color: "text-lime-700",
41 | bgColor: "bg-lime-700/10",
42 | href: "/video",
43 | },
44 | {
45 | label: "Code Generation",
46 | icon: Code,
47 | color: "text-blue-700",
48 | bgColor: "bg-blue-700/10",
49 | href: "/code",
50 | },
51 | ];
52 |
53 | const DashboardPage = () => {
54 | const router = useRouter();
55 |
56 | return (
57 |
58 |
59 |
60 | Explore the power of AI
61 |
62 |
63 | Chat with the smartest AI - Experience the power of AI
64 |
65 |
66 |
67 | {tools.map((tool) => (
68 |
router.push(tool.href)}
70 | key={tool.href}
71 | className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer"
72 | >
73 |
74 |
75 |
76 |
77 |
{tool.label}
78 |
79 |
80 |
81 | ))}
82 |
83 |
84 | );
85 | };
86 |
87 | export default DashboardPage;
88 |
--------------------------------------------------------------------------------
/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: "Image Prompt is required",
6 | }),
7 | amount: z.string().min(1),
8 | resolution: z.string().min(1),
9 | });
10 |
11 | export const amountOptions = [
12 | {
13 | value: "1",
14 | label: "1 Photo",
15 | },
16 | {
17 | value: "2",
18 | label: "2 Photos",
19 | },
20 | {
21 | value: "3",
22 | label: "3 Photos",
23 | },
24 | {
25 | value: "4",
26 | label: "4 Photos",
27 | },
28 | {
29 | value: "5",
30 | label: "5 Photos",
31 | },
32 | ];
33 |
34 | export const resolutionOptions = [
35 | {
36 | value: "256x256",
37 | label: "256x256",
38 | },
39 | {
40 | value: "512x512",
41 | label: "512x512",
42 | },
43 | {
44 | value: "1024x1024",
45 | label: "1024x1024",
46 | },
47 | ];
48 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/image/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { Download, ImageIcon } from "lucide-react";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { useRouter } from "next/navigation";
7 |
8 | import axios from "axios";
9 |
10 | import Heading from "@/components/heading";
11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
12 | import { Input } from "@/components/ui/input";
13 |
14 | import { amountOptions, formSchema, resolutionOptions } from "./constants";
15 | import { Button } from "@/components/ui/button";
16 | import { useState } from "react";
17 | import Empty from "@/components/empty";
18 | import Loader from "@/components/loader";
19 | import { cn } from "@/lib/utils";
20 |
21 | import {
22 | Select,
23 | SelectContent,
24 | SelectItem,
25 | SelectTrigger,
26 | SelectValue,
27 | } from "@/components/ui/select";
28 | import { Card, CardFooter } from "@/components/ui/card";
29 | import Image from "next/image";
30 | import { useProModal } from "@/hooks/use-pro-modal";
31 | import toast from "react-hot-toast";
32 |
33 | const ImagePage = () => {
34 | const proModal = useProModal();
35 | const router = useRouter();
36 | const [images, setImages] = useState([]);
37 |
38 | const form = useForm>({
39 | resolver: zodResolver(formSchema),
40 | defaultValues: {
41 | prompt: "",
42 | amount: "1",
43 | resolution: "512x512",
44 | },
45 | });
46 |
47 | const isLoading = form.formState.isSubmitting;
48 |
49 | const onSubmit = async (values: z.infer) => {
50 | try {
51 | setImages([]);
52 |
53 | const response = await axios.post("/api/image", values);
54 |
55 | const urls = response.data.map((image: { url: string }) => image.url);
56 |
57 | setImages(urls);
58 | form.reset();
59 | } catch (error: any) {
60 | if (error?.response?.status === 403) {
61 | proModal.onOpen();
62 | } else {
63 | toast.error("Something went wrong");
64 | }
65 | } finally {
66 | router.refresh();
67 | }
68 | };
69 |
70 | return (
71 |
72 |
79 |
80 |
81 |
163 |
164 |
165 |
166 | {isLoading && (
167 |
168 |
169 |
170 | )}
171 | {images.length === 0 && !isLoading && (
172 |
173 |
174 |
175 | )}
176 |
177 | {images.map((src) => (
178 |
179 |
180 |
181 |
182 |
183 | window.open(src)}
185 | variant="secondary"
186 | className="w-full"
187 | >
188 |
189 | Download
190 |
191 |
192 |
193 | ))}
194 |
195 |
196 |
197 |
198 | );
199 | };
200 |
201 | export default ImagePage;
202 |
--------------------------------------------------------------------------------
/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 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { Music } from "lucide-react";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { useRouter } from "next/navigation";
7 | import { ChatCompletionRequestMessage } from "openai";
8 | import axios from "axios";
9 |
10 | import Heading from "@/components/heading";
11 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
12 | import { Input } from "@/components/ui/input";
13 |
14 | import { formSchema } from "./constants";
15 | import { Button } from "@/components/ui/button";
16 | import { useState } from "react";
17 | import Empty from "@/components/empty";
18 | import Loader from "@/components/loader";
19 | import { cn } from "@/lib/utils";
20 | import UserAvatar from "@/components/user-avatar";
21 | import BotAvatar from "@/components/bot-avatar";
22 | import { useProModal } from "@/hooks/use-pro-modal";
23 | import toast from "react-hot-toast";
24 |
25 | const MusicPage = () => {
26 | const proModal = useProModal();
27 | const router = useRouter();
28 | const [music, setMusic] = useState();
29 |
30 | const form = useForm>({
31 | resolver: zodResolver(formSchema),
32 | defaultValues: {
33 | prompt: "",
34 | },
35 | });
36 |
37 | const isLoading = form.formState.isSubmitting;
38 |
39 | const onSubmit = async (values: z.infer) => {
40 | try {
41 | setMusic(undefined);
42 |
43 | const response = await axios.post("/api/music", values);
44 |
45 | setMusic(response.data.audio);
46 | form.reset();
47 | } catch (error: any) {
48 | if (error?.response?.status === 403) {
49 | proModal.onOpen();
50 | } else {
51 | toast.error("Something went wrong");
52 | }
53 | } finally {
54 | router.refresh();
55 | }
56 | };
57 |
58 | return (
59 |
60 |
67 |
68 |
69 |
97 |
98 |
99 |
100 | {isLoading && (
101 |
102 |
103 |
104 | )}
105 | {!music && !isLoading && (
106 |
107 |
108 |
109 | )}
110 | {music && (
111 |
112 |
113 |
114 | )}
115 |
116 |
117 |
118 | );
119 | };
120 |
121 | export default MusicPage;
122 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { Settings } from "lucide-react";
2 |
3 | import Heading from "@/components/heading";
4 | import { checkSubscription } from "@/lib/subscription";
5 | import { SubscriptionButton } from "@/components/subscription-button";
6 |
7 | const SettingsPage = async () => {
8 | const isPro = await checkSubscription();
9 |
10 | return (
11 |
12 |
19 |
20 |
21 | {isPro
22 | ? "You are currently on a pro plan."
23 | : "You are currently on a free plan."}
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default SettingsPage;
32 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/video/constants.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const formSchema = z.object({
4 | prompt: z.string().min(1, {
5 | message: "Video Prompt is required",
6 | }),
7 | });
8 |
--------------------------------------------------------------------------------
/app/(dashboard)/(routes)/video/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { VideoIcon } from "lucide-react";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { useRouter } from "next/navigation";
7 | import axios from "axios";
8 |
9 | import Heading from "@/components/heading";
10 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
11 | import { Input } from "@/components/ui/input";
12 |
13 | import { formSchema } from "./constants";
14 | import { Button } from "@/components/ui/button";
15 | import { useState } from "react";
16 | import Empty from "@/components/empty";
17 | import Loader from "@/components/loader";
18 | import { cn } from "@/lib/utils";
19 | import UserAvatar from "@/components/user-avatar";
20 | import BotAvatar from "@/components/bot-avatar";
21 | import { useProModal } from "@/hooks/use-pro-modal";
22 | import toast from "react-hot-toast";
23 |
24 | const VideoPage = () => {
25 | const proModal = useProModal();
26 | const router = useRouter();
27 | const [video, setVideo] = useState();
28 |
29 | const form = useForm>({
30 | resolver: zodResolver(formSchema),
31 | defaultValues: {
32 | prompt: "",
33 | },
34 | });
35 |
36 | const isLoading = form.formState.isSubmitting;
37 |
38 | const onSubmit = async (values: z.infer) => {
39 | try {
40 | setVideo(undefined);
41 |
42 | const response = await axios.post("/api/video", values);
43 |
44 | setVideo(response.data[0]);
45 | form.reset();
46 | } catch (error: any) {
47 | if (error?.response?.status === 403) {
48 | proModal.onOpen();
49 | } else {
50 | toast.error("Something went wrong");
51 | }
52 | } finally {
53 | router.refresh();
54 | }
55 | };
56 |
57 | return (
58 |
59 |
66 |
67 |
68 |
96 |
97 |
98 |
99 | {isLoading && (
100 |
101 |
102 |
103 | )}
104 | {!video && !isLoading && (
105 |
106 |
107 |
108 | )}
109 | {video && (
110 |
114 |
115 |
116 | )}
117 |
118 |
119 |
120 | );
121 | };
122 |
123 | export default VideoPage;
124 |
--------------------------------------------------------------------------------
/app/(dashboard)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@/components/footer";
2 | import Navbar from "@/components/navbar";
3 | import Sidebar from "@/components/sidebar";
4 | import { getApiLimitCount } from "@/lib/api-limit";
5 | import { checkSubscription } from "@/lib/subscription";
6 |
7 | const DashboardLayout = async ({ children }: { children: React.ReactNode }) => {
8 | const apiLimitCount = await getApiLimitCount();
9 | const isPro = await checkSubscription();
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | {children}
18 |
19 |
20 | );
21 | };
22 |
23 | export default DashboardLayout;
24 |
--------------------------------------------------------------------------------
/app/(landing)/layout.tsx:
--------------------------------------------------------------------------------
1 | const LandingLayout = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 | {children}
5 |
6 | );
7 | };
8 |
9 | export default LandingLayout;
10 |
--------------------------------------------------------------------------------
/app/(landing)/page.tsx:
--------------------------------------------------------------------------------
1 | import LandingContent from "@/components/landing-content";
2 | import LandingHero from "@/components/landing-hero";
3 | import { LandingNavbar } from "@/components/landing-navbar";
4 | import Link from "next/link";
5 |
6 | const credit = " ";
7 |
8 | const LandingPage = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | A sandbox project by
16 |
21 | {credit}
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default LandingPage;
29 |
--------------------------------------------------------------------------------
/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 { increaseApiLimit, checkApiLimit } from "@/lib/api-limit";
6 | import { checkSubscription } from "@/lib/subscription";
7 |
8 | const configuration = new Configuration({
9 | apiKey: process.env.OPENAI_API_KEY,
10 | });
11 |
12 | const openai = new OpenAIApi(configuration);
13 |
14 | const instructionMessage: ChatCompletionRequestMessage = {
15 | role: "system",
16 | content:
17 | "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.",
18 | };
19 |
20 | export async function POST(req: Request) {
21 | try {
22 | const { userId } = auth();
23 | const body = await req.json();
24 | const { messages } = body;
25 |
26 | if (!userId) {
27 | return new NextResponse("Unauthorized", { status: 401 });
28 | }
29 |
30 | if (!configuration.apiKey) {
31 | return new NextResponse("OpenAI API Key not configured", { status: 500 });
32 | }
33 |
34 | if (!messages) {
35 | return new NextResponse("Messages are required", { status: 400 });
36 | }
37 |
38 | const freeTrial = await checkApiLimit();
39 | const isPro = await checkSubscription();
40 |
41 | if (!freeTrial && !isPro) {
42 | return new NextResponse("Free trial has expired.", { status: 403 });
43 | }
44 |
45 | const response = await openai.createChatCompletion({
46 | model: "gpt-3.5-turbo",
47 | messages: [instructionMessage, ...messages],
48 | });
49 |
50 | if (!isPro) {
51 | await increaseApiLimit();
52 | }
53 |
54 | return NextResponse.json(response.data.choices[0].message);
55 | } catch (error) {
56 | console.log("[CODE_ERROR]", error);
57 | return new NextResponse("Internal error", { status: 500 });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/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 { increaseApiLimit, checkApiLimit } from "@/lib/api-limit";
6 | import { checkSubscription } from "@/lib/subscription";
7 |
8 | const configuration = new Configuration({
9 | apiKey: process.env.OPENAI_API_KEY,
10 | });
11 |
12 | const openai = new OpenAIApi(configuration);
13 |
14 | export async function POST(req: Request) {
15 | try {
16 | const { userId } = auth();
17 | const body = await req.json();
18 | const { messages } = body;
19 |
20 | if (!userId) {
21 | return new NextResponse("Unauthorized", { status: 401 });
22 | }
23 |
24 | if (!configuration.apiKey) {
25 | return new NextResponse("OpenAI API Key not configured", { status: 500 });
26 | }
27 |
28 | if (!messages) {
29 | return new NextResponse("Messages are required", { status: 400 });
30 | }
31 |
32 | const freeTrial = await checkApiLimit();
33 | const isPro = await checkSubscription();
34 |
35 | if (!freeTrial && !isPro) {
36 | return new NextResponse("Free trial has expired.", { status: 403 });
37 | }
38 |
39 | const response = await openai.createChatCompletion({
40 | model: "gpt-3.5-turbo",
41 | messages,
42 | });
43 |
44 | if (!isPro) {
45 | await increaseApiLimit();
46 | }
47 |
48 | return NextResponse.json(response.data.choices[0].message);
49 | } catch (error) {
50 | console.log("[CONVERSATION_ERROR]", error);
51 | return new NextResponse("Internal error", { status: 500 });
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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 { increaseApiLimit, checkApiLimit } from "@/lib/api-limit";
6 | import { checkSubscription } from "@/lib/subscription";
7 |
8 | const configuration = new Configuration({
9 | apiKey: process.env.OPENAI_API_KEY,
10 | });
11 |
12 | const openai = new OpenAIApi(configuration);
13 |
14 | export async function POST(req: Request) {
15 | try {
16 | const { userId } = auth();
17 | const body = await req.json();
18 | const { prompt, amount = 1, resolution = "512x512" } = body;
19 |
20 | if (!userId) {
21 | return new NextResponse("Unauthorized", { status: 401 });
22 | }
23 |
24 | if (!configuration.apiKey) {
25 | return new NextResponse("OpenAI API Key not configured", { status: 500 });
26 | }
27 |
28 | if (!prompt) {
29 | return new NextResponse("Prompt is required", { status: 400 });
30 | }
31 |
32 | if (!amount) {
33 | return new NextResponse("Amount is required", { status: 400 });
34 | }
35 |
36 | if (!resolution) {
37 | return new NextResponse("Resolution is required", { status: 400 });
38 | }
39 |
40 | const freeTrial = await checkApiLimit();
41 | const isPro = await checkSubscription();
42 |
43 | if (!freeTrial && !isPro) {
44 | return new NextResponse("Free trial has expired.", { status: 403 });
45 | }
46 |
47 | const response = await openai.createImage({
48 | prompt,
49 | n: parseInt(amount, 10),
50 | size: resolution,
51 | });
52 |
53 | if (!isPro) {
54 | await increaseApiLimit();
55 | }
56 |
57 | return NextResponse.json(response.data.data);
58 | } catch (error) {
59 | console.log("[IMAGEERROR]", error);
60 | return new NextResponse("Internal error", { status: 500 });
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/api/music/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 | import Replicate from "replicate";
4 |
5 | import { increaseApiLimit, 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(req: Request) {
13 | try {
14 | const { userId } = auth();
15 | const body = await req.json();
16 | const { prompt } = body;
17 |
18 | if (!userId) {
19 | return new NextResponse("Unauthorized", { status: 401 });
20 | }
21 |
22 | if (!prompt) {
23 | return new NextResponse("OpenAI API Key not configured", { status: 500 });
24 | }
25 |
26 | const freeTrial = await checkApiLimit();
27 | const isPro = await checkSubscription();
28 |
29 | if (!freeTrial && !isPro) {
30 | return new NextResponse("Free trial has expired.", { status: 403 });
31 | }
32 |
33 | const response = await replicate.run(
34 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05",
35 | {
36 | input: {
37 | prompt_a: prompt,
38 | },
39 | }
40 | );
41 |
42 | if (!isPro) {
43 | await increaseApiLimit();
44 | }
45 |
46 | return NextResponse.json(response);
47 | } catch (error) {
48 | console.log("[MUSIC_ERROR]", error);
49 | return new NextResponse("Internal error", { status: 500 });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/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: "Witty AI 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 { auth } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 | import Replicate from "replicate";
4 |
5 | import { increaseApiLimit, 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(req: Request) {
13 | try {
14 | const { userId } = auth();
15 | const body = await req.json();
16 | const { prompt } = body;
17 |
18 | if (!userId) {
19 | return new NextResponse("Unauthorized", { status: 401 });
20 | }
21 |
22 | if (!prompt) {
23 | return new NextResponse("OpenAI API Key not configured", { status: 500 });
24 | }
25 |
26 | const freeTrial = await checkApiLimit();
27 | const isPro = await checkSubscription();
28 |
29 | if (!freeTrial && !isPro) {
30 | return new NextResponse("Free trial has expired.", { status: 403 });
31 | }
32 |
33 | const response = await replicate.run(
34 | "anotherjesse/zeroscope-v2-xl:9f747673945c62801b13b84701c783929c0ee784e4748ec062204894dda1a351",
35 | {
36 | input: {
37 | prompt: prompt,
38 | },
39 | }
40 | );
41 |
42 | if (!isPro) {
43 | await increaseApiLimit();
44 | }
45 |
46 | return NextResponse.json(response);
47 | } catch (error) {
48 | console.log("[VIDEO_ERROR]", error);
49 | return new NextResponse("Internal error", { status: 500 });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import { headers } from "next/headers";
3 | import { NextResponse } from "next/server";
4 |
5 | import prismadb from "@/lib/prismadb";
6 | import { stripe } from "@/lib/stripe";
7 |
8 | export async function POST(req: Request) {
9 | const body = await req.text();
10 | const signature = headers().get("Stripe-Signature") as string;
11 |
12 | let event: Stripe.Event;
13 |
14 | try {
15 | event = stripe.webhooks.constructEvent(
16 | body,
17 | signature,
18 | process.env.STRIPE_WEBHOOK_SECRET!
19 | );
20 | } catch (error: any) {
21 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
22 | }
23 |
24 | const session = event.data.object as Stripe.Checkout.Session;
25 |
26 | if (event.type === "checkout.session.completed") {
27 | const subscription = await stripe.subscriptions.retrieve(
28 | session.subscription as string
29 | );
30 |
31 | if (!session?.metadata?.userId) {
32 | return new NextResponse("User id is required", { status: 400 });
33 | }
34 |
35 | await prismadb.userSubscription.create({
36 | data: {
37 | userId: session?.metadata?.userId,
38 | stripeSubscriptionId: subscription.id,
39 | stripeCustomerId: subscription.customer as string,
40 | stripePriceId: subscription.items.data[0].price.id,
41 | stripeCurrentPeriodEnd: new Date(
42 | subscription.current_period_end * 1000
43 | ),
44 | },
45 | });
46 | }
47 |
48 | if (event.type === "invoice.payment_succeeded") {
49 | const subscription = await stripe.subscriptions.retrieve(
50 | session.subscription as string
51 | );
52 |
53 | await prismadb.userSubscription.update({
54 | where: {
55 | stripeSubscriptionId: subscription.id,
56 | },
57 | data: {
58 | stripePriceId: subscription.items.data[0].price.id,
59 | stripeCurrentPeriodEnd: new Date(
60 | subscription.current_period_end * 1000
61 | ),
62 | },
63 | });
64 | }
65 |
66 | return new NextResponse(null, { status: 200 });
67 | }
68 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SammyLeths/witty-ai-saas/9be9c563cddef98b92600c4e70ece8bc366ca6c7/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 222.2 84% 4.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 84% 4.9%;
18 |
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 222.2 84% 4.9%;
21 |
22 | --primary: 248 90% 66%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --muted: 210 40% 96.1%;
29 | --muted-foreground: 215.4 16.3% 46.9%;
30 |
31 | --accent: 210 40% 96.1%;
32 | --accent-foreground: 222.2 47.4% 11.2%;
33 |
34 | --destructive: 0 84.2% 60.2%;
35 | --destructive-foreground: 210 40% 98%;
36 |
37 | --border: 214.3 31.8% 91.4%;
38 | --input: 214.3 31.8% 91.4%;
39 | --ring: 222.2 84% 4.9%;
40 |
41 | --radius: 0.5rem;
42 | }
43 |
44 | .dark {
45 | --background: 222.2 84% 4.9%;
46 | --foreground: 210 40% 98%;
47 |
48 | --card: 222.2 84% 4.9%;
49 | --card-foreground: 210 40% 98%;
50 |
51 | --popover: 222.2 84% 4.9%;
52 | --popover-foreground: 210 40% 98%;
53 |
54 | --primary: 210 40% 98%;
55 | --primary-foreground: 222.2 47.4% 11.2%;
56 |
57 | --secondary: 217.2 32.6% 17.5%;
58 | --secondary-foreground: 210 40% 98%;
59 |
60 | --muted: 217.2 32.6% 17.5%;
61 | --muted-foreground: 215 20.2% 65.1%;
62 |
63 | --accent: 217.2 32.6% 17.5%;
64 | --accent-foreground: 210 40% 98%;
65 |
66 | --destructive: 0 62.8% 30.6%;
67 | --destructive-foreground: 210 40% 98%;
68 |
69 | --border: 217.2 32.6% 17.5%;
70 | --input: 217.2 32.6% 17.5%;
71 | --ring: hsl(212.7, 26.8%, 83.9);
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 | import { ClerkProvider } from "@clerk/nextjs";
5 | import { ModalProvider } from "@/components/modal-provider";
6 | import ToasterProvider from "@/components/toaster-provider";
7 | import { CrispProvider } from "@/components/crisp-provider";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | export const metadata: Metadata = {
12 | title: "Witty AI Saas",
13 | description: "Witty AI Platform",
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: {
19 | children: React.ReactNode;
20 | }) {
21 | return (
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 "./ui/avatar";
2 |
3 | const BotAvatar = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default BotAvatar;
12 |
--------------------------------------------------------------------------------
/components/crisp-chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { Crisp } from "crisp-sdk-web";
5 |
6 | export const CrispChat = () => {
7 | useEffect(() => {
8 | Crisp.configure("e16f7ad2-4eac-4502-a226-c9c2aaac8d09");
9 | }, []);
10 |
11 | return null;
12 | };
13 |
--------------------------------------------------------------------------------
/components/crisp-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CrispChat } from "./crisp-chat";
4 |
5 | export const CrispProvider = () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/components/empty.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | interface EmptyProps {
4 | label: string;
5 | }
6 |
7 | const Empty = ({ label }: EmptyProps) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
{label}
14 |
15 | );
16 | };
17 |
18 | export default Empty;
19 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | const credit = " ";
4 |
5 | const Footer = () => {
6 | return (
7 |
8 | A sandbox project by
9 |
14 | {credit}
15 |
16 |
17 | );
18 | };
19 |
20 | export default Footer;
21 |
--------------------------------------------------------------------------------
/components/free-counter.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Card, CardContent } from "@/components/ui/card";
3 | import { MAX_FREE_COUNTS } from "@/constants";
4 | import { Progress } from "@/components/ui/progress";
5 | import { Button } from "@/components/ui/button";
6 | import { Zap } from "lucide-react";
7 | import { useProModal } from "@/hooks/use-pro-modal";
8 |
9 | interface FreeCounterProps {
10 | apiLimitCount: number;
11 | isPro: boolean;
12 | }
13 |
14 | export const FreeCounter = ({
15 | apiLimitCount = 0,
16 | isPro = false,
17 | }: FreeCounterProps) => {
18 | const proModal = useProModal();
19 | const [mounted, setMounted] = useState(false);
20 |
21 | useEffect(() => {
22 | setMounted(true);
23 | }, []);
24 |
25 | if (!mounted) {
26 | return null;
27 | }
28 |
29 | if (isPro) {
30 | return null;
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {apiLimitCount} / {MAX_FREE_COUNTS} Free Generations
40 |
41 |
45 |
46 |
51 | Upgrade
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/components/heading.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { LucideIcon } from "lucide-react";
3 |
4 | interface HeadingProps {
5 | title: string;
6 | description: string;
7 | icon: LucideIcon;
8 | iconColor?: string;
9 | bgColor?: string;
10 | }
11 |
12 | const Heading = ({
13 | title,
14 | description,
15 | icon: Icon,
16 | iconColor,
17 | bgColor,
18 | }: HeadingProps) => {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
{title}
26 |
{description}
27 |
28 |
29 | );
30 | };
31 |
32 | export default Heading;
33 |
--------------------------------------------------------------------------------
/components/landing-content.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
4 |
5 | const testimonials = [
6 | {
7 | name: "Stanley",
8 | avatar: "S",
9 | title: "Content Writer",
10 | description: "I absolutely love this seamless application!",
11 | },
12 | {
13 | name: "Morgan",
14 | avatar: "M",
15 | title: "Digital Marketer",
16 | description: "Content creation for marketing is a breeze!",
17 | },
18 | {
19 | name: "Bridget",
20 | avatar: "B",
21 | title: "Data Analyst",
22 | description: "My research papers are now created in half the time",
23 | },
24 | {
25 | name: "Svelte",
26 | avatar: "S",
27 | title: "Sales Manager",
28 | description: "I am now boosting sales like never before with AI",
29 | },
30 | ];
31 |
32 | const LandingContent = () => {
33 | return (
34 |
35 |
36 | Testimonials
37 |
38 |
39 | {testimonials.map((item) => (
40 |
44 |
45 |
46 |
47 |
{item.name}
48 |
{item.title}
49 |
50 |
51 |
52 | {item.description}
53 |
54 |
55 |
56 | ))}
57 |
58 |
59 | );
60 | };
61 |
62 | export default LandingContent;
63 |
--------------------------------------------------------------------------------
/components/landing-hero.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useAuth } from "@clerk/nextjs";
4 | import Link from "next/link";
5 | import TypewriterComponent from "typewriter-effect";
6 | import { Button } from "./ui/button";
7 |
8 | const LandingHero = () => {
9 | const { isSignedIn } = useAuth();
10 |
11 | return (
12 |
13 |
14 |
The Best AI Tool for
15 |
16 |
29 |
30 |
31 |
32 | Create content using AI 10x faster.
33 |
34 |
35 |
36 |
40 | Start Generating For Free
41 |
42 |
43 |
44 |
45 | No credit card required.
46 |
47 |
48 | );
49 | };
50 |
51 | export default LandingHero;
52 |
--------------------------------------------------------------------------------
/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({
12 | weight: "600",
13 | subsets: ["latin"],
14 | });
15 |
16 | export const LandingNavbar = () => {
17 | const { isSignedIn } = useAuth();
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 | Witty AI Saas
27 |
28 |
29 |
30 |
31 |
32 | Get Started
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | const Loader = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
Witty AI is thinking...
10 |
11 | );
12 | };
13 |
14 | export default Loader;
15 |
--------------------------------------------------------------------------------
/components/mobile-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Menu } from "lucide-react";
5 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
6 | import Sidebar from "@/components/sidebar";
7 | import { useEffect, useState } from "react";
8 |
9 | interface MobileSidebarProps {
10 | apiLimitCount: number;
11 | isPro: boolean;
12 | }
13 |
14 | const MobileSidebar = ({
15 | apiLimitCount = 0,
16 | isPro = false,
17 | }: MobileSidebarProps) => {
18 | const [isMounted, setIsMounted] = useState(false);
19 |
20 | useEffect(() => {
21 | setIsMounted(true);
22 | }, []);
23 |
24 | if (!isMounted) {
25 | return null;
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default MobileSidebar;
43 |
--------------------------------------------------------------------------------
/components/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import ProModal from "@/components/pro-modal";
5 |
6 | export const ModalProvider = () => {
7 | const [isMounted, setIsMounted] = useState(false);
8 |
9 | useEffect(() => {
10 | setIsMounted(true);
11 | }, []);
12 |
13 | if (!isMounted) {
14 | return null;
15 | }
16 |
17 | return (
18 | <>
19 |
20 | >
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { UserButton } from "@clerk/nextjs";
2 | import MobileSidebar from "@/components/mobile-sidebar";
3 | import { getApiLimitCount } from "@/lib/api-limit";
4 | import { checkSubscription } from "@/lib/subscription";
5 |
6 | const Navbar = async () => {
7 | const apiLimitCount = await getApiLimitCount();
8 | const isPro = await checkSubscription();
9 |
10 | return (
11 |
17 | );
18 | };
19 |
20 | export default Navbar;
21 |
--------------------------------------------------------------------------------
/components/pro-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | } from "@/components/ui/dialog";
11 | import { useProModal } from "@/hooks/use-pro-modal";
12 | import { Badge } from "@/components/ui/badge";
13 | import {
14 | Check,
15 | Code,
16 | ImageIcon,
17 | MessageSquare,
18 | Music,
19 | VideoIcon,
20 | Zap,
21 | } from "lucide-react";
22 | import { Card } from "./ui/card";
23 | import { cn } from "@/lib/utils";
24 | import { Button } from "./ui/button";
25 | import axios from "axios";
26 | import { useState } from "react";
27 | import toast from "react-hot-toast";
28 |
29 | const tools = [
30 | {
31 | label: "Conversation",
32 | icon: MessageSquare,
33 | color: "text-yellow-500",
34 | bgColor: "bg-yellow-500/10",
35 | },
36 | {
37 | label: "Music Generation",
38 | icon: Music,
39 | color: "text-fuchsia-500",
40 | bgColor: "bg-fuchsia-500/10",
41 | },
42 | {
43 | label: "Image Generation",
44 | icon: ImageIcon,
45 | color: "text-teal-700",
46 | bgColor: "bg-teal-700/10",
47 | },
48 | {
49 | label: "Video Generation",
50 | icon: VideoIcon,
51 | color: "text-lime-700",
52 | bgColor: "bg-lime-700/10",
53 | },
54 | {
55 | label: "Code Generation",
56 | icon: Code,
57 | color: "text-blue-700",
58 | bgColor: "bg-blue-700/10",
59 | },
60 | ];
61 |
62 | const ProModal = () => {
63 | const proModal = useProModal();
64 | const [loading, setLoading] = useState(false);
65 |
66 | const onSubscribe = async () => {
67 | try {
68 | setLoading(true);
69 | const response = await axios.get("/api/stripe");
70 |
71 | window.location.href = response.data.url;
72 | } catch (error) {
73 | toast.error("Something went wrong");
74 | } finally {
75 | setLoading(false);
76 | }
77 | };
78 |
79 | return (
80 |
81 |
82 |
83 |
84 |
85 | Upgrade to Witty AI Pro
86 |
87 | Pro
88 |
89 |
90 |
91 |
92 | {tools.map((tool) => (
93 |
97 |
98 |
99 |
100 |
101 |
{tool.label}
102 |
103 |
104 |
105 | ))}
106 |
107 |
108 |
109 |
116 | Upgrade
117 |
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default ProModal;
125 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import Image from "next/image";
5 | import { usePathname } from "next/navigation";
6 | import { Montserrat } from "next/font/google";
7 | import { cn } from "@/lib/utils";
8 | import {
9 | Code,
10 | ImageIcon,
11 | LayoutDashboard,
12 | MessageSquare,
13 | Music,
14 | Settings,
15 | VideoIcon,
16 | } from "lucide-react";
17 | import { FreeCounter } from "@/components/free-counter";
18 |
19 | const montserrat = Montserrat({ weight: "600", subsets: ["latin"] });
20 |
21 | const routes = [
22 | {
23 | label: "Dashboard",
24 | icon: LayoutDashboard,
25 | href: "/dashboard",
26 | color: "text-blue-500",
27 | },
28 | {
29 | label: "Conversation",
30 | icon: MessageSquare,
31 | href: "/conversation",
32 | color: "text-yellow-500",
33 | },
34 | {
35 | label: "Image Generation",
36 | icon: ImageIcon,
37 | href: "/image",
38 | color: "text-teal-700",
39 | },
40 | {
41 | label: "Video Generation",
42 | icon: VideoIcon,
43 | href: "/video",
44 | color: "text-lime-700",
45 | },
46 | {
47 | label: "Music Generation",
48 | icon: Music,
49 | href: "/music",
50 | color: "text-fuchsia-500",
51 | },
52 | {
53 | label: "Code Generation",
54 | icon: Code,
55 | href: "/code",
56 | color: "text-blue-700",
57 | },
58 | {
59 | label: "Settings",
60 | icon: Settings,
61 | href: "/settings",
62 | },
63 | ];
64 |
65 | interface SidebarProps {
66 | apiLimitCount: number;
67 | isPro: boolean;
68 | }
69 |
70 | const Sidebar = ({ apiLimitCount = 0, isPro = false }: SidebarProps) => {
71 | const pathname = usePathname();
72 |
73 | return (
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | Witty AI
82 |
83 |
84 |
85 | {routes.map((route) => (
86 |
96 |
97 |
98 | {route.label}
99 |
100 |
101 | ))}
102 |
103 |
104 |
105 |
106 | );
107 | };
108 |
109 | export default Sidebar;
110 |
--------------------------------------------------------------------------------
/components/subscription-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Zap } from "lucide-react";
4 | import { Button } from "./ui/button";
5 | import axios from "axios";
6 | import { useState } from "react";
7 | import toast from "react-hot-toast";
8 |
9 | interface SubscriptionButtonProps {
10 | isPro: boolean;
11 | }
12 |
13 | export const SubscriptionButton = ({
14 | isPro = false,
15 | }: SubscriptionButtonProps) => {
16 | const [loading, setLoading] = useState(false);
17 |
18 | const onClick = async () => {
19 | try {
20 | setLoading(true);
21 | const response = await axios.get("/api/stripe");
22 | window.location.href = response.data.url;
23 | } catch (error) {
24 | toast.error("Something went wrong");
25 | } finally {
26 | setLoading(false);
27 | }
28 | };
29 |
30 | return (
31 |
32 | {isPro ? "Manage Subscription" : "Upgrade"}
33 | {!isPro && }
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/components/toaster-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Toaster } from "react-hot-toast";
4 |
5 | const ToasterProvider = () => {
6 | return ;
7 | };
8 |
9 | export default ToasterProvider;
10 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | premium:
19 | "bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500 text-primary-foreground border-0",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | );
27 |
28 | export interface BadgeProps
29 | extends React.HTMLAttributes,
30 | VariantProps {}
31 |
32 | function Badge({ className, variant, ...props }: BadgeProps) {
33 | return (
34 |
35 | );
36 | }
37 |
38 | export { Badge, badgeVariants };
39 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-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 | generate: "bg-blue-600 text-white hover:bg-blue-600/70",
14 | destructive:
15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | premium:
23 | "bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500 text-white border-0",
24 | },
25 | size: {
26 | default: "h-10 px-4 py-2",
27 | sm: "h-9 rounded-md px-3",
28 | lg: "h-11 rounded-md px-8",
29 | icon: "h-10 w-10",
30 | },
31 | },
32 | defaultVariants: {
33 | variant: "default",
34 | size: "default",
35 | },
36 | }
37 | );
38 |
39 | export interface ButtonProps
40 | extends React.ButtonHTMLAttributes,
41 | VariantProps {
42 | asChild?: boolean;
43 | }
44 |
45 | const Button = React.forwardRef(
46 | ({ className, variant, size, asChild = false, ...props }, ref) => {
47 | const Comp = asChild ? Slot : "button";
48 | return (
49 |
54 | );
55 | }
56 | );
57 | Button.displayName = "Button";
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | ...props
16 | }: DialogPrimitive.DialogPortalProps) => (
17 |
18 | )
19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
20 |
21 | const DialogOverlay = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
33 | ))
34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
35 |
36 | const DialogContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, children, ...props }, ref) => (
40 |
41 |
42 |
50 | {children}
51 |
52 |
53 | Close
54 |
55 |
56 |
57 | ))
58 | DialogContent.displayName = DialogPrimitive.Content.displayName
59 |
60 | const DialogHeader = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | DialogHeader.displayName = "DialogHeader"
73 |
74 | const DialogFooter = ({
75 | className,
76 | ...props
77 | }: React.HTMLAttributes) => (
78 |
85 | )
86 | DialogFooter.displayName = "DialogFooter"
87 |
88 | const DialogTitle = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
100 | ))
101 | DialogTitle.displayName = DialogPrimitive.Title.displayName
102 |
103 | const DialogDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | DialogDescription.displayName = DialogPrimitive.Description.displayName
114 |
115 | export {
116 | Dialog,
117 | DialogTrigger,
118 | DialogContent,
119 | DialogHeader,
120 | DialogFooter,
121 | DialogTitle,
122 | DialogDescription,
123 | }
124 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
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/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 | import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
3 |
4 | const UserAvatar = () => {
5 | const { user } = useUser();
6 | return (
7 |
8 |
9 |
10 | {user?.firstName?.charAt(0)}
11 | {user?.lastName?.charAt(0)}
12 |
13 |
14 | );
15 | };
16 |
17 | export default UserAvatar;
18 |
--------------------------------------------------------------------------------
/constants.ts:
--------------------------------------------------------------------------------
1 | export const MAX_FREE_COUNTS = 5;
2 |
--------------------------------------------------------------------------------
/hooks/use-pro-modal.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface userProModalStore {
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 increaseApiLimit = async () => {
7 | const { userId } = auth();
8 |
9 | if (!userId) {
10 | return;
11 | }
12 |
13 | const userApiLimit = await prismadb.userApiLimit.findUnique({
14 | where: {
15 | userId,
16 | },
17 | });
18 |
19 | if (userApiLimit) {
20 | await prismadb.userApiLimit.update({
21 | where: { userId: userId },
22 | data: { count: userApiLimit.count + 1 },
23 | });
24 | } else {
25 | await prismadb.userApiLimit.create({
26 | data: { userId: userId, count: 1 },
27 | });
28 | }
29 | };
30 |
31 | export const checkApiLimit = async () => {
32 | const { userId } = auth();
33 |
34 | if (!userId) {
35 | return false;
36 | }
37 |
38 | const userApiLimit = await prismadb.userApiLimit.findUnique({
39 | where: { userId: userId },
40 | });
41 |
42 | if (!userApiLimit || userApiLimit.count < MAX_FREE_COUNTS) {
43 | return true;
44 | } else {
45 | return false;
46 | }
47 | };
48 |
49 | export const getApiLimitCount = async () => {
50 | const { userId } = auth();
51 |
52 | if (!userId) {
53 | return 0;
54 | }
55 |
56 | const userApiLimit = await prismadb.userApiLimit.findUnique({
57 | where: {
58 | userId,
59 | },
60 | });
61 |
62 | if (!userApiLimit) {
63 | return 0;
64 | }
65 |
66 | return userApiLimit.count;
67 | };
68 |
--------------------------------------------------------------------------------
/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 |
9 | if (process.env.NODE_ENV !== "production") globalThis.prisma = prismadb;
10 |
11 | export default prismadb;
12 |
--------------------------------------------------------------------------------
/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 "./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 >
33 | Date.now();
34 |
35 | return !!isValid;
36 | };
37 |
--------------------------------------------------------------------------------
/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 | }
11 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | // This example protects all routes including api/trpc routes
4 | // Please edit this to allow other routes to be public as needed.
5 | // See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware
6 | export default authMiddleware({
7 | publicRoutes: ["/", "/api/webhook"],
8 | });
9 |
10 | export const config = {
11 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
12 | };
13 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: [
5 | "googleusercontent.com",
6 | "oaidalleapiprodscus.blob.core.windows.net",
7 | "cdn.openai.com",
8 | ],
9 | },
10 | };
11 |
12 | module.exports = nextConfig;
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "witty-ai-saas",
3 | "version": "0.1.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.23.2",
14 | "@hookform/resolvers": "^3.2.0",
15 | "@prisma/client": "^5.1.1",
16 | "@radix-ui/react-avatar": "^1.0.3",
17 | "@radix-ui/react-dialog": "^1.0.4",
18 | "@radix-ui/react-label": "^2.0.2",
19 | "@radix-ui/react-progress": "^1.0.3",
20 | "@radix-ui/react-select": "^1.2.2",
21 | "@radix-ui/react-slot": "^1.0.2",
22 | "@types/node": "20.4.10",
23 | "@types/react": "18.2.20",
24 | "@types/react-dom": "18.2.7",
25 | "autoprefixer": "10.4.14",
26 | "axios": "^1.4.0",
27 | "class-variance-authority": "^0.7.0",
28 | "clsx": "^2.0.0",
29 | "crisp-sdk-web": "^1.0.21",
30 | "eslint": "8.47.0",
31 | "eslint-config-next": "13.4.13",
32 | "lucide-react": "^0.263.1",
33 | "next": "13.4.13",
34 | "openai": "^3.3.0",
35 | "postcss": "8.4.27",
36 | "react": "18.2.0",
37 | "react-dom": "18.2.0",
38 | "react-hook-form": "^7.45.4",
39 | "react-hot-toast": "^2.4.1",
40 | "react-markdown": "^8.0.7",
41 | "replicate": "^0.16.0",
42 | "sharp": "^0.32.6",
43 | "stripe": "^12.18.0",
44 | "tailwind-merge": "^1.14.0",
45 | "tailwindcss": "3.3.3",
46 | "tailwindcss-animate": "^1.0.6",
47 | "typescript": "5.1.6",
48 | "typewriter-effect": "^2.20.1",
49 | "zod": "^3.21.4",
50 | "zustand": "^4.4.1"
51 | },
52 | "devDependencies": {
53 | "prisma": "^5.1.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mysql"
10 | url = env("DATABASE_URL")
11 | relationMode = "prisma"
12 | }
13 |
14 | model UserApiLimit {
15 | id String @id @default(cuid())
16 | userId String @unique
17 | count Int @default(0)
18 | createdAt DateTime @default(now())
19 | updatedAt DateTime @updatedAt
20 | }
21 |
22 | model UserSubscription {
23 | id String @id @default(cuid())
24 | userId String @unique
25 | stripeCustomerId String? @unique @map(name: "stripe_customer_id")
26 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
27 | stripePriceId String? @map(name: "stripe_price_id")
28 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
29 | }
30 |
--------------------------------------------------------------------------------
/public/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SammyLeths/witty-ai-saas/9be9c563cddef98b92600c4e70ece8bc366ca6c7/public/empty.png
--------------------------------------------------------------------------------
/public/logo-old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SammyLeths/witty-ai-saas/9be9c563cddef98b92600c4e70ece8bc366ca6c7/public/logo-old.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SammyLeths/witty-ai-saas/9be9c563cddef98b92600c4e70ece8bc366ca6c7/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 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------