├── .eslintrc.json ├── app ├── favicon.ico ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (dashboard) │ ├── (routes) │ │ ├── video │ │ │ ├── constant.ts │ │ │ └── page.tsx │ │ ├── code │ │ │ ├── constant.ts │ │ │ └── page.tsx │ │ ├── music │ │ │ ├── constant.ts │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── constant.ts │ │ │ └── page.tsx │ │ ├── conversation │ │ │ ├── constant.ts │ │ │ └── page.tsx │ │ ├── image │ │ │ ├── constant.ts │ │ │ └── page.tsx │ │ └── dashboard │ │ │ └── page.tsx │ └── layout.tsx ├── (landing) │ ├── page.tsx │ └── layout.tsx ├── layout.tsx ├── api │ ├── music │ │ └── route.ts │ ├── video │ │ └── route.ts │ ├── conversation │ │ └── route.ts │ ├── image │ │ └── route.ts │ ├── code │ │ └── route.ts │ ├── stripe │ │ └── route.ts │ └── webhook │ │ └── route.ts └── globals.css ├── public ├── empty.png ├── logo.png ├── vercel.svg └── next.svg ├── postcss.config.js ├── components ├── toaster-provider.tsx ├── crisp-provider.tsx ├── bot-avatar.tsx ├── crisp-chat.tsx ├── theme-provider.tsx ├── modal-provider.tsx ├── user-avatar.tsx ├── loader.tsx ├── empty.tsx ├── navbar.tsx ├── ui │ ├── label.tsx │ ├── separator.tsx │ ├── progress.tsx │ ├── input.tsx │ ├── badge.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── select.tsx │ ├── form.tsx │ ├── sheet.tsx │ ├── command.tsx │ └── dropdown-menu.tsx ├── heading.tsx ├── subscription-button.tsx ├── mobile-sidebar.tsx ├── landing-navbar.tsx ├── free-counter.tsx ├── moddle-toggle.tsx ├── landing-hero.tsx ├── pro-modal.tsx └── sidebar.tsx ├── lib ├── stripe.ts ├── utils.ts ├── prismadb.ts ├── subscription.ts └── api-limit.ts ├── next.config.js ├── hooks └── use-pro-modal.tsx ├── components.json ├── middleware.ts ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── prisma └── schema.prisma ├── constant.ts ├── README.md ├── package.json └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruxin23/ai-saas/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruxin23/ai-saas/HEAD/public/empty.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruxin23/ai-saas/HEAD/public/logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /components/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "react-hot-toast" 4 | 5 | export const ToasterProvider = () => { 6 | return 7 | }; -------------------------------------------------------------------------------- /lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe' 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_API_KEY!, { 4 | apiVersion: '2023-08-16', 5 | typescript: true, 6 | }) -------------------------------------------------------------------------------- /components/crisp-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CrispChat } from "@/components/crisp-chat"; 4 | 5 | export const CrispProvider = () => { 6 | return 7 | }; 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/constant.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 | }); -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/code/constant.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Prompt is required.", 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/constant.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 | }); -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/constant.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/constant.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 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: [ 5 | "oaidalleapiprodscus.blob.core.windows.net" 6 | ] 7 | } 8 | } 9 | 10 | module.exports = nextConfig 11 | -------------------------------------------------------------------------------- /components/bot-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 2 | 3 | export const BotAvatar = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; -------------------------------------------------------------------------------- /components/crisp-chat.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useEffect } from "react"; 3 | import { Crisp } from "crisp-sdk-web"; 4 | 5 | export const CrispChat = () => { 6 | useEffect(() => { 7 | Crisp.configure("22cd3a04-d375-4031-8827-3b40993cd44a"); 8 | }, []); 9 | 10 | return null; 11 | }; -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /lib/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | declare global { 3 | var prisma: PrismaClient | undefined; 4 | } 5 | 6 | export const db = globalThis.prisma || new PrismaClient(); 7 | 8 | if (process.env.NODE_ENV !== "production") globalThis.prisma = db; 9 | 10 | export default db 11 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ 2 | children 3 | }: { 4 | children: React.ReactNode; 5 | }) => { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | 13 | export default AuthLayout; -------------------------------------------------------------------------------- /app/(landing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { LandingNavbar } from "@/components/landing-navbar"; 2 | import { LandingHero } from "@/components/landing-hero"; 3 | 4 | const LandingPage = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | 13 | export default LandingPage; -------------------------------------------------------------------------------- /hooks/use-pro-modal.tsx: -------------------------------------------------------------------------------- 1 | import {create} from "zustand" 2 | 3 | interface useProModalStore { 4 | isOpen: boolean 5 | onOpen: () => void 6 | onClose: () => void 7 | } 8 | 9 | export const useProModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({isOpen: true}), 12 | onClose: () => set({isOpen: false}) 13 | })) -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | const LandingLayout = ({ 2 | children 3 | }: { 4 | children: React.ReactNode; 5 | }) => { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | } 14 | 15 | export default LandingLayout; -------------------------------------------------------------------------------- /components/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import {ProModal} from "@/components/pro-modal"; 6 | 7 | export const ModalProvider = () => { 8 | const [isMounted, setIsMounted] = useState(false); 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) { 15 | return null; 16 | } 17 | 18 | return ( 19 | <> 20 | 21 | 22 | ); 23 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { useUser } from "@clerk/nextjs"; 2 | import { 3 | Avatar, 4 | AvatarFallback, 5 | AvatarImage 6 | } from "@/components/ui/avatar"; 7 | const UserAvatar = () => { 8 | const { user } = useUser(); 9 | return ( 10 | 11 | 12 | 13 | {user?.firstName?.charAt(0)} 14 | {user?.lastName?.charAt(0)} 15 | 16 | 17 | ); 18 | } 19 | 20 | export default UserAvatar; -------------------------------------------------------------------------------- /components/loader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | 3 | export const Loader = () => { 4 | return ( 5 |
6 |
7 | Logo 12 |
13 |

