diff --git a/.env.semple b/.env.semple
new file mode 100644
index 0000000..fd8eecc
--- /dev/null
+++ b/.env.semple
@@ -0,0 +1,17 @@
+NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
+CLERK_SECRET_KEY=
+
+NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
+NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
+NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
+NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
+
+OPENAI_API_KEY=
+REPLICATE_API_TOKEN=
+
+DATABASE_URL=
+
+STRIPE_API_KEY=
+STRIPE_WEBHOOK_SECRET=
+
+NEXT_PUBLIC_APP_URL="http://localhost:3000"
\ No newline at end of file
diff --git a/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx
index 2743945..2cc13d4 100644
--- a/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx
+++ b/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx
@@ -1,5 +1,5 @@
-import { SignUp } from "@clerk/nextjs";
+import { SignIn } from "@clerk/nextjs";
export default function Page() {
- return ;
+ return ;
}
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
new file mode 100644
index 0000000..fe4ed80
--- /dev/null
+++ b/app/(dashboard)/layout.tsx
@@ -0,0 +1,26 @@
+import Navbar from "@/components/navbar";
+import { Sidebar } from "@/components/sidebar";
+import { getApiLimitCount } from "@/lib/api-limit";
+import { checkSubscription } from "@/lib/subscription";
+import { ReactNode } from "react";
+
+export default async function DashboardLayout({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ const apiLimitCount = await getApiLimitCount();
+ const isPro = await checkSubscription();
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/hooks/use-pro-modal.ts b/app/hooks/use-pro-modal.ts
new file mode 100644
index 0000000..26bd850
--- /dev/null
+++ b/app/hooks/use-pro-modal.ts
@@ -0,0 +1,13 @@
+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 }),
+}));
diff --git a/components/free-counter.tsx b/components/free-counter.tsx
new file mode 100644
index 0000000..2c445af
--- /dev/null
+++ b/components/free-counter.tsx
@@ -0,0 +1,57 @@
+import { Zap } from "lucide-react";
+import { useEffect, useState } from "react";
+
+import { useProModal } from "@/app/hooks/use-pro-modal";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { MAX_FREE_COUNTS } from "@/constants";
+
+export const FreeCounter = ({
+ isPro = false,
+ apiLimitCount = 0,
+}: {
+ isPro: boolean;
+ apiLimitCount: number;
+}) => {
+ 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
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/mobile-sidebar.tsx b/components/mobile-sidebar.tsx
new file mode 100644
index 0000000..399f93a
--- /dev/null
+++ b/components/mobile-sidebar.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { Menu } from "lucide-react";
+import { useEffect, useState } from "react";
+
+import { Sidebar } from "@/components/sidebar";
+import { Button } from "@/components/ui/button";
+import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
+
+export const MobileSidebar = ({
+ apiLimitCount = 0,
+ isPro = false,
+}: {
+ apiLimitCount: number;
+ isPro: boolean;
+}) => {
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/navbar.tsx b/components/navbar.tsx
new file mode 100644
index 0000000..0c0cddb
--- /dev/null
+++ b/components/navbar.tsx
@@ -0,0 +1,19 @@
+import { UserButton } from "@clerk/nextjs";
+
+import { checkSubscription } from "@/lib/subscription";
+import { getApiLimitCount } from "../lib/api-limit";
+import { MobileSidebar } from "./mobile-sidebar";
+
+export default async function Navbar() {
+ const apiLimitCount = await getApiLimitCount();
+ const isPro = await checkSubscription();
+
+ return (
+
+ );
+}
diff --git a/components/pro-modal.tsx b/components/pro-modal.tsx
new file mode 100644
index 0000000..0ab10c0
--- /dev/null
+++ b/components/pro-modal.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import axios from "axios";
+import { Check, Zap } from "lucide-react";
+import { useState } from "react";
+import { toast } from "react-hot-toast";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { tools } from "@/constants";
+// import { useProModal } from "@/hooks/use-pro-modal";
+import { cn } from "@/lib/utils";
+import { useProModal } from "../hooks/use-pro-modal";
+
+export const ProModal = () => {
+ const proModal = useProModal();
+ const [loading, setLoading] = useState(false);
+
+ const onSubscribe = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get("/api/stripe");
+
+ window.location.href = response.data.url;
+ } catch (error) {
+ toast.error("Something went wrong");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/components/sidebar.tsx b/components/sidebar.tsx
new file mode 100644
index 0000000..431e66a
--- /dev/null
+++ b/components/sidebar.tsx
@@ -0,0 +1,109 @@
+"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 { cn } from "../lib/utils";
+import { FreeCounter } from "./free-counter";
+
+const poppins = 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,
+ color: "text-pink-700",
+ href: "/image",
+ },
+ {
+ label: "Video Generation",
+ icon: VideoIcon,
+ color: "text-orange-700",
+ href: "/video",
+ },
+ {
+ label: "Music Generation",
+ icon: Music,
+ color: "text-emerald-500",
+ href: "/music",
+ },
+ {
+ label: "Code Generation",
+ icon: Code,
+ color: "text-green-700",
+ href: "/code",
+ },
+ {
+ label: "Settings",
+ icon: Settings,
+ href: "/settings",
+ },
+];
+
+export const Sidebar = ({
+ apiLimitCount = 0,
+ isPro = false,
+}: {
+ apiLimitCount: number;
+ isPro: boolean;
+}) => {
+ const pathname = usePathname();
+
+ return (
+
+
+
+
+
+
+
+ Genius
+
+
+
+ {routes.map((route) => (
+
+
+
+ {route.label}
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..6f6f78f
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,38 @@
+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 };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 0ba4277..b2aca12 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+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"
+import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@@ -18,6 +18,8 @@ const buttonVariants = cva(
"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",
@@ -31,26 +33,26 @@ const buttonVariants = cva(
size: "default",
},
}
-)
+);
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
- asChild?: boolean
+ asChild?: boolean;
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot : "button";
return (
- )
+ );
}
-)
-Button.displayName = "Button"
+);
+Button.displayName = "Button";
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..afa13ec
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,79 @@
+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 }
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000..01ff19c
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"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 = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+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,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx
new file mode 100644
index 0000000..5c87ea4
--- /dev/null
+++ b/components/ui/progress.tsx
@@ -0,0 +1,28 @@
+"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 }
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
new file mode 100644
index 0000000..a37f17b
--- /dev/null
+++ b/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"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 = SheetPrimitive.Portal
+
+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,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/constants.ts b/constants.ts
new file mode 100644
index 0000000..61c71e7
--- /dev/null
+++ b/constants.ts
@@ -0,0 +1,41 @@
+import { Code, ImageIcon, MessageSquare, Music, VideoIcon } from "lucide-react";
+
+export const MAX_FREE_COUNTS = 5;
+
+export const tools = [
+ {
+ label: "Conversation",
+ icon: MessageSquare,
+ href: "/conversation",
+ color: "text-violet-500",
+ bgColor: "bg-violet-500/10",
+ },
+ {
+ label: "Music Generation",
+ icon: Music,
+ href: "/music",
+ color: "text-emerald-500",
+ bgColor: "bg-emerald-500/10",
+ },
+ {
+ 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",
+ },
+];
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..b61a5d7
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,12 @@
+# Use root/example as user/password credentials
+version: "3.1"
+
+services:
+ db:
+ image: mysql
+ command: --default-authentication-plugin=mysql_native_password
+ restart: always
+ ports:
+ - "3306:3306"
+ environment:
+ MYSQL_ROOT_PASSWORD: example
diff --git a/lib/api-limit.ts b/lib/api-limit.ts
new file mode 100644
index 0000000..99ba61b
--- /dev/null
+++ b/lib/api-limit.ts
@@ -0,0 +1,65 @@
+import { auth } from "@clerk/nextjs";
+
+import { MAX_FREE_COUNTS } from "@/constants";
+import prismadb from "@/lib/prismadb";
+
+export const incrementApiLimit = async () => {
+ const { userId } = auth();
+
+ if (!userId) {
+ return;
+ }
+
+ const userApiLimit = await prismadb.userApiLimit.findUnique({
+ where: { userId: userId },
+ });
+
+ if (userApiLimit) {
+ await prismadb.userApiLimit.update({
+ where: { userId: userId },
+ data: { count: userApiLimit.count + 1 },
+ });
+ } else {
+ await prismadb.userApiLimit.create({
+ data: { userId: userId, count: 1 },
+ });
+ }
+};
+
+export const checkApiLimit = async () => {
+ const { userId } = auth();
+
+ if (!userId) {
+ return false;
+ }
+
+ const userApiLimit = await prismadb.userApiLimit.findUnique({
+ where: { userId: 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;
+ }
+
+ return userApiLimit.count;
+};
diff --git a/lib/prismadb.ts b/lib/prismadb.ts
new file mode 100644
index 0000000..70456bf
--- /dev/null
+++ b/lib/prismadb.ts
@@ -0,0 +1,10 @@
+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;
diff --git a/lib/subscription.ts b/lib/subscription.ts
new file mode 100644
index 0000000..7591bb3
--- /dev/null
+++ b/lib/subscription.ts
@@ -0,0 +1,36 @@
+import { auth } from "@clerk/nextjs";
+
+import prismadb from "@/lib/prismadb";
+
+const DAY_IN_MS = 86_400_000;
+
+export const checkSubscription = async () => {
+ const { userId } = auth();
+
+ if (!userId) {
+ return false;
+ }
+
+ const userSubscription = await prismadb.userSubscription.findUnique({
+ where: {
+ userId: 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;
+};
diff --git a/lib/utils.ts b/lib/utils.ts
index d084cca..e43b884 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,6 +1,10 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+export function absoluteUrl(path: string) {
+ return `${process.env.NEXT_PUBLIC_APP_URL}${path}`;
}
diff --git a/package-lock.json b/package-lock.json
index 04d05b8..d842cd7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,12 @@
"version": "0.1.0",
"dependencies": {
"@clerk/nextjs": "^4.29.8",
+ "@hookform/resolvers": "^3.3.4",
+ "@prisma/client": "^5.10.2",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
+ "axios": "^1.6.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.341.0",
@@ -17,9 +22,12 @@
"next-intl": "^3.9.1",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.51.0",
+ "react-hot-toast": "^2.4.1",
"server-only": "^0.0.1",
"tailwind-merge": "^2.2.1",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20",
@@ -29,6 +37,7 @@
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
+ "prisma": "^5.10.2",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
@@ -353,6 +362,14 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
+ "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -696,6 +713,76 @@
"node": ">=14"
}
},
+ "node_modules/@prisma/client": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.10.2.tgz",
+ "integrity": "sha512-ef49hzB2yJZCvM5gFHMxSFL9KYrIP9udpT5rYo0CsHD4P9IKj473MbhU1gjKKftiwWBTIyrt9jukprzZXazyag==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=16.13"
+ },
+ "peerDependencies": {
+ "prisma": "*"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.2.tgz",
+ "integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==",
+ "devOptional": true
+ },
+ "node_modules/@prisma/engines": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.2.tgz",
+ "integrity": "sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/debug": "5.10.2",
+ "@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9",
+ "@prisma/fetch-engine": "5.10.2",
+ "@prisma/get-platform": "5.10.2"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz",
+ "integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==",
+ "devOptional": true
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz",
+ "integrity": "sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg==",
+ "devOptional": true,
+ "dependencies": {
+ "@prisma/debug": "5.10.2",
+ "@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9",
+ "@prisma/get-platform": "5.10.2"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.2.tgz",
+ "integrity": "sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg==",
+ "devOptional": true,
+ "dependencies": {
+ "@prisma/debug": "5.10.2"
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
+ "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ }
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
@@ -713,6 +800,240 @@
}
}
},
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
+ "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
+ "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-focus-guards": "1.0.1",
+ "@radix-ui/react-focus-scope": "1.0.4",
+ "@radix-ui/react-id": "1.0.1",
+ "@radix-ui/react-portal": "1.0.4",
+ "@radix-ui/react-presence": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-slot": "1.0.2",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.5.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
+ "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-escape-keydown": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
+ "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
+ "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
+ "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-use-layout-effect": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
+ "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
+ "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
+ "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-slot": "1.0.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz",
+ "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
@@ -731,6 +1052,76 @@
}
}
},
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
+ "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
+ "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
+ "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
+ "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rushstack/eslint-patch": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz",
@@ -864,7 +1255,7 @@
"version": "18.2.19",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz",
"integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"@types/react": "*"
}
@@ -1114,6 +1505,17 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
+ "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -1370,6 +1772,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.4",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -1693,8 +2118,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -1784,6 +2208,11 @@
"node": ">=6"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2546,6 +2975,25 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
+ "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -2669,6 +3117,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-symbol-description": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -2807,6 +3263,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/goober": {
+ "version": "2.1.14",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz",
+ "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -3002,6 +3466,14 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@@ -4264,6 +4736,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prisma": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.10.2.tgz",
+ "integrity": "sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/engines": "5.10.2"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=16.13"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4275,6 +4763,11 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4350,12 +4843,109 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.51.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.0.tgz",
+ "integrity": "sha512-BggOy5j58RdhdMzzRUHGOYhSz1oeylFAv6jUSG86OvCIvlAvS7KvnRY7yoAf2pfEiPN7BesnR0xx73nEk3qIiw==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-hot-toast": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+ "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
+ "dependencies": {
+ "goober": "^2.1.10"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
+ "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.3",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz",
+ "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -5303,6 +5893,26 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz",
+ "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-intl": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.9.1.tgz",
@@ -5315,6 +5925,27 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -5551,6 +6182,33 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz",
+ "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 85ed735..937ded7 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,12 @@
},
"dependencies": {
"@clerk/nextjs": "^4.29.8",
+ "@hookform/resolvers": "^3.3.4",
+ "@prisma/client": "^5.10.2",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
+ "axios": "^1.6.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.341.0",
@@ -18,9 +23,12 @@
"next-intl": "^3.9.1",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.51.0",
+ "react-hot-toast": "^2.4.1",
"server-only": "^0.0.1",
"tailwind-merge": "^2.2.1",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20",
@@ -30,6 +38,7 @@
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
+ "prisma": "^5.10.2",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
diff --git a/prisma/migrations/20240305164355_init/migration.sql b/prisma/migrations/20240305164355_init/migration.sql
new file mode 100644
index 0000000..da1fbc0
--- /dev/null
+++ b/prisma/migrations/20240305164355_init/migration.sql
@@ -0,0 +1,26 @@
+-- CreateTable
+CREATE TABLE `UserApiLimit` (
+ `id` VARCHAR(191) NOT NULL,
+ `userId` VARCHAR(191) NOT NULL,
+ `count` INTEGER NOT NULL DEFAULT 0,
+ `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ `updatedAt` DATETIME(3) NOT NULL,
+
+ UNIQUE INDEX `UserApiLimit_userId_key`(`userId`),
+ PRIMARY KEY (`id`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- CreateTable
+CREATE TABLE `UserSubscription` (
+ `id` VARCHAR(191) NOT NULL,
+ `userId` VARCHAR(191) NOT NULL,
+ `stripe_customer_id` VARCHAR(191) NULL,
+ `stripe_subscription_id` VARCHAR(191) NULL,
+ `stripe_price_id` VARCHAR(191) NULL,
+ `stripe_current_period_end` DATETIME(3) NULL,
+
+ UNIQUE INDEX `UserSubscription_userId_key`(`userId`),
+ UNIQUE INDEX `UserSubscription_stripe_customer_id_key`(`stripe_customer_id`),
+ UNIQUE INDEX `UserSubscription_stripe_subscription_id_key`(`stripe_subscription_id`),
+ PRIMARY KEY (`id`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..e5a788a
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "mysql"
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..11403ae
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,26 @@
+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? @map(name: "stripe_price_id")
+ stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
+}
\ No newline at end of file
diff --git a/public/chat.png b/public/chat.png
new file mode 100644
index 0000000..3e992b0
Binary files /dev/null and b/public/chat.png differ
diff --git a/public/empty.png b/public/empty.png
new file mode 100644
index 0000000..6127b77
Binary files /dev/null and b/public/empty.png differ
diff --git a/public/home.png b/public/home.png
new file mode 100644
index 0000000..3846dd5
Binary files /dev/null and b/public/home.png differ
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000..afc0f05
Binary files /dev/null and b/public/logo.png differ
diff --git a/public/mail.png b/public/mail.png
new file mode 100644
index 0000000..02eefc9
Binary files /dev/null and b/public/mail.png differ
diff --git a/public/next (1).svg b/public/next (1).svg
new file mode 100644
index 0000000..5174b28
--- /dev/null
+++ b/public/next (1).svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/photo.png b/public/photo.png
new file mode 100644
index 0000000..1d8065a
Binary files /dev/null and b/public/photo.png differ
diff --git a/public/pro.png b/public/pro.png
new file mode 100644
index 0000000..ddef4f4
Binary files /dev/null and b/public/pro.png differ
diff --git a/public/transcript.png b/public/transcript.png
new file mode 100644
index 0000000..47b97a7
Binary files /dev/null and b/public/transcript.png differ
diff --git a/public/vercel (1).svg b/public/vercel (1).svg
new file mode 100644
index 0000000..d2f8422
--- /dev/null
+++ b/public/vercel (1).svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/voice.png b/public/voice.png
new file mode 100644
index 0000000..c81a6eb
Binary files /dev/null and b/public/voice.png differ