├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── README.md ├── app.vue ├── assets └── css │ └── tailwind.css ├── components.json ├── components ├── BotAvatar.vue ├── Empty.vue ├── Heading.vue ├── Loader.vue ├── MobileSidebar.vue ├── Navbar.vue ├── ProModal.vue ├── Sidebar.vue ├── SubscriptionButton.vue ├── UserAvatar.vue └── ui │ ├── avatar │ ├── Avatar.vue │ ├── AvatarFallback.vue │ ├── AvatarImage.vue │ └── index.ts │ ├── badge │ ├── Badge.vue │ └── index.ts │ ├── button │ ├── Button.vue │ └── index.ts │ ├── card │ ├── Card.vue │ ├── CardContent.vue │ ├── CardDescription.vue │ ├── CardFooter.vue │ ├── CardHeader.vue │ ├── CardTitle.vue │ └── index.ts │ ├── dialog │ ├── Dialog.vue │ ├── DialogContent.vue │ ├── DialogDescription.vue │ ├── DialogFooter.vue │ ├── DialogHeader.vue │ ├── DialogTitle.vue │ ├── DialogTrigger.vue │ └── index.ts │ ├── dropdown-menu │ ├── DropdownMenu.vue │ ├── DropdownMenuCheckboxItem.vue │ ├── DropdownMenuContent.vue │ ├── DropdownMenuGroup.vue │ ├── DropdownMenuItem.vue │ ├── DropdownMenuLabel.vue │ ├── DropdownMenuRadioGroup.vue │ ├── DropdownMenuRadioItem.vue │ ├── DropdownMenuSeparator.vue │ ├── DropdownMenuShortcut.vue │ ├── DropdownMenuSub.vue │ ├── DropdownMenuSubContent.vue │ ├── DropdownMenuSubTrigger.vue │ ├── DropdownMenuTrigger.vue │ └── index.ts │ ├── progress │ ├── Progress.vue │ └── index.ts │ ├── select │ ├── Select.vue │ ├── SelectContent.vue │ ├── SelectGroup.vue │ ├── SelectItem.vue │ ├── SelectItemText.vue │ ├── SelectLabel.vue │ ├── SelectSeparator.vue │ ├── SelectTrigger.vue │ ├── SelectValue.vue │ └── index.ts │ └── sheet │ ├── Sheet.vue │ ├── SheetClose.vue │ ├── SheetContent.vue │ ├── SheetDescription.vue │ ├── SheetFooter.vue │ ├── SheetHeader.vue │ ├── SheetTitle.vue │ ├── SheetTrigger.vue │ └── index.ts ├── layouts ├── default.vue └── home.vue ├── lib └── utils.ts ├── notes.md ├── nuxt.config.ts ├── package.json ├── pages ├── auth.vue ├── code.vue ├── conversation.vue ├── dashboard.vue ├── image.vue ├── index.vue ├── music.vue ├── settings.vue └── video.vue ├── plugins ├── crisp.client.js └── markdownit.ts ├── prisma └── schema.prisma ├── public ├── empty.png ├── favicon.ico ├── logo.svg ├── logo1.svg ├── logo2.svg ├── logo3.svg ├── logo4.svg └── logo5.svg ├── server ├── api │ ├── code │ │ └── index.post.ts │ ├── conversation │ │ └── index.post.ts │ ├── image │ │ └── index.post.ts │ ├── music │ │ └── index.post.ts │ ├── stripe │ │ ├── checkStatus.ts │ │ ├── index.get.ts │ │ └── webhook.ts │ ├── user │ │ └── index.get.ts │ └── video │ │ └── index.post.ts ├── tsconfig.json ├── types.d.ts └── utils │ └── index.ts ├── store └── useProModal.ts ├── tailwind.config.js ├── tsconfig.json ├── types.d.ts ├── utils └── index.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vue3snippets.enable-compile-vue-file-on-did-save-code": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | Build and Ship Nuxt 3 fullstack starter apps with Auth, DB, Payments, Email & File storage 3 | 4 | Try [Supersaas today](https://dub.sh/4RZanYC) 5 | --- 6 | 7 | 8 | [](https://dub.sh/4RZanYC) 9 | 10 | 11 | --- 12 | Build Faster, Design Better with QuasarUI Components and Templates. 13 | 14 | Try [QuasarUI now](https://www.quasarui.com/) 15 | --- 16 | 17 | 18 | [](https://www.quasarui.com/) 19 | 20 | Build Faster, Design Better with QuasarUI Components and Templates. 21 | 22 | QuasarUI is a collection of templates and over 480 beautifully crafted UI components built with Qusar, aimed at making your development process easier and faster. 23 | 24 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 215.4 16.3% 46.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --border: 214.3 31.8% 91.4%; 20 | --input: 214.3 31.8% 91.4%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 222.2 84% 4.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 222.2 84% 4.9%; 41 | --foreground: 210 40% 98%; 42 | 43 | --muted: 217.2 32.6% 17.5%; 44 | --muted-foreground: 215 20.2% 65.1%; 45 | 46 | --popover: 222.2 84% 4.9%; 47 | --popover-foreground: 210 40% 98%; 48 | 49 | --card: 222.2 84% 4.9%; 50 | --card-foreground: 210 40% 98%; 51 | 52 | --border: 217.2 32.6% 17.5%; 53 | --input: 217.2 32.6% 17.5%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --secondary: 217.2 32.6% 17.5%; 59 | --secondary-foreground: 210 40% 98%; 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 | --ring: 212.7 26.8% 83.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "style": "default", 3 | "typescript": true, 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "assets/css/tailwind.css", 7 | "baseColor": "slate", 8 | "cssVariables": true 9 | }, 10 | "framework": "nuxt", 11 | "aliases": { 12 | "components": "@/components", 13 | "utils": "@/lib/utils" 14 | } 15 | } -------------------------------------------------------------------------------- /components/BotAvatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/Empty.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /components/Heading.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /components/Loader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /components/MobileSidebar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /components/ProModal.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /components/SubscriptionButton.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /components/ui/avatar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /components/ui/avatar/AvatarFallback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/avatar/AvatarImage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority' 2 | 3 | export { default as Avatar } from './Avatar.vue' 4 | export { default as AvatarImage } from './AvatarImage.vue' 5 | export { default as AvatarFallback } from './AvatarFallback.vue' 6 | 7 | export const avatarVariant = cva( 8 | 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', 9 | { 10 | variants: { 11 | size: { 12 | sm: 'h-10 w-10 text-xs', 13 | base: 'h-16 w-16 text-2xl', 14 | lg: 'h-32 w-32 text-5xl', 15 | }, 16 | shape: { 17 | circle: 'rounded-full', 18 | square: 'rounded-md', 19 | }, 20 | }, 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority' 2 | 3 | export { default as Badge } from './Badge.vue' 4 | 5 | export const badgeVariants = cva( 6 | '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', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0" 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: 'default', 22 | }, 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority' 2 | 3 | export { default as Button } from './Button.vue' 4 | 5 | export const buttonVariants = cva( 6 | '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', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 11 | destructive: 12 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 13 | outline: 14 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 15 | secondary: 16 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 17 | ghost: 'hover:bg-accent hover:text-accent-foreground', 18 | link: 'text-primary underline-offset-4 hover:underline', 19 | premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", 20 | }, 21 | size: { 22 | default: 'h-10 px-4 py-2', 23 | sm: 'h-9 rounded-md px-3', 24 | lg: 'h-11 rounded-md px-8', 25 | icon: 'h-10 w-10', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default', 31 | }, 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardHeader } from './CardHeader.vue' 3 | export { default as CardTitle } from './CardTitle.vue' 4 | export { default as CardDescription } from './CardDescription.vue' 5 | export { default as CardContent } from './CardContent.vue' 6 | export { default as CardFooter } from './CardFooter.vue' 7 | -------------------------------------------------------------------------------- /components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogTrigger } from './DialogTrigger.vue' 3 | export { default as DialogHeader } from './DialogHeader.vue' 4 | export { default as DialogTitle } from './DialogTitle.vue' 5 | export { default as DialogDescription } from './DialogDescription.vue' 6 | export { default as DialogContent } from './DialogContent.vue' 7 | export { default as DialogFooter } from './DialogFooter.vue' 8 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuContent.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 37 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuItem.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuRadioItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuSubContent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | export { DropdownMenuPortal } from 'radix-vue' 2 | 3 | export { default as DropdownMenu } from './DropdownMenu.vue' 4 | export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' 5 | export { default as DropdownMenuContent } from './DropdownMenuContent.vue' 6 | export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue' 7 | export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue' 8 | export { default as DropdownMenuItem } from './DropdownMenuItem.vue' 9 | export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue' 10 | export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue' 11 | export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue' 12 | export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue' 13 | export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue' 14 | export { default as DropdownMenuSub } from './DropdownMenuSub.vue' 15 | export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue' 16 | export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue' 17 | -------------------------------------------------------------------------------- /components/ui/progress/Progress.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Progress } from './Progress.vue' 2 | -------------------------------------------------------------------------------- /components/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/ui/select/SelectContent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /components/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /components/ui/select/SelectItem.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /components/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /components/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /components/ui/select/SelectTrigger.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | -------------------------------------------------------------------------------- /components/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select.vue' 2 | export { default as SelectValue } from './SelectValue.vue' 3 | export { default as SelectTrigger } from './SelectTrigger.vue' 4 | export { default as SelectContent } from './SelectContent.vue' 5 | export { default as SelectGroup } from './SelectGroup.vue' 6 | export { default as SelectItem } from './SelectItem.vue' 7 | export { default as SelectItemText } from './SelectItemText.vue' 8 | export { default as SelectLabel } from './SelectLabel.vue' 9 | export { default as SelectSeparator } from './SelectSeparator.vue' 10 | -------------------------------------------------------------------------------- /components/ui/sheet/Sheet.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetContent.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetDescription.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetFooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetTitle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /components/ui/sheet/SheetTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Sheet } from './Sheet.vue' 2 | export { default as SheetTrigger } from './SheetTrigger.vue' 3 | export { default as SheetClose } from './SheetClose.vue' 4 | export { default as SheetContent } from './SheetContent.vue' 5 | export { default as SheetHeader } from './SheetHeader.vue' 6 | export { default as SheetTitle } from './SheetTitle.vue' 7 | export { default as SheetDescription } from './SheetDescription.vue' 8 | export { default as SheetFooter } from './SheetFooter.vue' 9 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /layouts/home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | import { camelize, getCurrentInstance, toHandlerKey } from 'vue' 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | 1. Install and Setup ShadCN 2 | 2. Auth 3 | 3. Basic Layout ( ) 4 | 4. All API modules ( Conversation, Code, Music, Video ) 5 | 5. Prisma Setup ( Supabase ) 6 | 7 | 8 | 1. install prisma: yarn add --dev prisma 9 | 2. npx prisma init 10 | 3. Insert database url to .env file 11 | 4. Create Prisma Schema 12 | 5. npx prisma generate + npx prisma db push 13 | 14 | 6. Do the Api counter ( API + UI ) 15 | 7. Do the Pro Modal ( Dynamic Open ) 16 | 8. Stripe Integration 17 | 18 | 19 | 1. Stripe Keys with url key as well 20 | 2. Stripe Util 21 | 3. Update Prisma 22 | 4. Absolute URL 23 | 4. Stripe Basic API 24 | 5. Stripe Webhook ( API ) 25 | 6. Webhook login and Keys ( Run every time while development ) 26 | 7. User Stripe Status API 27 | 28 | 9. Settings Page ( Integrate the last step API ) (Must activate Customer Portal Link) 29 | 10. Don't show modal or counter or upgrade button in sidebar if we are already pro 30 | 11. Don't throw 403 error when I'm pro while I'm out above 5 (max count) 31 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 2 | export default defineNuxtConfig({ 3 | 4 | modules: ['@nuxtjs/supabase', '@nuxtjs/tailwindcss', 'nuxt-icon', '@pinia/nuxt'], 5 | supabase: { 6 | redirectOptions: { 7 | login: '/auth', 8 | callback: '/confirm', 9 | // Add URLS which don't require any Authentication like landing page 10 | exclude: ['/'], 11 | }, 12 | }, 13 | components: [ 14 | { 15 | path: '~/components/ui', 16 | extensions: ['.vue'], 17 | pathPrefix: false 18 | }, 19 | { 20 | path: '~/components', 21 | extensions: ['.vue'], 22 | pathPrefix: false 23 | }, 24 | ], 25 | runtimeConfig: { 26 | openaiKey: '', 27 | replicateKey: '', 28 | stripeSecret: '', 29 | stripeWebhookSecret: '', 30 | appUrl: '', 31 | public: { 32 | publicStripeKey: '', 33 | } 34 | } 35 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "devDependencies": { 13 | "@nuxt/devtools": "latest", 14 | "@nuxtbase/auth-ui-vue": "^0.3.3", 15 | "@nuxtjs/supabase": "^1.1.2", 16 | "@supabase/auth-ui-shared": "^0.1.7", 17 | "@types/markdown-it": "^13.0.2", 18 | "nuxt": "^3.7.4", 19 | "nuxt-icon": "^0.5.0", 20 | "prisma": "^5.4.1", 21 | "typescript": "^5.2.2", 22 | "vue": "^3.3.4", 23 | "vue-router": "^4.2.5" 24 | }, 25 | "dependencies": { 26 | "@nuxtjs/tailwindcss": "^6.8.0", 27 | "@pinia/nuxt": "^0.4.11", 28 | "@prisma/client": "5.4.1", 29 | "class-variance-authority": "^0.7.0", 30 | "clsx": "^2.0.0", 31 | "lucide-vue-next": "^0.284.0", 32 | "markdown-it": "^13.0.2", 33 | "openai": "^4.11.1", 34 | "pinia": "^2.1.6", 35 | "radix-vue": "^0.4.1", 36 | "replicate": "^0.20.0", 37 | "stripe": "^13.9.0", 38 | "tailwind-merge": "^1.14.0", 39 | "tailwindcss-animate": "^1.0.7" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pages/auth.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /pages/code.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /pages/conversation.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /pages/dashboard.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /pages/image.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /pages/music.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 82 | -------------------------------------------------------------------------------- /pages/settings.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /pages/video.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 87 | -------------------------------------------------------------------------------- /plugins/crisp.client.js: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin(() => { 2 | window.$crisp = []; 3 | window.CRISP_WEBSITE_ID = "d65aaebb-bef1-46fc-80ba-562e7c2956ed"; 4 | 5 | (function () { 6 | var d = document; 7 | var s = d.createElement("script"); 8 | 9 | s.src = "https://client.crisp.chat/l.js"; 10 | s.async = 1; 11 | d.getElementsByTagName("head")[0].appendChild(s); 12 | })(); 13 | }); 14 | -------------------------------------------------------------------------------- /plugins/markdownit.ts: -------------------------------------------------------------------------------- 1 | import md from "markdown-it"; 2 | 3 | export default defineNuxtPlugin(() => { 4 | const renderer = md(); 5 | return { 6 | provide: { 7 | mdRenderer: renderer, 8 | }, 9 | }; 10 | }); -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model UserApiLimit { 14 | id String @id @default(cuid()) 15 | userId String @unique 16 | count Int @default(0) 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @updatedAt 19 | } 20 | model UserSubscription { 21 | id String @id @default(cuid()) 22 | userId String @unique 23 | // the map is just to give another name so we can access by two ways (the maped name is used by stripe that's why) 24 | stripeCustomerId String? @unique @map(name: "stripe_customer_id") 25 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id") 26 | stripePriceId String? @map(name: "stripe_price_id") 27 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end") 28 | } -------------------------------------------------------------------------------- /public/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hafizjavaid/Nuxt3-AI-Sass/0aa91d9603b34c4ff28ed752b9da6b962eb76a37/public/empty.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hafizjavaid/Nuxt3-AI-Sass/0aa91d9603b34c4ff28ed752b9da6b962eb76a37/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /public/logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/logo3.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 10 | 12 | -------------------------------------------------------------------------------- /public/logo4.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | -------------------------------------------------------------------------------- /public/logo5.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /server/api/code/index.post.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { protectedRoute } from '~/server/utils'; 3 | import { User } from "~/server/types"; 4 | 5 | const config = useRuntimeConfig(); 6 | const openai = new OpenAI({ 7 | apiKey: config.openaiKey 8 | }); 9 | const instructionMessage = { 10 | role: "system", 11 | content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations." 12 | }; 13 | export default defineEventHandler(async (event) => { 14 | 15 | await protectedRoute(event); 16 | const user = event.context.user as User 17 | 18 | const { messages } = await readBody(event); 19 | if (!openai.apiKey) { 20 | throw createError({ 21 | statusCode: 500, 22 | statusMessage: 'OpenAI API Key not configured.', 23 | }) 24 | } 25 | if (!messages) { 26 | throw createError({ 27 | statusCode: 400, 28 | statusMessage: 'Messages are required', 29 | }) 30 | 31 | } 32 | const freeTrial = await checkApiLimit(user.id); 33 | const isPro = await checkSubscription(user.id) 34 | if (!freeTrial && !isPro) { 35 | throw createError({ 36 | statusCode: 403, 37 | statusMessage: 'Free trial has expired. Please upgrade to pro.', 38 | }) 39 | 40 | } 41 | const response = await openai.chat.completions.create({ 42 | model: "gpt-3.5-turbo", 43 | messages: [instructionMessage, ...messages] 44 | }) 45 | await incrementApiLimit(user.id); 46 | 47 | return response.choices[0].message 48 | 49 | }) -------------------------------------------------------------------------------- /server/api/conversation/index.post.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { User } from "~/server/types"; 3 | 4 | const config = useRuntimeConfig(); 5 | const openai = new OpenAI({ 6 | apiKey: config.openaiKey 7 | }); 8 | 9 | export default defineEventHandler(async (event) => { 10 | 11 | // TODO: Verify and Get User 12 | await protectedRoute(event); 13 | const user = event.context.user as User 14 | const { messages } = await readBody(event); 15 | if (!openai.apiKey) { 16 | throw createError({ 17 | statusCode: 500, 18 | statusMessage: 'OpenAI API Key not configured.', 19 | }) 20 | } 21 | if (!messages) { 22 | throw createError({ 23 | statusCode: 400, 24 | statusMessage: 'Messages are required', 25 | }) 26 | 27 | } 28 | const freeTrial = await checkApiLimit(user.id); 29 | const isPro = await checkSubscription(user.id) 30 | if (!freeTrial && !isPro) { 31 | throw createError({ 32 | statusCode: 403, 33 | statusMessage: 'Free trial has expired. Please upgrade to pro.', 34 | }) 35 | 36 | } 37 | const response = await openai.chat.completions.create({ 38 | model: "gpt-3.5-turbo", 39 | messages, 40 | }) 41 | await incrementApiLimit(user.id); 42 | return response.choices[0].message 43 | 44 | }) -------------------------------------------------------------------------------- /server/api/image/index.post.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { protectedRoute } from '~/server/utils'; 3 | import { User } from "~/server/types"; 4 | 5 | const config = useRuntimeConfig(); 6 | const openai = new OpenAI({ 7 | apiKey: config.openaiKey 8 | }); 9 | 10 | export default defineEventHandler(async (event) => { 11 | 12 | await protectedRoute(event); 13 | const user = event.context.user as User 14 | const { prompt, amount = 1, resolution = "512x512" } = await readBody(event); 15 | if (!openai.apiKey) { 16 | throw createError({ 17 | statusCode: 500, 18 | statusMessage: 'OpenAI API Key not configured.', 19 | }) 20 | } 21 | if (!prompt) { 22 | throw createError({ 23 | statusCode: 400, 24 | statusMessage: 'Prompt are required', 25 | }) 26 | 27 | } 28 | if (!amount) { 29 | throw createError({ 30 | statusCode: 400, 31 | statusMessage: 'Amount are required', 32 | }) 33 | 34 | } 35 | if (!resolution) { 36 | throw createError({ 37 | statusCode: 400, 38 | statusMessage: 'Resolution are required', 39 | }) 40 | 41 | } 42 | const freeTrial = await checkApiLimit(user.id); 43 | const isPro = await checkSubscription(user.id) 44 | 45 | if (!freeTrial && !isPro) { 46 | throw createError({ 47 | statusCode: 403, 48 | statusMessage: 'Free trial has expired. Please upgrade to pro.', 49 | }) 50 | 51 | } 52 | const response = await openai.images.generate({ 53 | prompt, 54 | n: parseInt(amount, 10), 55 | size: resolution 56 | }) 57 | await incrementApiLimit(user.id); 58 | 59 | return response.data 60 | 61 | }) -------------------------------------------------------------------------------- /server/api/music/index.post.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { User } from "~/server/types"; 3 | 4 | const config = useRuntimeConfig(); 5 | 6 | const replicate = new Replicate({ 7 | auth: config.replicateKey 8 | }) 9 | 10 | export default defineEventHandler(async (event) => { 11 | 12 | await protectedRoute(event); 13 | const user = event.context.user as User 14 | const { prompt } = await readBody(event); 15 | if (!replicate.auth) { 16 | throw createError({ 17 | statusCode: 500, 18 | statusMessage: 'Replicate API Key not configured.', 19 | }) 20 | } 21 | if (!prompt) { 22 | throw createError({ 23 | statusCode: 400, 24 | statusMessage: 'Prompt is required', 25 | }) 26 | 27 | } 28 | 29 | 30 | const freeTrial = await checkApiLimit(user.id); 31 | const isPro = await checkSubscription(user.id) 32 | 33 | if (!freeTrial && !isPro) { 34 | throw createError({ 35 | statusCode: 403, 36 | statusMessage: 'Free trial has expired. Please upgrade to pro.', 37 | }) 38 | 39 | } 40 | const model = "riffusion/riffusion:8cf61ea6c56afd61d8f5b9ffd14d7c216c0a93844ce2d82ac1c9ecc9c7f24e05" 41 | const response = await replicate.run(model, { 42 | input: { 43 | prompt_a: prompt 44 | } 45 | }) 46 | await incrementApiLimit(user.id); 47 | 48 | return response 49 | 50 | }) -------------------------------------------------------------------------------- /server/api/stripe/checkStatus.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { User } from "~/server/types"; 3 | 4 | const prisma = new PrismaClient(); 5 | const DAY_IN_MS = 86_400_000; 6 | 7 | export default defineEventHandler(async (event) => { 8 | 9 | await protectedRoute(event); 10 | const user = event.context.user as User 11 | 12 | const userSubscription = await prisma.userSubscription.findUnique({ 13 | where: { 14 | userId: user.id, 15 | }, 16 | select: { 17 | stripeSubscriptionId: true, 18 | stripeCurrentPeriodEnd: true, 19 | stripeCustomerId: true, 20 | stripePriceId: true 21 | } 22 | }) 23 | 24 | console.log('[USER_SUBSCRIPTION]', userSubscription); 25 | 26 | 27 | if (!userSubscription) { 28 | return false; 29 | } 30 | 31 | const isValid = 32 | userSubscription.stripePriceId && 33 | userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > Date.now() 34 | 35 | console.log('[IS_VALID]', isValid); 36 | 37 | // For returning the boolean value 38 | return !!isValid; 39 | }) -------------------------------------------------------------------------------- /server/api/stripe/index.get.ts: -------------------------------------------------------------------------------- 1 | import { User } from "~/server/types"; 2 | import { PrismaClient } from '@prisma/client'; 3 | const prisma = new PrismaClient(); 4 | const returlUrl = absoluteUrl('/settings'); 5 | export default defineEventHandler(async (event) => { 6 | 7 | await protectedRoute(event); 8 | const user = event.context.user as User 9 | 10 | const userSubscription = await prisma.userSubscription.findUnique({ 11 | where: { 12 | userId: user.id 13 | } 14 | }) 15 | 16 | // Cancel or Upgrade Subscription 17 | if (userSubscription && userSubscription.stripeCustomerId) { 18 | const stripeSession = await stripe.billingPortal.sessions.create({ 19 | customer: userSubscription.stripeCustomerId, 20 | return_url: returlUrl 21 | }) 22 | console.log(stripeSession.url); 23 | 24 | return { 25 | url: stripeSession.url 26 | } 27 | 28 | 29 | 30 | 31 | } 32 | // New Subscription 33 | const stripeSession = await stripe.checkout.sessions.create({ 34 | success_url: returlUrl, 35 | cancel_url: returlUrl, 36 | payment_method_types: ["card"], 37 | mode: "subscription", 38 | billing_address_collection: "auto", 39 | customer_email: user.email, 40 | line_items: [ 41 | { 42 | price_data: { 43 | currency: "USD", 44 | product_data: { 45 | name: "MultiGeniX Pro", 46 | description: "Unlimited AI Generations" 47 | }, 48 | unit_amount: 2000, // $20 49 | recurring: { 50 | interval: "month" 51 | } 52 | }, 53 | quantity: 1, 54 | }, 55 | ], 56 | metadata: { 57 | userId: user.id, 58 | }, 59 | }) 60 | console.log(stripeSession.url); 61 | return { 62 | url: stripeSession.url 63 | } 64 | }) -------------------------------------------------------------------------------- /server/api/stripe/webhook.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import Stripe from "stripe" 3 | 4 | const prisma = new PrismaClient(); 5 | const STRIPE_WEBHOOK_SECRET = 6 | useRuntimeConfig().stripeWebhookSecret; 7 | 8 | export default defineEventHandler(async (event) => { 9 | const signature = getHeader(event, 'Stripe-Signature') as string; 10 | const body = await readRawBody(event); 11 | 12 | // Verify the webhook signature 13 | let stripeEvent 14 | try { 15 | // @ts-ignore 16 | stripeEvent = stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET); 17 | } catch (error) { 18 | console.error(error); 19 | throw createError({ 20 | statusCode: 400, 21 | statusMessage: 'Invalid signature', 22 | }); 23 | } 24 | 25 | const session = stripeEvent.data.object as Stripe.Checkout.Session 26 | 27 | // 28 | if (stripeEvent.type === "checkout.session.completed") { 29 | 30 | const subscription = await stripe.subscriptions.retrieve(session.subscription as string); 31 | 32 | if (!session?.metadata?.userId) { 33 | throw createError({ 34 | statusCode: 400, 35 | statusMessage: 'User id is required', 36 | }); 37 | } 38 | 39 | await prisma.userSubscription.create({ 40 | data: { 41 | userId: session?.metadata?.userId, 42 | stripeSubscriptionId: subscription.id, 43 | stripeCustomerId: subscription.customer as string, 44 | stripePriceId: subscription.items.data[0].price.id, 45 | stripeCurrentPeriodEnd: new Date( 46 | subscription.current_period_end * 1000 47 | ), 48 | } 49 | }) 50 | 51 | } 52 | 53 | // If they upgraded or cancelled 54 | if (stripeEvent.type === "invoice.payment_succeeded") { 55 | const subscription = await stripe.subscriptions.retrieve(session.subscription as string); 56 | await prisma.userSubscription.update({ 57 | where: { 58 | stripeSubscriptionId: subscription.id, 59 | }, 60 | data: { 61 | stripePriceId: subscription.items.data[0].price.id, 62 | stripeCurrentPeriodEnd: new Date( 63 | subscription.current_period_end * 1000 64 | ), 65 | }, 66 | }) 67 | } 68 | 69 | return 200; 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /server/api/user/index.get.ts: -------------------------------------------------------------------------------- 1 | import { User } from "~/server/types"; 2 | export default defineEventHandler(async (event) => { 3 | 4 | await protectedRoute(event); 5 | const user = event.context.user as User 6 | 7 | const apiCount = await getApiLimitCount(user.id) 8 | 9 | return { 10 | ...user, 11 | apiCount 12 | } 13 | }) -------------------------------------------------------------------------------- /server/api/video/index.post.ts: -------------------------------------------------------------------------------- 1 | import Replicate from "replicate"; 2 | import { User } from "~/server/types"; 3 | 4 | const config = useRuntimeConfig(); 5 | 6 | const replicate = new Replicate({ 7 | auth: config.replicateKey 8 | }) 9 | 10 | export default defineEventHandler(async (event) => { 11 | 12 | await protectedRoute(event); 13 | const user = event.context.user as User 14 | const { prompt } = await readBody(event); 15 | if (!replicate.auth) { 16 | throw createError({ 17 | statusCode: 500, 18 | statusMessage: 'Replicate API Key not configured.', 19 | }) 20 | } 21 | if (!prompt) { 22 | throw createError({ 23 | statusCode: 400, 24 | statusMessage: 'Prompt is required', 25 | }) 26 | 27 | } 28 | 29 | 30 | const freeTrial = await checkApiLimit(user.id); 31 | const isPro = await checkSubscription(user.id) 32 | 33 | if (!freeTrial && !isPro) { 34 | throw createError({ 35 | statusCode: 403, 36 | statusMessage: 'Free trial has expired. Please upgrade to pro.', 37 | }) 38 | 39 | } 40 | const model = "anotherjesse/zeroscope-v2-xl:9f747673945c62801b13b84701c783929c0ee784e4748ec062204894dda1a351" 41 | const response = await replicate.run(model, { 42 | input: { 43 | prompt 44 | } 45 | }) 46 | await incrementApiLimit(user.id); 47 | 48 | return response 49 | 50 | }) -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /server/types.d.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string 3 | email: string 4 | }; -------------------------------------------------------------------------------- /server/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { H3Event } from 'h3'; 2 | import { serverSupabaseUser } from '#supabase/server'; 3 | import { PrismaClient } from '@prisma/client'; 4 | import Stripe from 'stripe'; 5 | 6 | const prisma = new PrismaClient(); 7 | export const MAX_COUNT = 5 8 | export const protectedRoute = async (event: H3Event) => { 9 | const user = await serverSupabaseUser(event); 10 | 11 | if (!user) { 12 | throw createError({ 13 | statusCode: 401, 14 | statusMessage: 'Unauthorized', 15 | }); 16 | } 17 | event.context.user = user 18 | 19 | }; 20 | 21 | export const incrementApiLimit = async (userId: string) => { 22 | 23 | const userApiLimit = await prisma.userApiLimit.findUnique({ 24 | where: { userId: userId }, 25 | }); 26 | 27 | if (userApiLimit) { 28 | await prisma.userApiLimit.update({ 29 | where: { userId: userId }, 30 | data: { count: userApiLimit.count + 1 }, 31 | }); 32 | } else { 33 | await prisma.userApiLimit.create({ 34 | data: { userId: userId, count: 1 }, 35 | }); 36 | } 37 | 38 | 39 | } 40 | 41 | export const checkApiLimit = async (userId: string) => { 42 | 43 | const userApiLimit = await prisma.userApiLimit.findUnique({ 44 | where: { userId: userId }, 45 | }); 46 | 47 | if (!userApiLimit || userApiLimit.count < MAX_COUNT) { 48 | return true; 49 | } else { 50 | return false; 51 | } 52 | }; 53 | 54 | export const getApiLimitCount = async (userId: string) => { 55 | 56 | 57 | const userApiLimit = await prisma.userApiLimit.findUnique({ 58 | where: { 59 | userId 60 | } 61 | }); 62 | 63 | if (!userApiLimit) { 64 | return 0; 65 | } 66 | 67 | return userApiLimit.count; 68 | }; 69 | 70 | const config = useRuntimeConfig(); 71 | export const stripe = new Stripe(config.stripeSecret, { 72 | apiVersion: '2023-08-16', 73 | typescript: true 74 | }); 75 | 76 | export function absoluteUrl(path: string) { 77 | return `${config.appUrl}${path}` 78 | } 79 | const DAY_IN_MS = 86_400_000; 80 | 81 | export const checkSubscription = async (userId: string) => { 82 | const userSubscription = await prisma.userSubscription.findUnique({ 83 | where: { 84 | userId 85 | }, 86 | select: { 87 | stripeSubscriptionId: true, 88 | stripeCurrentPeriodEnd: true, 89 | stripeCustomerId: true, 90 | stripePriceId: true 91 | } 92 | }) 93 | 94 | if (!userSubscription) { 95 | return false; 96 | } 97 | 98 | const isValid = 99 | userSubscription.stripePriceId && 100 | userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > Date.now() 101 | 102 | 103 | // For returning the boolean value 104 | return !!isValid; 105 | } -------------------------------------------------------------------------------- /store/useProModal.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | export const useProModal = defineStore('ProModal', { 3 | state: () => ({ isOpen: false, }), 4 | 5 | actions: { 6 | 7 | 8 | onOpen() { 9 | 10 | this.isOpen = true 11 | }, 12 | onClose() { 13 | 14 | this.isOpen = false 15 | } 16 | 17 | 18 | }, 19 | }) -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | 5 | theme: { 6 | container: { 7 | center: true, 8 | padding: "2rem", 9 | screens: { 10 | "2xl": "1400px", 11 | }, 12 | }, 13 | extend: { 14 | colors: { 15 | border: "hsl(var(--border))", 16 | input: "hsl(var(--input))", 17 | ring: "hsl(var(--ring))", 18 | background: "hsl(var(--background))", 19 | foreground: "hsl(var(--foreground))", 20 | primary: { 21 | DEFAULT: "hsl(var(--primary))", 22 | foreground: "hsl(var(--primary-foreground))", 23 | }, 24 | secondary: { 25 | DEFAULT: "hsl(var(--secondary))", 26 | foreground: "hsl(var(--secondary-foreground))", 27 | }, 28 | destructive: { 29 | DEFAULT: "hsl(var(--destructive))", 30 | foreground: "hsl(var(--destructive-foreground))", 31 | }, 32 | muted: { 33 | DEFAULT: "hsl(var(--muted))", 34 | foreground: "hsl(var(--muted-foreground))", 35 | }, 36 | accent: { 37 | DEFAULT: "hsl(var(--accent))", 38 | foreground: "hsl(var(--accent-foreground))", 39 | }, 40 | popover: { 41 | DEFAULT: "hsl(var(--popover))", 42 | foreground: "hsl(var(--popover-foreground))", 43 | }, 44 | card: { 45 | DEFAULT: "hsl(var(--card))", 46 | foreground: "hsl(var(--card-foreground))", 47 | }, 48 | }, 49 | borderRadius: { 50 | lg: "var(--radius)", 51 | md: "calc(var(--radius) - 2px)", 52 | sm: "calc(var(--radius) - 4px)", 53 | }, 54 | keyframes: { 55 | "accordion-down": { 56 | from: { height: 0 }, 57 | to: { height: "var(--radix-accordion-content-height)" }, 58 | }, 59 | "accordion-up": { 60 | from: { height: "var(--radix-accordion-content-height)" }, 61 | to: { height: 0 }, 62 | }, 63 | }, 64 | animation: { 65 | "accordion-down": "accordion-down 0.2s ease-out", 66 | "accordion-up": "accordion-up 0.2s ease-out", 67 | }, 68 | }, 69 | }, 70 | plugins: [require("tailwindcss-animate")], 71 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type ChatCompletionRequestMessage = { 3 | role: 'user' | 'assistant'; 4 | content: string; 5 | } 6 | export type AudioResponse = { 7 | audio: string; 8 | spectrogram: string; 9 | } -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export const dashboardRoutes = [ 2 | { 3 | label: 'Dashboard', 4 | icon: 'lucide:layout-dashboard', 5 | link: '/dashboard', 6 | color: 'text-sky-500', 7 | }, 8 | { 9 | label: 'Conversation', 10 | icon: 'lucide:message-square', 11 | link: '/conversation', 12 | color: 'text-violet-500', 13 | }, 14 | { 15 | label: 'Image Generation', 16 | icon: 'lucide:image', 17 | color: 'text-pink-700', 18 | link: '/image', 19 | }, 20 | { 21 | label: 'Video Generation', 22 | icon: 'lucide:video', 23 | color: 'text-orange-700', 24 | link: '/video', 25 | }, 26 | { 27 | label: 'Music Generation', 28 | icon: 'lucide:music', 29 | color: 'text-emerald-500', 30 | link: '/music', 31 | }, 32 | { 33 | label: 'Code Generation', 34 | icon: 'lucide:code', 35 | color: 'text-green-700', 36 | link: '/code', 37 | }, 38 | { 39 | label: 'Settings', 40 | icon: 'lucide:settings', 41 | link: '/settings', 42 | }, 43 | ] 44 | export const tools = [ 45 | 46 | { 47 | label: 'Conversation', 48 | icon: 'lucide:message-square', 49 | link: '/conversation', 50 | color: 'text-violet-500', 51 | bgColor: "bg-violet-500/10", 52 | }, 53 | { 54 | label: 'Image Generation', 55 | icon: 'lucide:image', 56 | color: 'text-pink-700', 57 | link: '/image', 58 | bgColor: "bg-pink-700/10", 59 | }, 60 | { 61 | label: 'Video Generation', 62 | icon: 'lucide:video', 63 | color: 'text-orange-700', 64 | link: '/video', 65 | bgColor: "bg-orange-700/10", 66 | }, 67 | { 68 | label: 'Music Generation', 69 | icon: 'lucide:music', 70 | color: 'text-emerald-500', 71 | link: '/music', 72 | bgColor: "bg-emerald-500/10", 73 | }, 74 | { 75 | label: 'Code Generation', 76 | icon: 'lucide:code', 77 | color: 'text-green-700', 78 | link: '/code', 79 | bgColor: "bg-green-700/10", 80 | }, 81 | 82 | ] 83 | export const resolutionOptions = [ 84 | { 85 | value: "256x256", 86 | text: "256x256", 87 | }, 88 | { 89 | value: "512x512", 90 | text: "512x512", 91 | }, 92 | { 93 | value: "1024x1024", 94 | text: "1024x1024", 95 | }, 96 | ]; 97 | 98 | export const amountOptions = [ 99 | { 100 | value: "1", 101 | text: "1 Photo" 102 | }, 103 | { 104 | value: "2", 105 | text: "2 Photos" 106 | }, 107 | { 108 | value: "3", 109 | text: "3 Photos" 110 | }, 111 | { 112 | value: "4", 113 | text: "4 Photos" 114 | }, 115 | { 116 | value: "5", 117 | text: "5 Photos" 118 | } 119 | ]; 120 | 121 | export const MAX_COUNT = 5 122 | --------------------------------------------------------------------------------