Repository: ayusshrathore/ai-saas
Branch: main
Commit: 623dba771e01
Files: 76
Total size: 97.7 KB
Directory structure:
gitextract_d84y1w4g/
├── .eslintrc.json
├── .github/
│ └── workflows/
│ └── npm-gulp.yml
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── (auth)/
│ │ ├── (routes)/
│ │ │ ├── sign-in/
│ │ │ │ └── [[...sign-in]]/
│ │ │ │ └── page.tsx
│ │ │ └── sign-up/
│ │ │ └── [[...sign-up]]/
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── (routes)/
│ │ │ ├── code/
│ │ │ │ ├── constants.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── conversation/
│ │ │ │ ├── constants.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── dashboard/
│ │ │ │ └── page.tsx
│ │ │ ├── image/
│ │ │ │ ├── constants.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── music/
│ │ │ │ ├── constants.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── settings/
│ │ │ │ └── page.tsx
│ │ │ └── video/
│ │ │ ├── constants.tsx
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (landing)/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/
│ │ ├── code/
│ │ │ └── route.ts
│ │ ├── conversation/
│ │ │ └── route.ts
│ │ ├── image/
│ │ │ └── route.ts
│ │ ├── music/
│ │ │ └── route.ts
│ │ ├── stripe/
│ │ │ └── route.ts
│ │ ├── video/
│ │ │ └── route.ts
│ │ └── webhook/
│ │ └── route.ts
│ ├── globals.css
│ └── layout.tsx
├── components/
│ ├── bot-avatar.tsx
│ ├── crisp-chat.tsx
│ ├── crisp-provider.tsx
│ ├── empty.tsx
│ ├── free-counter.tsx
│ ├── heading.tsx
│ ├── landing-content.tsx
│ ├── landing-hero.tsx
│ ├── landing-navbar.tsx
│ ├── loader.tsx
│ ├── mobile-sidebar.tsx
│ ├── modal-provider.tsx
│ ├── navbar.tsx
│ ├── pro-modal.tsx
│ ├── sidebar.tsx
│ ├── subscription-button.tsx
│ ├── toaster-provider.tsx
│ ├── ui/
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── progress.tsx
│ │ ├── select.tsx
│ │ └── sheet.tsx
│ └── user-avatar.tsx
├── components.json
├── constants.ts
├── hooks/
│ └── use-pro-modal.tsx
├── lib/
│ ├── api-limit.ts
│ ├── prismadb.ts
│ ├── stripe.ts
│ ├── subscription.ts
│ └── utils.ts
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prisma/
│ └── schema.prisma
├── tailwind.config.js
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: .github/workflows/npm-gulp.yml
================================================
name: NodeJS with Gulp
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Build
run: |
npm install
gulp
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Ayush Rathore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# AI-SaaS - AI-Powered Software-as-a-Service Application
[](https://opensource.org/licenses/MIT)
[](https://opensource.org/licenses/MIT)
[](https://nextjs.org/)
[](https://openai.com/)
[](https://replicate.ai/)
[](https://tailwindcss.com/)
[](https://prisma.io/)
[](https://stripe.com/)
AI-SaaS is an advanced and adaptable Software-as-a-Service (SaaS) application that harnesses the capabilities of cutting-edge technologies, including Next.js, OpenAI, Replicate, Tailwind CSS, Prisma, and Stripe. The primary goal of this application is to empower users by offering AI-powered services that facilitate easy access and utilization of artificial intelligence in their projects and workflows.
## Features
- **AI Services**: AI-SaaS provides an extensive array of AI services, including conversation, code generation, image generation, music generation, and video generation. These services are accessible through an intuitive and user-friendly interface.
- **Next.js**: AI-SaaS is built on the Next.js framework, offering server-side rendering, routing, and other essential features out of the box. This ensures superior performance and search engine optimization (SEO) for the application.
- **OpenAI Integration**: The application seamlessly integrates with OpenAI's powerful AI models and APIs, enabling users to leverage state-of-the-art AI capabilities. From generating human-like text to answering questions, AI-SaaS harnesses the full potential of OpenAI.
- **Replicate**: AI-SaaS employs Replicate to enhance model reproducibility and facilitate seamless experimentation with various AI models. This ensures the AI models used in the application are robust and reliable.
- **Tailwind CSS**: The UI of AI-SaaS is meticulously styled using Tailwind CSS, a utility-first CSS framework. This enables easy customization and consistent design throughout the application.
- **Prisma**: The application utilizes Prisma as its ORM (Object-Relational Mapping) tool, simplifying database access and management. This enhances the efficiency of handling user data and preferences.
- **Stripe Integration**: AI-SaaS seamlessly incorporates Stripe for secure and efficient payment processing. Users can subscribe to premium plans and access additional AI services based on their subscription level.
## Screenshots
## Getting Started
To run AI-SaaS locally, follow these steps:
1. **Clone the repository**:
```bash
git clone https://github.com/ayusshrathore/ai-saas.git
cd ai-saas
```
2. **Install dependencies**:
```bash
npm install
# or
yarn install
```
3. **Configure environment variables**:
To ensure proper functionality, set up environment variables for API keys and other sensitive information. Create a `.env` file in the root directory and populate it with the necessary variables. For reference, consult the `.env.example` file for the required variables.
4. **Run the application**:
```bash
npm run dev
# or
yarn dev
```
The application should now be running locally at `http://localhost:3000`.
## Deployment
AI-SaaS can be deployed to various hosting platforms that support Next.js applications. Before deployment, make sure you have configured the necessary environment variables for production.
## Contributions
Contributions to AI-SaaS are highly appreciated! If you encounter any bugs or have suggestions for new features, please feel free to open an issue or submit a pull request.
When contributing, adhere to the existing code style and include comprehensive test cases for new features.
## License
AI-SaaS is released under the [MIT License](https://opensource.org/licenses/MIT).
## Acknowledgments
AI-SaaS is built with the invaluable support and integration of several open-source projects and technologies. I extend my gratitude to the developers and maintainers of Next.js, OpenAI, Replicate, Tailwind CSS, Prisma, and Stripe for their significant contributions to the development community.
[](https://app.netlify.com/sites/superlative-malabi-796b55/deploys)
================================================
FILE: app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx
================================================
import { SignIn } from "@clerk/nextjs";
export default function Page() {
return ;
}
================================================
FILE: app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx
================================================
import { SignUp } from "@clerk/nextjs";
export default function Page() {
return ;
}
================================================
FILE: app/(auth)/layout.tsx
================================================
import React from "react";
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
return
{children}
;
};
export default AuthLayout;
================================================
FILE: app/(dashboard)/(routes)/code/constants.tsx
================================================
import * as z from "zod";
export const formSchema = z.object({
prompt: z.string().min(1, {
message: "Prompt is required.",
}),
});
================================================
FILE: app/(dashboard)/(routes)/code/page.tsx
================================================
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { Code } from "lucide-react";
import { useRouter } from "next/navigation";
import { ChatCompletionRequestMessage } from "openai";
import { useState } from "react";
import { useForm } from "react-hook-form";
import ReactMarkdown from "react-markdown";
import * as z from "zod";
import { BotAvatar } from "@/components/bot-avatar";
import { Empty } from "@/components/empty";
import { Heading } from "@/components/heading";
import { Loader } from "@/components/loader";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { UserAvatar } from "@/components/user-avatar";
import { cn } from "@/lib/utils";
import useProModal from "@/hooks/use-pro-modal";
import { toast } from "react-hot-toast";
import { formSchema } from "./constants";
const CodePage = () => {
const router = useRouter();
const proModal = useProModal();
const [messages, setMessages] = useState([]);
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
prompt: "",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (values: z.infer) => {
console.log(values);
try {
const userMessage: ChatCompletionRequestMessage = {
role: "user",
content: values.prompt,
};
const newMessages = [...messages, userMessage];
const response = await axios.post("/api/code", {
messages: newMessages,
});
setMessages((current) => [...current, userMessage, response.data]);
form.reset();
} catch (error: any) {
console.log(error);
if (error?.response?.status === 403) {
proModal.onOpen();
} else {
toast.error("Something went wrong.");
}
} finally {
router.refresh();
}
};
return (
{isLoading && (
)}
{messages.length === 0 && !isLoading &&
}
{messages.map((message, index) => (
{message.role === "user" ?
:
}
(
),
code: ({ node, ...props }) => ,
}}
>
{message.content || ""}
))}
);
};
export default CodePage;
================================================
FILE: app/(dashboard)/(routes)/conversation/constants.tsx
================================================
import * as z from "zod";
export const formSchema = z.object({
prompt: z.string().min(1, {
message: "Prompt is required.",
}),
});
================================================
FILE: app/(dashboard)/(routes)/conversation/page.tsx
================================================
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { MessageSquare } from "lucide-react";
import { useRouter } from "next/navigation";
import { ChatCompletionRequestMessage } from "openai";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { BotAvatar } from "@/components/bot-avatar";
import { Empty } from "@/components/empty";
import { Heading } from "@/components/heading";
import { Loader } from "@/components/loader";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { UserAvatar } from "@/components/user-avatar";
import { cn } from "@/lib/utils";
import useProModal from "@/hooks/use-pro-modal";
import { toast } from "react-hot-toast";
import { formSchema } from "./constants";
const ConversationPage = () => {
const router = useRouter();
const proModal = useProModal();
const [messages, setMessages] = useState([]);
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
prompt: "",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (values: z.infer) => {
console.log(values);
try {
const userMessage: ChatCompletionRequestMessage = {
role: "user",
content: values.prompt,
};
const newMessages = [...messages, userMessage];
const response = await axios.post("/api/conversation", {
messages: newMessages,
});
setMessages((current) => [...current, userMessage, response.data]);
form.reset();
} catch (error: any) {
console.log(error);
if (error?.response?.status === 403) {
proModal.onOpen();
} else {
toast.error("Something went wrong.");
}
} finally {
router.refresh();
}
};
return (
{isLoading && (
)}
{messages.length === 0 && !isLoading &&
}
{messages.map((message, index) => (
{message.role === "user" ?
:
}
{message.content}
))}
);
};
export default ConversationPage;
================================================
FILE: app/(dashboard)/(routes)/dashboard/page.tsx
================================================
"use client";
import {
ArrowRight,
Code,
ImageIcon,
MessageSquare,
Music,
VideoIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
const tools = [
{
label: "Conversation",
icon: MessageSquare,
color: "text-violet-500",
bgColor: "bg-violet-500/10",
href: "/conversation",
},
{
label: "Music Generation",
icon: Music,
color: "text-emerald-500",
bgColor: "bg-emerald-500/10",
href: "/music",
},
{
label: "Image Generation",
icon: ImageIcon,
color: "text-pink-700",
bgColor: "bg-pink-700/10",
href: "/image",
},
{
label: "Video Generation",
icon: VideoIcon,
color: "text-orange-700",
bgColor: "bg-orange-700/10",
href: "/video",
},
{
label: "Code Generation",
icon: Code,
color: "text-green-700",
bgColor: "bg-green-700/10",
href: "/code",
},
];
const DashboardPage = () => {
const router = useRouter();
return (
Explore the power of AI
Prometheus is a platform that allows you to generate music, videos,
and code using the power of AI.
{tools.map((tool) => (
router.push(tool.href)}
key={tool.href}
className={
"p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer"
}
>
))}
);
};
export default DashboardPage;
================================================
FILE: app/(dashboard)/(routes)/image/constants.tsx
================================================
import * as z from "zod";
export const formSchema = z.object({
prompt: z.string().min(1, {
message: "Image Prompt is required.",
}),
amount: z.string().min(1),
resolution: z.string().min(1),
});
export const amountOptions = [
{
value: "1",
label: "1 Photo",
},
{
value: "2",
label: "2 Photos",
},
{
value: "3",
label: "3 Photos",
},
{
value: "4",
label: "4 Photos",
},
{
value: "5",
label: "5 Photos",
},
];
export const resolutionOptions = [
{
value: "256x256",
label: "256x256",
},
{
value: "512x512",
label: "512x512",
},
{
value: "1024x1024",
label: "1024x1024",
},
];
================================================
FILE: app/(dashboard)/(routes)/image/page.tsx
================================================
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { Download, ImageIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Empty } from "@/components/empty";
import { Heading } from "@/components/heading";
import { Loader } from "@/components/loader";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardFooter } from "@/components/ui/card";
import useProModal from "@/hooks/use-pro-modal";
import Image from "next/image";
import { toast } from "react-hot-toast";
import { amountOptions, formSchema, resolutionOptions } from "./constants";
const ImagePage = () => {
const router = useRouter();
const proModal = useProModal();
const [images, setImages] = useState([]);
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
prompt: "",
amount: "1",
resolution: "512x512",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (values: z.infer) => {
console.log(values);
try {
setImages([]);
const response = await axios.post("/api/image", values);
const urls = response.data.map((image: { url: string }) => image.url);
setImages(urls);
form.reset();
} catch (error: any) {
console.log(error);
if (error?.response?.status === 403) {
proModal.onOpen();
} else {
toast.error("Something went wrong.");
}
} finally {
router.refresh();
}
};
return (
{isLoading && (
)}
{images.length === 0 && !isLoading &&
}
{images.map((image, index) => (
window.open(image)} variant="secondary" className="w-full">
Download
))}
);
};
export default ImagePage;
================================================
FILE: app/(dashboard)/(routes)/music/constants.tsx
================================================
import * as z from "zod";
export const formSchema = z.object({
prompt: z.string().min(1, {
message: "Prompt is required.",
}),
});
================================================
FILE: app/(dashboard)/(routes)/music/page.tsx
================================================
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { Music } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Empty } from "@/components/empty";
import { Heading } from "@/components/heading";
import { Loader } from "@/components/loader";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import useProModal from "@/hooks/use-pro-modal";
import { toast } from "react-hot-toast";
import { formSchema } from "./constants";
const MusicPage = () => {
const router = useRouter();
const proModal = useProModal();
const [music, setMusic] = useState("");
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
prompt: "",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (values: z.infer) => {
console.log(values);
try {
setMusic("");
const response = await axios.post("/api/music");
setMusic(response.data.audio);
form.reset();
} catch (error: any) {
console.log(error);
if (error?.response?.status === 403) {
proModal.onOpen();
} else {
toast.error("Something went wrong.");
}
} finally {
router.refresh();
}
};
return (
{isLoading && (
)}
{!music && !isLoading &&
}
{music && (
)}
);
};
export default MusicPage;
================================================
FILE: app/(dashboard)/(routes)/settings/page.tsx
================================================
import { Heading } from "@/components/heading";
import { SubscriptionButton } from "@/components/subscription-button";
import { checkSubscription } from "@/lib/subscription";
import { Settings } from "lucide-react";
const SettingsPage = async () => {
const isPro = await checkSubscription();
return (
{isPro ? "You are currently on a pro plan." : "You are currently on a free plan."}
);
};
export default SettingsPage;
================================================
FILE: app/(dashboard)/(routes)/video/constants.tsx
================================================
import * as z from "zod";
export const formSchema = z.object({
prompt: z.string().min(1, {
message: "Prompt is required.",
}),
});
================================================
FILE: app/(dashboard)/(routes)/video/page.tsx
================================================
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { Video } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Empty } from "@/components/empty";
import { Heading } from "@/components/heading";
import { Loader } from "@/components/loader";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import useProModal from "@/hooks/use-pro-modal";
import { toast } from "react-hot-toast";
import { formSchema } from "./constants";
const VideoPage = () => {
const router = useRouter();
const proModal = useProModal();
const [video, setVideo] = useState("");
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
prompt: "",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (values: z.infer) => {
console.log(values);
try {
setVideo("");
const response = await axios.post("/api/music");
setVideo(response.data.audio);
form.reset();
} catch (error: any) {
console.log(error);
if (error?.response?.status === 403) {
proModal.onOpen();
} else {
toast.error("Something went wrong.");
}
} finally {
router.refresh();
}
};
return (
{isLoading && (
)}
{!video && !isLoading &&
}
{video && (
)}
);
};
export default VideoPage;
================================================
FILE: app/(dashboard)/layout.tsx
================================================
import Navbar from "@/components/navbar";
import Sidebar from "@/components/sidebar";
import { getApiLimitCount } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
const DashboardLayout = async ({ children }: { children: React.ReactNode }) => {
const apiLimitCount = await getApiLimitCount();
const isPro = await checkSubscription();
return (
);
};
export default DashboardLayout;
================================================
FILE: app/(landing)/layout.tsx
================================================
const LandingLayout = ({ children }: { children: React.ReactNode }) => {
return (
{children}
);
};
export default LandingLayout;
================================================
FILE: app/(landing)/page.tsx
================================================
import LandingContent from "@/components/landing-content";
import { LandingHero } from "@/components/landing-hero";
import { LandingNabvbar } from "@/components/landing-navbar";
function LandingPage() {
return (
);
}
export default LandingPage;
================================================
FILE: app/api/code/route.ts
================================================
import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openAi = new OpenAIApi(configuration);
const instructionMessage: ChatCompletionRequestMessage = {
role: "system",
content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations.",
};
export async function POST(req: Request) {
try {
const { userId } = auth();
const body = await req.json();
const { messages } = body;
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!configuration) {
return new NextResponse("OpenAI API Key not configured", { status: 500 });
}
if (!messages) {
return new NextResponse("Missing messages", { status: 400 });
}
const isAllowed = await checkApiLimit();
const isPro = await checkSubscription();
if (!isAllowed && !isPro) {
return new NextResponse("API Limit Exceeded", { status: 403 });
}
const response = await openAi.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [instructionMessage, ...messages],
});
if (!isPro) {
await increaseApiLimit();
}
return NextResponse.json(response.data.choices[0].message, { status: 200 });
} catch (error) {
console.log("[CODE_ERROR]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
================================================
FILE: app/api/conversation/route.ts
================================================
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { Configuration, OpenAIApi } from "openai";
import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openAi = new OpenAIApi(configuration);
export async function POST(req: Request) {
try {
const { userId } = auth();
const body = await req.json();
const { messages } = body;
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!configuration) {
return new NextResponse("OpenAI API Key not configured", { status: 500 });
}
if (!messages) {
return new NextResponse("Missing messages", { status: 400 });
}
const isAllowed = await checkApiLimit();
const isPro = await checkSubscription();
if (!isAllowed && !isPro) {
return new NextResponse("API Limit Exceeded", { status: 403 });
}
const response = await openAi.createChatCompletion({
model: "gpt-3.5-turbo",
messages,
});
if (!isPro) {
await increaseApiLimit();
}
return NextResponse.json(response.data.choices[0].message, { status: 200 });
} catch (error) {
console.log("[CONVERSATION_ERROR]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
================================================
FILE: app/api/image/route.ts
================================================
import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openAi = new OpenAIApi(configuration);
export async function POST(req: Request) {
try {
const { userId } = auth();
const body = await req.json();
const { prompt, amount = 1, resolution = "512x512" } = body;
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!configuration) {
return new NextResponse("OpenAI API Key not configured", { status: 500 });
}
if (!prompt) {
return new NextResponse("Missing prompt", { status: 400 });
}
if (!amount) {
return new NextResponse("Missing amount", { status: 400 });
}
if (!resolution) {
return new NextResponse("Missing resolution", { status: 400 });
}
const isAllowed = await checkApiLimit();
const isPro = await checkSubscription();
if (!isAllowed && !isPro) {
return new NextResponse("API Limit Exceeded", { status: 403 });
}
const response = await openAi.createImage({
prompt,
n: parseInt(amount, 10),
size: resolution,
});
if (!isPro) {
await increaseApiLimit();
}
return NextResponse.json(response.data.data, { status: 200 });
} catch (error) {
console.log("[CONVERSATION_ERROR]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
================================================
FILE: app/api/music/route.ts
================================================
import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import Replicate from "replicate";
const replicate = new Replicate({
auth: process.env.REPLICATE_API_TOKEN!,
});
export async function POST(req: Request) {
try {
const { userId } = auth();
const body = await req.json();
const { prompt } = body;
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!prompt) {
return new NextResponse("Prompt is required", { status: 400 });
}
const isAllowed = await checkApiLimit();
const isPro = await checkSubscription();
if (!isAllowed && !isPro) {
return new NextResponse("API Limit Exceeded", { status: 403 });
}
const response = await replicate.run("riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", {
input: {
prompt_a: prompt,
},
});
if (!isPro) {
await increaseApiLimit();
}
return NextResponse.json(response, { status: 200 });
} catch (error) {
console.log("[MUSIC_ERROR]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
================================================
FILE: app/api/stripe/route.ts
================================================
import { auth, currentUser } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import prismadb from "@/lib/prismadb";
import { stripe } from "@/lib/stripe";
import { absoluteUrl } from "@/lib/utils";
const settingsUrl = absoluteUrl("/settings");
export async function GET() {
try {
const { userId } = auth();
const user = await currentUser();
if (!userId || !user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const userSubscription = await prismadb.userSubscription.findUnique({
where: {
userId,
},
});
if (userSubscription && userSubscription.stripeCustomerId) {
const stripeSession = await stripe.billingPortal.sessions.create({
customer: userSubscription.stripeCustomerId,
return_url: settingsUrl,
});
return new NextResponse(JSON.stringify({ url: stripeSession.url }), { status: 200 });
}
const stripeSession = await stripe.checkout.sessions.create({
success_url: settingsUrl,
cancel_url: settingsUrl,
payment_method_types: ["card"],
mode: "subscription",
billing_address_collection: "auto",
customer_email: user.emailAddresses[0].emailAddress,
line_items: [
{
price_data: {
currency: "USD",
product_data: {
name: "Prometheus Pro",
description: "Prometheus Pro",
},
unit_amount: 2000,
recurring: {
interval: "month",
},
},
quantity: 1,
},
],
metadata: {
userId,
},
});
return new NextResponse(JSON.stringify({ url: stripeSession.url }), { status: 200 });
} catch (error) {
console.log("[STRIPE_ERROR]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
================================================
FILE: app/api/video/route.ts
================================================
import { checkApiLimit, increaseApiLimit } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import Replicate from "replicate";
const replicate = new Replicate({
auth: process.env.REPLICATE_API_TOKEN!,
});
export async function POST(req: Request) {
try {
const { userId } = auth();
const body = await req.json();
const { prompt } = body;
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!prompt) {
return new NextResponse("Prompt is required", { status: 400 });
}
const isAllowed = await checkApiLimit();
const isPro = await checkSubscription();
if (!isAllowed && !isPro) {
return new NextResponse("API Limit Exceeded", { status: 403 });
}
const response = await replicate.run("anotherjesse/zeroscope-v2-xl:9f747673945c62801b13b84701c783929c0ee784e4748ec062204894dda1a351", {
input: {
prompt,
},
});
if (!isPro) {
await increaseApiLimit();
}
return NextResponse.json(response, { status: 200 });
} catch (error) {
console.log("[VIDEO_ERROR]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
================================================
FILE: app/api/webhook/route.ts
================================================
import { headers } from "next/headers";
import Stripe from "stripe";
import prismadb from "@/lib/prismadb";
import { stripe } from "@/lib/stripe";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get("Stripe-Signature") as string;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (error: any) {
return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
}
const session = event.data.object as Stripe.Checkout.Session;
if (event.type === "checkout.session.completed") {
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
if (!session?.metadata?.userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
await prismadb.userSubscription.create({
data: {
userId: session.metadata.userId,
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
if (event.type === "invoice.payment_succeeded") {
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await prismadb.userSubscription.update({
where: {
stripeSubscriptionId: subscription.id,
},
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
return new NextResponse("OK", { status: 200 });
}
================================================
FILE: app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
:root {
height: 100%;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 248 90% 66%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 217.2 32.6% 17.5%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
================================================
FILE: app/layout.tsx
================================================
import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import CrispProvider from "@/components/crisp-provider";
import { ModalProvider } from "@/components/modal-provider";
import { ToasterProvider } from "@/components/toaster-provider";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Prometheus AI",
description: "An AI platform.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
================================================
FILE: components/bot-avatar.tsx
================================================
import { Avatar, AvatarImage } from "./ui/avatar";
export const BotAvatar = () => {
return (
);
};
================================================
FILE: components/crisp-chat.tsx
================================================
"use client";
import { Crisp } from "crisp-sdk-web";
import { useEffect } from "react";
export const CrispChat = () => {
useEffect(() => {
Crisp.configure(process.env.CRISP_WEBSITE_ID!);
}, []);
return null;
};
================================================
FILE: components/crisp-provider.tsx
================================================
import { CrispChat } from "./crisp-chat";
export const CrispProvider = () => {
return ;
};
export default CrispProvider;
================================================
FILE: components/empty.tsx
================================================
import Image from "next/image";
interface EmptyProps {
label: string;
}
export const Empty = ({ label }: EmptyProps) => {
return (
);
};
================================================
FILE: components/free-counter.tsx
================================================
"use client";
import { FC, useEffect, useState } from "react";
import { MAX_FREE_COUNTS } from "@/constants";
import useProModal from "@/hooks/use-pro-modal";
import { Zap } from "lucide-react";
import { Button } from "./ui/button";
import { Card, CardContent } from "./ui/card";
import { Progress } from "./ui/progress";
interface FreeCounterProps {
apiLimitCount: number;
isPro: boolean;
}
export const FreeCounter: FC = ({ apiLimitCount = 0, isPro = false }) => {
const [mounted, setMounted] = useState(false);
const proModal = useProModal();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
if (isPro) {
return null;
}
return (
{apiLimitCount} / {MAX_FREE_COUNTS} Free Generations
Upgrade
);
};
export default FreeCounter;
================================================
FILE: components/heading.tsx
================================================
import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface HeadingProps {
title: string;
description: string;
icon: LucideIcon;
iconColor?: string;
bgColor?: string;
}
export const Heading = ({
title,
description,
icon: Icon,
iconColor,
bgColor,
}: HeadingProps) => {
return (
);
};
================================================
FILE: components/landing-content.tsx
================================================
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
const testimonials = [
{
name: "John Doe",
avatar: "A",
title: "Software Engineer",
description: "This is the best application I've ever used!",
},
{
name: "John Doe",
avatar: "A",
title: "Software Engineer",
description: "This is the best application I've ever used!",
},
{
name: "John Doe",
avatar: "A",
title: "Software Engineer",
description: "This is the best application I've ever used!",
},
{
name: "John Doe",
avatar: "A",
title: "Software Engineer",
description: "This is the best application I've ever used!",
},
];
export const LandingContent = () => {
return (
Testimonials
{testimonials.map((item) => (
{item.description}
))}
);
};
export default LandingContent;
================================================
FILE: components/landing-hero.tsx
================================================
"use client";
import { useAuth } from "@clerk/nextjs";
import Link from "next/link";
import TypewriterComponent from "typewriter-effect";
import { Button } from "./ui/button";
export const LandingHero = () => {
const { isSignedIn } = useAuth();
return (
Create content using the power of AI.
Start Generating For Free
No credit card required. Cancel anytime.
);
};
================================================
FILE: components/landing-navbar.tsx
================================================
"use client";
import { useAuth } from "@clerk/nextjs";
import { Montserrat } from "next/font/google";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
const font = Montserrat({
weight: "600",
subsets: ["latin"],
});
export const LandingNabvbar = () => {
const { isSignedIn } = useAuth();
return (
Prometheus
Get Started
);
};
================================================
FILE: components/loader.tsx
================================================
import Image from "next/image";
export const Loader = () => {
return (
Prometheus is thinking...
);
};
================================================
FILE: components/mobile-sidebar.tsx
================================================
"use client";
import { Menu } from "lucide-react";
import { useEffect, useState } from "react";
import Sidebar from "./sidebar";
import { Button } from "./ui/button";
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";
const MobileSidebar = ({ apiLimitCount = 0, isPro = false }: { apiLimitCount: number; isPro: boolean }) => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null;
}
return (
);
};
export default MobileSidebar;
================================================
FILE: components/modal-provider.tsx
================================================
"use client";
import { useEffect, useState } from "react";
import ProModal from "./pro-modal";
export const ModalProvider = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
>
);
};
================================================
FILE: components/navbar.tsx
================================================
import { UserButton } from "@clerk/nextjs";
import { getApiLimitCount } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
import MobileSidebar from "./mobile-sidebar";
const Navbar = async () => {
const apiLimitCount = await getApiLimitCount();
const isPro = await checkSubscription();
return (
);
};
export default Navbar;
================================================
FILE: components/pro-modal.tsx
================================================
"use client";
import axios from "axios";
import { useState } from "react";
import useProModal from "@/hooks/use-pro-modal";
import { cn } from "@/lib/utils";
import { Check, Code, ImageIcon, MessageSquare, Music, VideoIcon, Zap } from "lucide-react";
import { toast } from "react-hot-toast";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { Card } from "./ui/card";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
export const ProModal = () => {
const proModal = useProModal();
const [loading, setLoading] = useState(false);
const tools = [
{
label: "Conversation",
icon: MessageSquare,
color: "text-violet-500",
bgColor: "bg-violet-500/10",
},
{
label: "Music Generation",
icon: Music,
color: "text-emerald-500",
bgColor: "bg-emerald-500/10",
},
{
label: "Image Generation",
icon: ImageIcon,
color: "text-pink-700",
bgColor: "bg-pink-700/10",
},
{
label: "Video Generation",
icon: VideoIcon,
color: "text-orange-700",
bgColor: "bg-orange-700/10",
},
{
label: "Code Generation",
icon: Code,
color: "text-green-700",
bgColor: "bg-green-700/10",
},
];
const onSubscribe = async () => {
try {
setLoading(true);
const response = await axios.get("/api/stripe");
window.location.href = response.data.url;
} catch (error) {
console.log("[STRIPE_CLIENT_ERROR]", error);
toast.error("Something went wrong.");
} finally {
setLoading(false);
}
};
return (
Upgrade to Prometheus Pro
pro
{tools.map((tool) => (
))}
Upgrade
);
};
export default ProModal;
================================================
FILE: components/sidebar.tsx
================================================
"use client";
import { Code, ImageIcon, LayoutDashboard, MessageSquare, Music, Settings, VideoIcon } from "lucide-react";
import { Montserrat } from "next/font/google";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FC } from "react";
import { cn } from "@/lib/utils";
import FreeCounter from "./free-counter";
const montserrat = Montserrat({ weight: "600", subsets: ["latin"] });
const routes = [
{
label: "Dashboard",
icon: LayoutDashboard,
href: "/dashboard",
color: "text-sky-500",
},
{
label: "Conversation",
icon: MessageSquare,
href: "/conversation",
color: "text-violet-500",
},
{
label: "Image Generation",
icon: ImageIcon,
href: "/image",
color: "text-pink-700",
},
{
label: "Video Generation",
icon: VideoIcon,
href: "/video",
color: "text-orange-700",
},
{
label: "Music Generation",
icon: Music,
href: "/music",
color: "text-emerald-500",
},
{
label: "Code Generation",
icon: Code,
href: "/code",
color: "text-green-700",
},
{
label: "Settings",
icon: Settings,
href: "/settings",
},
];
interface SidebarProps {
apiLimitCount: number;
isPro: boolean;
}
const Sidebar: FC = ({ apiLimitCount = 0, isPro = false }) => {
const pathname = usePathname();
return (
Prometheus
{routes.map((route) => (
{route.label}
))}
);
};
export default Sidebar;
================================================
FILE: components/subscription-button.tsx
================================================
"use client";
import axios from "axios";
import { Zap } from "lucide-react";
import { FC, useState } from "react";
import { toast } from "react-hot-toast";
import { Button } from "./ui/button";
interface SubscriptionButtonProps {
isPro: boolean;
}
export const SubscriptionButton: FC = ({ isPro = false }) => {
const [loading, setLoading] = useState(false);
const onClick = async () => {
try {
setLoading(true);
const response = await axios.get("/api/stripe");
window.location.href = response.data.url;
} catch (error) {
console.log("[BILLING_ERROR]", error);
toast.error("Something went wrong.");
} finally {
setLoading(false);
}
};
return (
{isPro ? "Manage Subscription" : "Upgrade to Pro"}
{!isPro && }
);
};
================================================
FILE: components/toaster-provider.tsx
================================================
"use client";
import { Toaster } from "react-hot-toast";
export const ToasterProvider = () => {
return ;
};
================================================
FILE: components/ui/avatar.tsx
================================================
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
================================================
FILE: components/ui/badge.tsx
================================================
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return
;
}
export { Badge, badgeVariants };
================================================
FILE: components/ui/button.tsx
================================================
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {
asChild?: boolean;
}
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return ;
});
Button.displayName = "Button";
export { Button, buttonVariants };
================================================
FILE: components/ui/card.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
================================================
FILE: components/ui/dialog.tsx
================================================
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
...props
}: DialogPrimitive.DialogPortalProps) => (
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
================================================
FILE: components/ui/form.tsx
================================================
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath
> = {
name: TName
}
const FormFieldContext = React.createContext(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath
>({
...props
}: ControllerProps) => {
return (
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within ")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
{body}
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
================================================
FILE: components/ui/input.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes {}
const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
return (
)
}
)
Input.displayName = "Input"
export { Input }
================================================
FILE: components/ui/label.tsx
================================================
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, ...props }, ref) => (
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
================================================
FILE: components/ui/progress.tsx
================================================
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, value, ...props }, ref) => (
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
================================================
FILE: components/ui/select.tsx
================================================
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, position = "popper", ...props }, ref) => (
{children}
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}
================================================
FILE: components/ui/sheet.tsx
================================================
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = ({
className,
...props
}: SheetPrimitive.DialogPortalProps) => (
)
SheetPortal.displayName = SheetPrimitive.Portal.displayName
const SheetOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"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",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
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",
right:
"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",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef,
VariantProps {}
const SheetContent = React.forwardRef<
React.ElementRef,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
{children}
Close
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
================================================
FILE: components/user-avatar.tsx
================================================
import { useUser } from "@clerk/nextjs";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
export const UserAvatar = () => {
const { user } = useUser();
return (
{user?.firstName?.charAt(0)}
{user?.lastName?.charAt(0)}
);
};
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
================================================
FILE: constants.ts
================================================
export const MAX_FREE_COUNTS = 5;
================================================
FILE: hooks/use-pro-modal.tsx
================================================
import { create } from "zustand";
interface useProModalStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
export const useProModal = create((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
export default useProModal;
================================================
FILE: lib/api-limit.ts
================================================
import { auth } from "@clerk/nextjs";
import { MAX_FREE_COUNTS } from "@/constants";
import prismadb from "./prismadb";
export const increaseApiLimit = async () => {
const { userId } = auth();
if (!userId) {
return;
}
const userApiLimit = await prismadb.userApiLimit.findUnique({
where: {
userId,
},
});
if (!userApiLimit) {
await prismadb.userApiLimit.create({
data: {
userId,
count: 1,
},
});
} else {
await prismadb.userApiLimit.update({
where: {
userId,
},
data: {
count: userApiLimit.count + 1,
},
});
}
};
export const checkApiLimit = async () => {
const { userId } = auth();
if (!userId) {
return false;
}
const userApiLimit = await prismadb.userApiLimit.findUnique({
where: {
userId,
},
});
if (!userApiLimit || userApiLimit.count < MAX_FREE_COUNTS) {
return true;
} else {
return false;
}
};
export const getApiLimitCount = async () => {
const { userId } = auth();
if (!userId) {
return 0;
}
const userApiLimit = await prismadb.userApiLimit.findUnique({
where: {
userId,
},
});
if (!userApiLimit) {
return 0;
} else {
return userApiLimit.count;
}
};
================================================
FILE: lib/prismadb.ts
================================================
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
const prismadb = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalThis.prisma = prismadb;
export default prismadb;
================================================
FILE: lib/stripe.ts
================================================
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
typescript: true,
});
================================================
FILE: lib/subscription.ts
================================================
import { auth } from "@clerk/nextjs";
import prismadb from "./prismadb";
const DAY_IN_MS = 1000 * 60 * 60 * 24;
export const checkSubscription = async () => {
const { userId } = auth();
if (!userId) {
return false;
}
const userSubscription = await prismadb.userSubscription.findUnique({
where: {
userId,
},
select: {
stripeSubscriptionId: true,
stripeCurrentPeriodEnd: true,
stripeCustomerId: true,
stripePriceId: true,
},
});
if (!userSubscription) {
return false;
}
const isValid = userSubscription.stripePriceId && userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > Date.now();
return !!isValid;
};
================================================
FILE: lib/utils.ts
================================================
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`;
}
================================================
FILE: middleware.ts
================================================
import { authMiddleware } from "@clerk/nextjs";
// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware
export default authMiddleware({
publicRoutes: ["/", "/api/webhook"],
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["oaidalleapiprodscus.blob.core.windows.net"],
},
};
module.exports = nextConfig;
================================================
FILE: package.json
================================================
{
"name": "ai-saas",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@clerk/nextjs": "^4.23.0",
"@hookform/resolvers": "^3.1.1",
"@prisma/client": "^5.0.0",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-slot": "^1.0.2",
"@types/node": "20.4.3",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"axios": "^1.4.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"crisp-sdk-web": "^1.0.19",
"eslint": "8.45.0",
"eslint-config-next": "13.4.12",
"lucide-react": "^0.263.0",
"next": "13.4.12",
"openai": "^3.3.0",
"postcss": "8.4.27",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.45.2",
"react-hot-toast": "^2.4.1",
"react-markdown": "^8.0.7",
"replicate": "^0.12.3",
"stripe": "^12.16.0",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.6",
"typescript": "5.1.6",
"typewriter-effect": "^2.20.1",
"zod": "^3.21.4",
"zustand": "^4.3.9"
},
"devDependencies": {
"prisma": "^5.0.0"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: prisma/schema.prisma
================================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model UserApiLimit {
id String @id @default(cuid())
userId String @unique
count Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model UserSubscription {
id String @id @default(cuid())
userId String @unique
stripeCustomerId String? @unique @map(name: "stripe_customer_id")
stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
stripePriceId String? @unique @map(name: "stripe_price_id")
stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}