14 | Genius is thinking... 15 |

16 |
17 | ); 18 | }; -------------------------------------------------------------------------------- /components/empty.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface EmptyProps { 4 | label: string; 5 | } 6 | const Empty = ({ 7 | label 8 | }: EmptyProps) => { 9 | return ( 10 |
11 |
12 | Empty 13 |
14 |

15 | {label} 16 |

17 |
18 | ); 19 | } 20 | 21 | export default Empty; -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@clerk/nextjs" 2 | import MobileSidebar from './mobile-sidebar' 3 | import { getApiLimitCount } from "@/lib/api-limit" 4 | import { checkSubscription } from "@/lib/subscription"; 5 | import { ModeToggle } from "./moddle-toggle"; 6 | 7 | const Navbar = async () => { 8 | const apiLimitCount = await getApiLimitCount() 9 | const isPro = await checkSubscription() 10 | return ( 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | ) 19 | } 20 | 21 | export default Navbar 22 | 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/subscription.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | 5 | const DAY_IN_MS = 86_400_000; 6 | 7 | export const checkSubscription = async () => { 8 | const { userId } = auth(); 9 | 10 | if (!userId) { 11 | return false; 12 | } 13 | 14 | const userSubscription = await prismadb.userSubscription.findUnique({ 15 | where: { 16 | userId: userId, 17 | }, 18 | select: { 19 | stripeSubscriptionId: true, 20 | stripeCurrentPeriodEnd: true, 21 | stripeCustomerId: true, 22 | stripePriceId: true, 23 | }, 24 | }) 25 | 26 | if (!userSubscription) { 27 | return false; 28 | } 29 | 30 | const isValid = 31 | userSubscription.stripePriceId && 32 | userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > Date.now() 33 | 34 | return !!isValid; 35 | }; 36 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /components/ui/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/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 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar" 2 | import Sidebar from "@/components/sidebar" 3 | import { getApiLimitCount } from "@/lib/api-limit" 4 | import { checkSubscription } from "@/lib/subscription"; 5 | 6 | const DashboardLayout = async ({ 7 | children 8 | }: { 9 | children: React.ReactNode 10 | }) => { 11 | const isPro = await checkSubscription() 12 | const apiLimitCount = await getApiLimitCount() 13 | return ( 14 |
15 |
17 | 18 |
19 |
20 | 21 | {children} 22 |
23 |
24 | ) 25 | } 26 | 27 | export default DashboardLayout -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/image/constant.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | prompt: z.string().min(1, { 5 | message: "Photo prompt is required", 6 | }), 7 | amount: z.string().min(1), 8 | resolution: z.string().min(1), 9 | }); 10 | 11 | export const amountOptions = [ 12 | { 13 | value: "1", 14 | label: "1 Photo", 15 | }, 16 | { 17 | value: "2", 18 | label: "2 Photos", 19 | }, 20 | { 21 | value: "3", 22 | label: "3 Photos", 23 | }, 24 | { 25 | value: "4", 26 | label: "4 Photos", 27 | }, 28 | { 29 | value: "5", 30 | label: "5 Photos", 31 | }, 32 | ]; 33 | 34 | export const resolutionOptions = [ 35 | { 36 | value: "256x256", 37 | label: "256x256", 38 | }, 39 | { 40 | value: "512x512", 41 | label: "512x512", 42 | }, 43 | { 44 | value: "1024x1024", 45 | label: "1024x1024", 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /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 | datasource db { 5 | provider = "mysql" 6 | url = env("DATABASE_URL") 7 | relationMode = "prisma" 8 | } 9 | 10 | generator client { 11 | provider = "prisma-client-js" 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 | -------------------------------------------------------------------------------- /components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from "lucide-react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface HeadingProps { 6 | title: string; 7 | description: string; 8 | icon: LucideIcon; 9 | iconColor?: string; 10 | bgColor?: string; 11 | } 12 | 13 | export const Heading = ({ 14 | title, 15 | description, 16 | icon: Icon, 17 | iconColor, 18 | bgColor, 19 | }: HeadingProps) => { 20 | return ( 21 | <> 22 |
23 |
24 | 25 |
26 |
27 |

{title}

28 |

29 | {description} 30 |

31 |
32 |
33 | 34 | ); 35 | }; -------------------------------------------------------------------------------- /constant.ts: -------------------------------------------------------------------------------- 1 | import { Code, ImageIcon, MessageSquare, Music, VideoIcon } from "lucide-react"; 2 | 3 | export const MAX_FREE_COUNTS = 5; 4 | 5 | export const tools = [ 6 | { 7 | label: "Conversation", 8 | icon: MessageSquare, 9 | href: "/conversation", 10 | color: "text-violet-500", 11 | bgColor: "bg-violet-500/10", 12 | }, 13 | { 14 | label: "Music Generation", 15 | icon: Music, 16 | href: "/music", 17 | color: "text-emerald-500", 18 | bgColor: "bg-emerald-500/10", 19 | }, 20 | { 21 | label: "Image Generation", 22 | icon: ImageIcon, 23 | color: "text-pink-700", 24 | bgColor: "bg-pink-700/10", 25 | href: "/image", 26 | }, 27 | { 28 | label: "Video Generation", 29 | icon: VideoIcon, 30 | color: "text-orange-700", 31 | bgColor: "bg-orange-700/10", 32 | href: "/video", 33 | }, 34 | { 35 | label: "Code Generation", 36 | icon: Code, 37 | color: "text-green-700", 38 | bgColor: "bg-green-700/10", 39 | href: "/code", 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Settings } from "lucide-react"; 2 | 3 | import { Heading } from "@/components/heading"; 4 | import { SubscriptionButton } from "@/components/subscription-button"; 5 | import { checkSubscription } from "@/lib/subscription"; 6 | 7 | const SettingsPage = async () => { 8 | const isPro = await checkSubscription(); 9 | 10 | return ( 11 |
12 | 19 |
20 |
21 | {isPro ? "You are currently on a Pro plan." : "You are currently on a free plan."} 22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | export default SettingsPage; 30 | 31 | -------------------------------------------------------------------------------- /components/subscription-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useState } from "react"; 5 | import { Zap } from "lucide-react"; 6 | import { toast } from "react-hot-toast"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | 10 | export const SubscriptionButton = ({ 11 | isPro = false 12 | }: { 13 | isPro: boolean; 14 | }) => { 15 | const [loading, setLoading] = useState(false); 16 | 17 | const onClick = async () => { 18 | try { 19 | setLoading(true); 20 | 21 | const response = await axios.get("/api/stripe"); 22 | 23 | window.location.href = response.data.url; 24 | } catch (error) { 25 | toast.error("Something went wrong"); 26 | } finally { 27 | setLoading(false); 28 | } 29 | }; 30 | 31 | return ( 32 | 36 | ) 37 | }; 38 | -------------------------------------------------------------------------------- /components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { Menu } from "lucide-react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" 5 | import Sidebar from "./sidebar"; 6 | import { useEffect, useState } from "react"; 7 | 8 | interface MobileSidebarProps { 9 | apiLimitCount: number 10 | isPro: boolean 11 | } 12 | const MobileSidebar = ({ apiLimitCount, isPro = false }: MobileSidebarProps) => { 13 | const [isMounted, setIsMounted] = useState(false) 14 | useEffect(() => { 15 | setIsMounted(true) 16 | }, []) 17 | if (!isMounted) return null 18 | return ( 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default MobileSidebar; -------------------------------------------------------------------------------- /components/landing-navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Montserrat } from "next/font/google"; 4 | import Image from "next/image" 5 | import Link from "next/link" 6 | import { useAuth } from "@clerk/nextjs"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "@/components/ui/button"; 10 | 11 | const font = Montserrat({ weight: '600', subsets: ['latin'] }); 12 | 13 | export const LandingNavbar = () => { 14 | const { isSignedIn } = useAuth(); 15 | 16 | return ( 17 | 34 | ) 35 | } -------------------------------------------------------------------------------- /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 { ToasterProvider } from '@/components/toaster-provider' 6 | import { ModalProvider } from '@/components/modal-provider' 7 | import { CrispProvider } from '@/components/crisp-provider' 8 | import { ThemeProvider } from '@/components/theme-provider' 9 | const inter = Inter({ subsets: ['latin'] }) 10 | 11 | export const metadata: Metadata = { 12 | title: 'Genius', 13 | description: 'Genius is a platform for building AI-powered apps.', 14 | } 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode 20 | }) { 21 | return ( 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0" 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | } 25 | ) 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ) 35 | } 36 | 37 | export { Badge, badgeVariants } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | 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. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /lib/api-limit.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import { MAX_FREE_COUNTS } from "@/constant"; 5 | 6 | export const incrementApiLimit = async () => { 7 | const { userId } = auth(); 8 | 9 | if (!userId) { 10 | return; 11 | } 12 | 13 | const userApiLimit = await prismadb.userApiLimit.findUnique({ 14 | where: { userId: userId }, 15 | }); 16 | 17 | if (userApiLimit) { 18 | await prismadb.userApiLimit.update({ 19 | where: { userId: userId }, 20 | data: { count: userApiLimit.count + 1 }, 21 | }); 22 | } else { 23 | await prismadb.userApiLimit.create({ 24 | data: { userId: userId, count: 1 }, 25 | }); 26 | } 27 | }; 28 | 29 | export const checkApiLimit = async () => { 30 | const { userId } = auth(); 31 | 32 | if (!userId) { 33 | return false; 34 | } 35 | 36 | const userApiLimit = await prismadb.userApiLimit.findUnique({ 37 | where: { userId: userId }, 38 | }); 39 | 40 | if (!userApiLimit || userApiLimit.count < MAX_FREE_COUNTS) { 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | }; 46 | 47 | export const getApiLimitCount = async () => { 48 | const { userId } = auth(); 49 | 50 | if (!userId) { 51 | return 0; 52 | } 53 | 54 | const userApiLimit = await prismadb.userApiLimit.findUnique({ 55 | where: { 56 | userId 57 | } 58 | }); 59 | 60 | if (!userApiLimit) { 61 | return 0; 62 | } 63 | 64 | return userApiLimit.count; 65 | }; -------------------------------------------------------------------------------- /components/free-counter.tsx: -------------------------------------------------------------------------------- 1 | import { Zap } from "lucide-react"; 2 | import { useEffect, useState } from "react"; 3 | 4 | import { MAX_FREE_COUNTS } from "@/constant"; 5 | import { Card, CardContent } from "@/components/ui/card"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Progress } from "@/components/ui/progress"; 8 | import { useProModal } from "@/hooks/use-pro-modal"; 9 | 10 | export const FreeCounter = ({ 11 | isPro = false, 12 | apiLimitCount = 0, 13 | }: { 14 | isPro: boolean, 15 | apiLimitCount: number 16 | }) => { 17 | const [mounted, setMounted] = useState(false); 18 | const proModal = useProModal(); 19 | 20 | useEffect(() => { 21 | setMounted(true); 22 | }, []); 23 | 24 | if (!mounted) { 25 | return null; 26 | } 27 | 28 | 29 | if (isPro) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 |
38 |

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

41 | 42 |
43 | 47 |
48 |
49 |
50 | ) 51 | } -------------------------------------------------------------------------------- /app/api/music/route.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const replicate = new Replicate({ 9 | auth: process.env.REPLICATE_API_TOKEN!, 10 | }); 11 | 12 | export async function POST( 13 | req: Request 14 | ) { 15 | try { 16 | const { userId } = auth(); 17 | const body = await req.json(); 18 | const { prompt } = body; 19 | 20 | if (!userId) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | if (!prompt) { 25 | return new NextResponse("Prompt is required", { status: 400 }); 26 | } 27 | 28 | const freeTrial = await checkApiLimit(); 29 | const isPro = await checkSubscription(); 30 | 31 | if (!freeTrial && !isPro) { 32 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 33 | } 34 | 35 | const response = await replicate.run( 36 | "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05", 37 | { 38 | input: { 39 | prompt_a: prompt 40 | } 41 | } 42 | ); 43 | 44 | if (!isPro) { 45 | await incrementApiLimit(); 46 | } 47 | 48 | return NextResponse.json(response); 49 | } catch (error) { 50 | console.log('[MUSIC_ERROR]', error); 51 | return new NextResponse("Internal Error", { status: 500 }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /app/api/video/route.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | const replicate = new Replicate({ 9 | auth: process.env.REPLICATE_API_TOKEN!, 10 | }); 11 | 12 | export async function POST( 13 | req: Request 14 | ) { 15 | try { 16 | const { userId } = auth(); 17 | const body = await req.json(); 18 | const { prompt } = body; 19 | 20 | if (!userId) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | if (!prompt) { 25 | return new NextResponse("Prompt is required", { status: 400 }); 26 | } 27 | 28 | const freeTrial = await checkApiLimit(); 29 | const isPro = await checkSubscription(); 30 | 31 | if (!freeTrial && !isPro) { 32 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 33 | } 34 | 35 | const response = await replicate.run( 36 | "anotherjesse/zeroscope-v2-xl:71996d331e8ede8ef7bd76eba9fae076d31792e4ddf4ad057779b443d6aea62f", 37 | { 38 | input: { 39 | prompt, 40 | } 41 | } 42 | ); 43 | 44 | if (!isPro) { 45 | await incrementApiLimit(); 46 | } 47 | 48 | return NextResponse.json(response); 49 | } catch (error) { 50 | console.log('[VIDEO_ERROR]', error); 51 | return new NextResponse("Internal Error", { status: 500 }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /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/moddle-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { MoonIcon, SunIcon } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/landing-hero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TypewriterComponent from "typewriter-effect"; 4 | import Link from "next/link"; 5 | import { useAuth } from "@clerk/nextjs"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | 9 | export const LandingHero = () => { 10 | const { isSignedIn } = useAuth(); 11 | 12 | return ( 13 |
14 |
15 |

The Best AI Tool for

16 |
17 | 29 |
30 |
31 |
32 | Create content using AI 10x faster. 33 |
34 |
35 | 36 | 39 | 40 |
41 |
42 | No credit card required. 43 |
44 |
45 | ); 46 | }; -------------------------------------------------------------------------------- /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 { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | 12 | const openai = new OpenAIApi(configuration); 13 | 14 | export async function POST( 15 | req: Request 16 | ) { 17 | try { 18 | const { userId } = auth(); 19 | const body = await req.json(); 20 | const { messages } = body; 21 | 22 | if (!userId) { 23 | return new NextResponse("Unauthorized", { status: 401 }); 24 | } 25 | 26 | if (!configuration.apiKey) { 27 | return new NextResponse("OpenAI API Key not configured.", { status: 500 }); 28 | } 29 | 30 | if (!messages) { 31 | return new NextResponse("Messages are required", { status: 400 }); 32 | } 33 | 34 | const freeTrial = await checkApiLimit(); 35 | const isPro = await checkSubscription(); 36 | 37 | if (!freeTrial && !isPro) { 38 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 39 | } 40 | 41 | const response = await openai.createChatCompletion({ 42 | model: "gpt-3.5-turbo", 43 | messages 44 | }); 45 | 46 | if (!isPro) { 47 | await incrementApiLimit(); 48 | } 49 | 50 | return NextResponse.json(response.data.choices[0].message); 51 | } catch (error) { 52 | console.log('[CONVERSATION_ERROR]', error); 53 | return new NextResponse("Internal Error", { status: 500 }); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { ArrowRight } from "lucide-react"; 3 | import { tools } from "@/constant"; 4 | import { Card } from "@/components/ui/card"; 5 | import { cn } from "@/lib/utils"; 6 | import { useRouter } from "next/navigation"; 7 | export default function Dashboard() { 8 | const router = useRouter() 9 | return ( 10 |
11 |
12 |

13 | Explore the power of AI 14 |

15 |

16 | Chat with the smartest AI - Experience the power of AI 17 |

18 |
19 |
20 | {tools.map((tool) => ( 21 | router.push(tool.href)} key={tool.href} className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer"> 22 |
23 |
24 | 25 |
26 |
27 | {tool.label} 28 |
29 |
30 | 31 |
32 | ))} 33 |
34 |
35 | 36 | ) 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "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.24.1", 14 | "@hookform/resolvers": "^3.3.1", 15 | "@prisma/client": "^5.4.1", 16 | "@radix-ui/react-avatar": "^1.0.4", 17 | "@radix-ui/react-dialog": "^1.0.4", 18 | "@radix-ui/react-dropdown-menu": "^2.0.6", 19 | "@radix-ui/react-label": "^2.0.2", 20 | "@radix-ui/react-progress": "^1.0.3", 21 | "@radix-ui/react-select": "^2.0.0", 22 | "@radix-ui/react-separator": "^1.0.3", 23 | "@radix-ui/react-slot": "^1.0.2", 24 | "@types/node": "20.6.5", 25 | "@types/react": "18.2.22", 26 | "@types/react-dom": "18.2.7", 27 | "autoprefixer": "10.4.16", 28 | "axios": "^1.5.1", 29 | "class-variance-authority": "^0.7.0", 30 | "clsx": "^2.0.0", 31 | "cmdk": "^0.2.0", 32 | "crisp-sdk-web": "^1.0.21", 33 | "eslint": "8.50.0", 34 | "eslint-config-next": "13.5.2", 35 | "lucide-react": "^0.279.0", 36 | "next": "13.5.2", 37 | "next-themes": "^0.2.1", 38 | "openai": "3.3.0", 39 | "postcss": "8.4.30", 40 | "prisma": "^5.4.1", 41 | "react": "18.2.0", 42 | "react-dom": "18.2.0", 43 | "react-hook-form": "^7.46.2", 44 | "react-hot-toast": "^2.4.1", 45 | "react-markdown": "^8.0.7", 46 | "replicate": "^0.18.1", 47 | "stripe": "^13.8.0", 48 | "tailwind-merge": "^1.14.0", 49 | "tailwindcss": "3.3.3", 50 | "tailwindcss-animate": "^1.0.7", 51 | "typescript": "5.2.2", 52 | "typewriter-effect": "^2.21.0", 53 | "zod": "^3.22.2", 54 | "zustand": "^4.4.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html,body { 6 | height:100%; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 222.2 84% 4.9%; 13 | 14 | --card: 0 0% 100%; 15 | --card-foreground: 222.2 84% 4.9%; 16 | 17 | --popover: 0 0% 100%; 18 | --popover-foreground: 222.2 84% 4.9%; 19 | 20 | --primary: 222.2 47.4% 11.2%; 21 | --primary-foreground: 210 40% 98%; 22 | 23 | --secondary: 210 40% 96.1%; 24 | --secondary-foreground: 222.2 47.4% 11.2%; 25 | 26 | --muted: 210 40% 96.1%; 27 | --muted-foreground: 215.4 16.3% 46.9%; 28 | 29 | --accent: 210 40% 96.1%; 30 | --accent-foreground: 222.2 47.4% 11.2%; 31 | 32 | --destructive: 0 84.2% 60.2%; 33 | --destructive-foreground: 210 40% 98%; 34 | 35 | --border: 214.3 31.8% 91.4%; 36 | --input: 214.3 31.8% 91.4%; 37 | --ring: 222.2 84% 4.9%; 38 | 39 | --radius: 0.5rem; 40 | } 41 | 42 | .dark { 43 | --background: 222.2 84% 4.9%; 44 | --foreground: 210 40% 98%; 45 | 46 | --card: 222.2 84% 4.9%; 47 | --card-foreground: 210 40% 98%; 48 | 49 | --popover: 222.2 84% 4.9%; 50 | --popover-foreground: 210 40% 98%; 51 | 52 | --primary: 210 40% 98%; 53 | --primary-foreground: 222.2 47.4% 11.2%; 54 | 55 | --secondary: 217.2 32.6% 17.5%; 56 | --secondary-foreground: 210 40% 98%; 57 | 58 | --muted: 217.2 32.6% 17.5%; 59 | --muted-foreground: 215 20.2% 65.1%; 60 | 61 | --accent: 217.2 32.6% 17.5%; 62 | --accent-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --border: 217.2 32.6% 17.5%; 68 | --input: 217.2 32.6% 17.5%; 69 | --ring: 212.7 26.8% 83.9%; 70 | } 71 | } 72 | 73 | @layer base { 74 | * { 75 | @apply border-border; 76 | } 77 | body { 78 | @apply bg-background text-foreground; 79 | } 80 | } -------------------------------------------------------------------------------- /app/api/image/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | import { Configuration, OpenAIApi } from "openai"; 4 | 5 | import { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | 12 | const openai = new OpenAIApi(configuration); 13 | 14 | export async function POST( 15 | req: Request 16 | ) { 17 | try { 18 | const { userId } = auth(); 19 | const body = await req.json(); 20 | const { prompt, amount = 1, resolution = "512x512" } = body; 21 | 22 | if (!userId) { 23 | return new NextResponse("Unauthorized", { status: 401 }); 24 | } 25 | 26 | if (!configuration.apiKey) { 27 | return new NextResponse("OpenAI API Key not configured.", { status: 500 }); 28 | } 29 | 30 | if (!prompt) { 31 | return new NextResponse("Prompt is required", { status: 400 }); 32 | } 33 | 34 | if (!amount) { 35 | return new NextResponse("Amount is required", { status: 400 }); 36 | } 37 | 38 | if (!resolution) { 39 | return new NextResponse("Resolution is required", { status: 400 }); 40 | } 41 | 42 | const freeTrial = await checkApiLimit(); 43 | const isPro = await checkSubscription(); 44 | 45 | if (!freeTrial && !isPro) { 46 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 47 | } 48 | 49 | const response = await openai.createImage({ 50 | prompt, 51 | n: parseInt(amount, 10), 52 | size: resolution, 53 | }); 54 | 55 | if (!isPro) { 56 | await incrementApiLimit(); 57 | } 58 | 59 | return NextResponse.json(response.data.data); 60 | } catch (error) { 61 | console.log('[IMAGE_ERROR]', error); 62 | return new NextResponse("Internal Error", { status: 500 }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /app/api/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 { checkSubscription } from "@/lib/subscription"; 6 | import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit"; 7 | 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | 12 | const openai = new OpenAIApi(configuration); 13 | 14 | const instructionMessage: ChatCompletionRequestMessage = { 15 | role: "system", 16 | content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations." 17 | }; 18 | 19 | export async function POST( 20 | req: Request 21 | ) { 22 | try { 23 | const { userId } = auth(); 24 | const body = await req.json(); 25 | const { messages } = body; 26 | 27 | if (!userId) { 28 | return new NextResponse("Unauthorized", { status: 401 }); 29 | } 30 | 31 | if (!configuration.apiKey) { 32 | return new NextResponse("OpenAI API Key not configured.", { status: 500 }); 33 | } 34 | 35 | if (!messages) { 36 | return new NextResponse("Messages are required", { status: 400 }); 37 | } 38 | 39 | const freeTrial = await checkApiLimit(); 40 | const isPro = await checkSubscription(); 41 | 42 | if (!freeTrial && !isPro) { 43 | return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 }); 44 | } 45 | 46 | const response = await openai.createChatCompletion({ 47 | model: "gpt-3.5-turbo", 48 | messages: [instructionMessage, ...messages] 49 | }); 50 | 51 | if (!isPro) { 52 | await incrementApiLimit(); 53 | } 54 | 55 | return NextResponse.json(response.data.choices[0].message); 56 | } catch (error) { 57 | console.log('[CODE_ERROR]', error); 58 | return new NextResponse("Internal Error", { status: 500 }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /app/api/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, currentUser } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import { stripe } from "@/lib/stripe"; 6 | import { absoluteUrl } from "@/lib/utils"; 7 | 8 | const settingsUrl = absoluteUrl("/settings"); 9 | 10 | export async function GET() { 11 | try { 12 | const { userId } = auth(); 13 | const user = await currentUser(); 14 | 15 | if (!userId || !user) { 16 | return new NextResponse("Unauthorized", { status: 401 }); 17 | } 18 | 19 | const userSubscription = await prismadb.userSubscription.findUnique({ 20 | where: { 21 | userId 22 | } 23 | }) 24 | 25 | if (userSubscription && userSubscription.stripeCustomerId) { 26 | const stripeSession = await stripe.billingPortal.sessions.create({ 27 | customer: userSubscription.stripeCustomerId, 28 | return_url: settingsUrl, 29 | }) 30 | 31 | return new NextResponse(JSON.stringify({ url: stripeSession.url })) 32 | } 33 | 34 | const stripeSession = await stripe.checkout.sessions.create({ 35 | success_url: settingsUrl, 36 | cancel_url: settingsUrl, 37 | payment_method_types: ["card"], 38 | mode: "subscription", 39 | billing_address_collection: "auto", 40 | customer_email: user.emailAddresses[0].emailAddress, 41 | line_items: [ 42 | { 43 | price_data: { 44 | currency: "USD", 45 | product_data: { 46 | name: "Genius Pro", 47 | description: "Unlimited AI Generations" 48 | }, 49 | unit_amount: 2000, 50 | recurring: { 51 | interval: "month" 52 | } 53 | }, 54 | quantity: 1, 55 | }, 56 | ], 57 | metadata: { 58 | userId, 59 | }, 60 | }) 61 | 62 | return new NextResponse(JSON.stringify({ url: stripeSession.url })) 63 | } catch (error) { 64 | console.log("[STRIPE_ERROR]", error); 65 | return new NextResponse("Internal Error", { status: 500 }); 66 | } 67 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /components/pro-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Check, Zap } from "lucide-react"; 5 | import { toast } from "react-hot-toast"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogHeader, 12 | DialogTitle, 13 | DialogDescription, 14 | DialogFooter 15 | } from "@/components/ui/dialog"; 16 | import { Badge } from "@/components/ui/badge"; 17 | import { Button } from "@/components/ui/button"; 18 | import { useProModal } from "@/hooks/use-pro-modal"; 19 | import { tools } from "@/constant"; 20 | import { Card } from "@/components/ui/card"; 21 | import { cn } from "@/lib/utils"; 22 | 23 | export const ProModal = () => { 24 | const proModal = useProModal(); 25 | const [loading, setLoading] = useState(false); 26 | 27 | const onSubscribe = async () => { 28 | try { 29 | setLoading(true); 30 | const response = await axios.get("/api/stripe"); 31 | 32 | window.location.href = response.data.url; 33 | } catch (error) { 34 | toast.error("Something went wrong"); 35 | } finally { 36 | setLoading(false); 37 | } 38 | } 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 |
46 | Upgrade to Genius 47 | 48 | pro 49 | 50 |
51 |
52 | 53 | {tools.map((tool) => ( 54 | 55 |
56 |
57 | 58 |
59 |
60 | {tool.label} 61 |
62 |
63 | 64 |
65 | ))} 66 |
67 |
68 | 69 | 73 | 74 |
75 |
76 | ); 77 | }; -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { Montserrat } from 'next/font/google' 6 | import { Code, ImageIcon, LayoutDashboard, MessageSquare, Music, Settings, VideoIcon } from "lucide-react"; 7 | import { usePathname } from "next/navigation"; 8 | 9 | import { cn } from "@/lib/utils"; 10 | import { FreeCounter } from "@/components/free-counter"; 11 | 12 | const poppins = Montserrat({ weight: '600', subsets: ['latin'] }); 13 | 14 | const routes = [ 15 | { 16 | label: 'Dashboard', 17 | icon: LayoutDashboard, 18 | href: '/dashboard', 19 | color: "text-sky-500" 20 | }, 21 | { 22 | label: 'Conversation', 23 | icon: MessageSquare, 24 | href: '/conversation', 25 | color: "text-violet-500", 26 | }, 27 | { 28 | label: 'Image Generation', 29 | icon: ImageIcon, 30 | color: "text-pink-700", 31 | href: '/image', 32 | }, 33 | { 34 | label: 'Video Generation', 35 | icon: VideoIcon, 36 | color: "text-orange-700", 37 | href: '/video', 38 | }, 39 | { 40 | label: 'Music Generation', 41 | icon: Music, 42 | color: "text-emerald-500", 43 | href: '/music', 44 | }, 45 | { 46 | label: 'Code Generation', 47 | icon: Code, 48 | color: "text-green-700", 49 | href: '/code', 50 | }, 51 | { 52 | label: 'Settings', 53 | icon: Settings, 54 | href: '/settings', 55 | }, 56 | ]; 57 | 58 | const Sidebar = ({ 59 | apiLimitCount = 0, 60 | isPro = false 61 | }: { 62 | apiLimitCount: number; 63 | isPro: boolean; 64 | }) => { 65 | const pathname = usePathname(); 66 | 67 | return ( 68 |
69 |
70 | 71 |
72 | Logo 73 |
74 |

75 | Genius 76 |

77 | 78 |
79 | {routes.map((route) => ( 80 | 88 |
89 | 90 | {route.label} 91 |
92 | 93 | ))} 94 |
95 |
96 | 100 |
101 | ); 102 | }; 103 | 104 | export default Sidebar; -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/music/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { useState } from "react"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { useForm } from "react-hook-form"; 8 | import { toast } from "react-hot-toast"; 9 | import { useRouter } from "next/navigation"; 10 | import { Music, Send } from "lucide-react"; 11 | 12 | import { Heading } from "@/components/heading"; 13 | import { Button } from "@/components/ui/button"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 16 | import { Loader } from "@/components/loader"; 17 | import Empty from "@/components/empty"; 18 | 19 | import { formSchema } from "./constant"; 20 | import { useProModal } from "@/hooks/use-pro-modal"; 21 | 22 | const MusicPage = () => { 23 | const proModal = useProModal(); 24 | const router = useRouter(); 25 | const [music, setMusic] = useState(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(formSchema), 29 | defaultValues: { 30 | prompt: "", 31 | } 32 | }); 33 | 34 | const isLoading = form.formState.isSubmitting; 35 | 36 | const onSubmit = async (values: z.infer) => { 37 | try { 38 | setMusic(undefined); 39 | 40 | const response = await axios.post('/api/music', values); 41 | console.log(response) 42 | 43 | setMusic(response.data.audio); 44 | form.reset(); 45 | } catch (error: any) { 46 | if (error?.response?.status === 403) { 47 | proModal.onOpen(); 48 | } 49 | else { 50 | toast.error("Something went wrong."); 51 | } 52 | } finally { 53 | router.refresh(); 54 | } 55 | } 56 | 57 | return ( 58 |
59 | 66 |
67 |
68 | 83 | ( 86 | 87 | 88 | 94 | 95 | 96 | )} 97 | /> 98 | 101 | 102 | 103 | {isLoading && ( 104 |
105 | 106 |
107 | )} 108 | {!music && !isLoading && ( 109 | 110 | )} 111 | {music && ( 112 | 115 | )} 116 |
117 |
118 | ); 119 | } 120 | 121 | export default MusicPage; -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/video/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { useState } from "react"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { useForm } from "react-hook-form"; 8 | import { toast } from "react-hot-toast"; 9 | import { FileAudio } from "lucide-react"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | import { Heading } from "@/components/heading"; 13 | import { Button } from "@/components/ui/button"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; 16 | import { Loader } from "@/components/loader"; 17 | import Empty from "@/components/empty"; 18 | 19 | import { formSchema } from "./constant"; 20 | import { useProModal } from "@/hooks/use-pro-modal"; 21 | 22 | const VideoPage = () => { 23 | const proModal = useProModal(); 24 | const router = useRouter(); 25 | const [video, setVideo] = useState(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(formSchema), 29 | defaultValues: { 30 | prompt: "", 31 | } 32 | }); 33 | 34 | const isLoading = form.formState.isSubmitting; 35 | 36 | const onSubmit = async (values: z.infer) => { 37 | try { 38 | setVideo(undefined); 39 | 40 | const response = await axios.post('/api/video', values); 41 | 42 | setVideo(response.data[0]); 43 | form.reset(); 44 | } catch (error: any) { 45 | if (error?.response?.status === 403) { 46 | proModal.onOpen(); 47 | } else { 48 | toast.error("Something went wrong."); 49 | } 50 | } finally { 51 | router.refresh(); 52 | } 53 | } 54 | 55 | return ( 56 |
57 | 64 |
65 |
66 | 81 | ( 84 | 85 | 86 | 92 | 93 | 94 | )} 95 | /> 96 | 99 | 100 | 101 | {isLoading && ( 102 |
103 | 104 |
105 | )} 106 | {!video && !isLoading && ( 107 | 108 | )} 109 | {video && ( 110 | 113 | )} 114 |
115 |
116 | ); 117 | } 118 | 119 | export default VideoPage; -------------------------------------------------------------------------------- /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/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/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 |