├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── (auth) │ │ └── (routes) │ │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ │ ├── sign-up │ │ │ └── [[...sign-up]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ ├── (routes) │ │ ├── video │ │ │ ├── data.ts │ │ │ ├── page.tsx │ │ │ └── components │ │ │ │ ├── video-body.tsx │ │ │ │ ├── video-content.tsx │ │ │ │ └── video-form.tsx │ │ ├── conversation │ │ │ ├── data.ts │ │ │ ├── page.tsx │ │ │ └── components │ │ │ │ ├── conversation-body.tsx │ │ │ │ ├── conversation-content.tsx │ │ │ │ └── conversation-form.tsx │ │ ├── settings │ │ │ ├── data.ts │ │ │ ├── page.tsx │ │ │ └── components │ │ │ │ └── settings-form.tsx │ │ ├── code │ │ │ ├── data.ts │ │ │ ├── page.tsx │ │ │ └── components │ │ │ │ ├── code-body.tsx │ │ │ │ ├── code-content.tsx │ │ │ │ └── code-form.tsx │ │ ├── layout.tsx │ │ ├── music │ │ │ ├── page.tsx │ │ │ ├── components │ │ │ │ ├── music-body.tsx │ │ │ │ ├── music-content.tsx │ │ │ │ └── music-form.tsx │ │ │ └── data.ts │ │ ├── image │ │ │ ├── components │ │ │ │ ├── generated-image.tsx │ │ │ │ ├── image-body.tsx │ │ │ │ ├── image-prompt-section.tsx │ │ │ │ ├── image-content.tsx │ │ │ │ └── image-form.tsx │ │ │ ├── page.tsx │ │ │ └── data.ts │ │ └── dashboard │ │ │ └── page.tsx │ ├── (landing) │ │ ├── layout.tsx │ │ ├── components │ │ │ ├── Heading.tsx │ │ │ ├── heading.tsx │ │ │ ├── landing-footer.tsx │ │ │ └── example-prompts.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── api │ │ ├── settings │ │ │ └── route.ts │ │ ├── video │ │ │ └── route.ts │ │ ├── music │ │ │ └── route.ts │ │ ├── image │ │ │ ├── [imagePromptId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── webhooks │ │ │ └── route.ts │ │ ├── code │ │ │ └── route.ts │ │ └── conversation │ │ │ └── route.ts │ └── globals.css ├── lib │ ├── styles.ts │ ├── open-ai.ts │ ├── replicate-ai.ts │ ├── utils.ts │ ├── animations.ts │ └── azure.ts ├── components │ ├── ui │ │ ├── motion-div.tsx │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── modal.tsx │ │ ├── sonner.tsx │ │ ├── tooltip.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ └── select.tsx │ ├── services │ │ ├── animations.ts │ │ └── index.tsx │ ├── Empty.tsx │ ├── empty.tsx │ ├── clerk-button.tsx │ ├── tooltip-wrapper.tsx │ ├── reset-form-button.tsx │ ├── custom-avatar.tsx │ ├── scroll-to-bottom-arrow.tsx │ ├── navbar │ │ ├── Data.ts │ │ ├── data.ts │ │ ├── navbar-button.tsx │ │ ├── Animations.ts │ │ ├── animations.ts │ │ ├── Header.tsx │ │ ├── header.tsx │ │ └── index.tsx │ ├── heading │ │ └── index.tsx │ └── modals │ │ └── reset-form.tsx ├── middleware.ts ├── hooks │ ├── useCode.ts │ ├── useImage.ts │ ├── useConversation.ts │ ├── useResetFormModal.ts │ ├── useMediaQuery.ts │ └── useScroll.ts ├── providers │ └── modal-provider.tsx ├── actions │ ├── fetchUser.ts │ ├── fetchImages.ts │ ├── fetchCode.ts │ └── fetchConversations.ts ├── db │ ├── types.ts │ ├── migration.ts │ ├── index.ts │ └── schema.ts └── constants │ └── index.ts ├── screenshots ├── code-page.png ├── image-page.png ├── music-page.png ├── video-page.png └── conversation-page.png ├── postcss.config.js ├── Dockerfile ├── entrypoint.sh ├── next.config.js ├── components.json ├── drizzle.config.ts ├── .prettierrc.json ├── public ├── vercel.svg └── next.svg ├── .gitignore ├── types.d.ts ├── tsconfig.json ├── .dockerignore ├── compose.yaml ├── package.json ├── tailwind.config.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoshino-koya/SaaS__Ai_project/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/lib/styles.ts: -------------------------------------------------------------------------------- 1 | export const styles = { 2 | paddingX: 'max-w-7xl mx-auto px-4 md:px-12', 3 | }; 4 | -------------------------------------------------------------------------------- /screenshots/code-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoshino-koya/SaaS__Ai_project/HEAD/screenshots/code-page.png -------------------------------------------------------------------------------- /screenshots/image-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoshino-koya/SaaS__Ai_project/HEAD/screenshots/image-page.png -------------------------------------------------------------------------------- /screenshots/music-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoshino-koya/SaaS__Ai_project/HEAD/screenshots/music-page.png -------------------------------------------------------------------------------- /screenshots/video-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoshino-koya/SaaS__Ai_project/HEAD/screenshots/video-page.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /screenshots/conversation-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoshino-koya/SaaS__Ai_project/HEAD/screenshots/conversation-page.png -------------------------------------------------------------------------------- /src/components/ui/motion-div.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | export const MotionDiv = motion.div; 6 | -------------------------------------------------------------------------------- /src/lib/open-ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAi from 'openai'; 2 | 3 | export const openai = new OpenAi({ 4 | apiKey: process.env.OPENAI_API_KEY, 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/replicate-ai.ts: -------------------------------------------------------------------------------- 1 | import Replicate from 'replicate'; 2 | 3 | export const replicate = new Replicate({ 4 | auth: process.env.REPLICATE_API_TOKEN, 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs'; 2 | 3 | const Page = () => { 4 | return ; 5 | }; 6 | 7 | export default Page; 8 | -------------------------------------------------------------------------------- /src/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs'; 2 | 3 | const Page = () => { 4 | return ; 5 | }; 6 | 7 | export default Page; 8 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/app/(routes)/video/data.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const VideoFormSchema = z.object({ 4 | prompt: z.string().min(1, { message: 'Video prompt is required.' }), 5 | }); 6 | 7 | export type VideoFormValues = z.infer; 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=20.14.0 2 | FROM node:${NODE_VERSION} 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY ./package.json ./ 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN chown -R node:node /usr/src/app 12 | 13 | EXPOSE 3000 14 | CMD [ "npm", "run", "dev" ] 15 | -------------------------------------------------------------------------------- /src/app/(routes)/conversation/data.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const ConversationFormSchema = z.object({ 4 | prompt: z.string().min(1, { message: 'Prompt is required.' }), 5 | }); 6 | 7 | export type ConversationFormValues = z.infer; 8 | -------------------------------------------------------------------------------- /src/app/(routes)/settings/data.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const SettingsFormSchema = z.object({ 4 | name: z.string().min(1, { message: 'Name is required.' }), 5 | about: z.string(), 6 | }); 7 | 8 | export type SettingsFormValues = z.infer; 9 | -------------------------------------------------------------------------------- /src/app/(auth)/(routes)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function AuthLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 |
{children}
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Wait for the database to be ready 4 | echo "Waiting for database to be ready..." 5 | sleep 5 6 | 7 | # Run migrations 8 | echo "Database is up - running migrations..." 9 | npm run migrate 10 | 11 | # Start the app 12 | echo "Starting the app..." 13 | npm run dev 14 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware } from '@clerk/nextjs/server'; 2 | 3 | export default clerkMiddleware(); 4 | 5 | export const config = { 6 | // The following matcher runs middleware on all routes 7 | // except static assets. 8 | matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], 9 | }; 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'aisaaskpirabaharan.blob.core.windows.net', 8 | pathname: '**', 9 | }, 10 | ], 11 | }, 12 | }; 13 | 14 | module.exports = nextConfig; 15 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/components/services/animations.ts: -------------------------------------------------------------------------------- 1 | import { Variants } from 'framer-motion'; 2 | 3 | const serviceVariants: Variants = { 4 | hidden: { 5 | opacity: 0, 6 | y: 10, 7 | }, 8 | visible: { 9 | opacity: 1, 10 | y: 0, 11 | transition: { 12 | duration: 0.5, 13 | ease: 'easeInOut', 14 | }, 15 | }, 16 | }; 17 | 18 | export { serviceVariants }; 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@clerk/nextjs/server'; 2 | import { redirect } from 'next/navigation'; 3 | 4 | const LandingLayout = ({ children }: { children: React.ReactNode }) => { 5 | const { userId } = auth(); 6 | 7 | if (userId) { 8 | redirect('/dashboard'); 9 | } 10 | 11 | return
{children}
; 12 | }; 13 | 14 | export default LandingLayout; 15 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | 3 | export default defineConfig({ 4 | dialect: 'postgresql', 5 | schema: './src/db/schema.ts', 6 | out: './drizzle', 7 | dbCredentials: { 8 | host: process.env.DB_HOST, 9 | port: process.env.DB_PORT, 10 | database: process.env.DB_NAME, 11 | user: process.env.DB_USER, 12 | password: process.env.DB_PASSWORD, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/lib/animations.ts: -------------------------------------------------------------------------------- 1 | import { Variants } from 'framer-motion'; 2 | 3 | const scrollableDivArrowVariants: Variants = { 4 | hidden: { 5 | opacity: 0, 6 | y: 10, 7 | transition: { 8 | duration: 0.5, 9 | }, 10 | }, 11 | visible: { 12 | opacity: 1, 13 | y: 0, 14 | transition: { 15 | duration: 0.5, 16 | }, 17 | }, 18 | }; 19 | 20 | export default scrollableDivArrowVariants; 21 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "trailingComma": "all", 8 | "printWidth": 80, 9 | "arrowParens": "avoid", 10 | "endOfLine": "auto", 11 | "bracketSpacing": true, 12 | "bracketSameLine": false, 13 | "proseWrap": "preserve", 14 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(landing)/components/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { BotIcon } from 'lucide-react'; 2 | 3 | const Heading = () => { 4 | return ( 5 |
6 |

7 | Chatxyz 8 |

9 | 10 |
11 | ); 12 | }; 13 | 14 | export default Heading; 15 | -------------------------------------------------------------------------------- /src/app/(landing)/components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { BotIcon } from 'lucide-react'; 2 | 3 | const Heading = () => { 4 | return ( 5 |
6 |

7 | Chatxyz 8 |

9 | 10 |
11 | ); 12 | }; 13 | 14 | export default Heading; 15 | -------------------------------------------------------------------------------- /src/hooks/useCode.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; 2 | import { create } from 'zustand'; 3 | 4 | interface CodeState { 5 | code: ChatCompletionMessageParam[]; 6 | setCode: (code: ChatCompletionMessageParam[]) => void; 7 | resetCode: () => void; 8 | } 9 | 10 | export const useCode = create(set => ({ 11 | code: [], 12 | setCode: code => set(() => ({ code })), 13 | resetCode: () => set(() => ({ code: [] })), 14 | })); 15 | -------------------------------------------------------------------------------- /src/hooks/useImage.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | import { ImagePromptWithImages } from '@/db/types'; 4 | 5 | interface ImageState { 6 | imagePrompts: ImagePromptWithImages[]; 7 | setImagePrompts: (images: ImagePromptWithImages[]) => void; 8 | resetImagePrompts: () => void; 9 | } 10 | 11 | export const useImage = create(set => ({ 12 | imagePrompts: [], 13 | setImagePrompts: imagePrompts => set(() => ({ imagePrompts })), 14 | resetImagePrompts: () => set(() => ({ imagePrompts: [] })), 15 | })); 16 | -------------------------------------------------------------------------------- /src/providers/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | 5 | import ResetFormModal from '@/components/modals/reset-form'; 6 | 7 | const ModalProvider = () => { 8 | const [isMounted, setIsMounted] = useState(false); 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) { 15 | return null; 16 | } 17 | 18 | return ( 19 | <> 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default ModalProvider; 26 | -------------------------------------------------------------------------------- /src/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { BotIcon } from 'lucide-react'; 2 | 3 | interface EmptyProps { 4 | label: string; 5 | } 6 | 7 | const Empty = ({ label }: EmptyProps) => { 8 | return ( 9 |
10 |
11 | 12 |
13 |

{label}

14 |
15 | ); 16 | }; 17 | 18 | export default Empty; 19 | -------------------------------------------------------------------------------- /src/components/empty.tsx: -------------------------------------------------------------------------------- 1 | import { BotIcon } from 'lucide-react'; 2 | 3 | interface EmptyProps { 4 | label: string; 5 | } 6 | 7 | const Empty = ({ label }: EmptyProps) => { 8 | return ( 9 |
10 |
11 | 12 |
13 |

{label}

14 |
15 | ); 16 | }; 17 | 18 | export default Empty; 19 | -------------------------------------------------------------------------------- /src/actions/fetchUser.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@clerk/nextjs/server'; 2 | import { eq } from 'drizzle-orm'; 3 | import { redirect } from 'next/navigation'; 4 | 5 | import { db } from '@/db'; 6 | import { users } from '@/db/schema'; 7 | 8 | export const fetchUser = async () => { 9 | 'use server'; 10 | 11 | const { userId } = auth(); 12 | 13 | if (!userId) redirect('/'); 14 | 15 | const user = await db.query.users.findFirst({ 16 | where: eq(users.userId, userId), 17 | }); 18 | 19 | if (!user) redirect('/'); 20 | 21 | return user; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/clerk-button.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from '@clerk/nextjs'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { Skeleton } from '@/components/ui/skeleton'; 5 | 6 | const ClerkButton = () => { 7 | const [isMounted, setIsMounted] = useState(false); 8 | 9 | useEffect(() => { 10 | setIsMounted(true); 11 | }, []); 12 | 13 | if (!isMounted) { 14 | return ; 15 | } 16 | 17 | return ; 18 | }; 19 | 20 | export default ClerkButton; 21 | -------------------------------------------------------------------------------- /src/app/(routes)/code/data.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; 2 | import * as z from 'zod'; 3 | 4 | export const codeGenerationSetting: ChatCompletionMessageParam = { 5 | role: 'system', 6 | content: 7 | 'You are a code generator. Your must answer only in markdown code snippets. Use code comments in point form for explanations.', 8 | }; 9 | 10 | export const CodeFormSchema = z.object({ 11 | prompt: z.string().min(1, { message: 'Prompt is required.' }), 12 | }); 13 | 14 | export type CodeFormValues = z.infer; 15 | -------------------------------------------------------------------------------- /src/hooks/useConversation.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; 2 | import { create } from 'zustand'; 3 | 4 | interface ConversationState { 5 | conversation: ChatCompletionMessageParam[]; 6 | setConversation: (conversation: ChatCompletionMessageParam[]) => void; 7 | resetConversation: () => void; 8 | } 9 | 10 | export const useConversation = create(set => ({ 11 | conversation: [], 12 | setConversation: conversation => set(() => ({ conversation })), 13 | resetConversation: () => set(() => ({ conversation: [] })), 14 | })); 15 | -------------------------------------------------------------------------------- /src/db/types.ts: -------------------------------------------------------------------------------- 1 | import { InferSelectModel } from 'drizzle-orm'; 2 | 3 | import { code, conversation, image, imagePrompt, users } from '@/db/schema'; 4 | 5 | export type User = InferSelectModel; 6 | 7 | export type Conversation = InferSelectModel; 8 | 9 | export type Code = InferSelectModel; 10 | 11 | export type ImagePrompt = InferSelectModel; 12 | 13 | export type Image = InferSelectModel; 14 | 15 | export type ImagePromptWithImages = InferSelectModel & { 16 | images: Image[]; 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/useResetFormModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface ResetModalStore { 4 | isOpen: boolean; 5 | title: string | undefined; 6 | api: string | undefined; 7 | onOpen: ({ title, api }: { title: string; api: string }) => void; 8 | onClose: () => void; 9 | } 10 | 11 | export const useResetFormModal = create(set => ({ 12 | isOpen: false, 13 | title: undefined, 14 | api: undefined, 15 | onOpen: ({ title, api }) => set({ isOpen: true, title, api }), 16 | onClose: () => set({ isOpen: false, title: undefined, api: undefined }), 17 | })); 18 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useMediaQuery = (query: string) => { 4 | const [isMatches, setIsMatches] = useState(false); 5 | 6 | useEffect(() => { 7 | const media = window.matchMedia(query); 8 | if (media.matches !== isMatches) { 9 | setIsMatches(media.matches); 10 | } 11 | const listener = () => setIsMatches(media.matches); 12 | window.addEventListener('resize', listener); 13 | return () => window.removeEventListener('resize', listener); 14 | }, [isMatches, query]); 15 | 16 | return isMatches; 17 | }; 18 | 19 | export default useMediaQuery; 20 | -------------------------------------------------------------------------------- /src/app/(routes)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@clerk/nextjs/server'; 2 | import { redirect } from 'next/navigation'; 3 | 4 | import Header from '@/components/navbar/header'; 5 | 6 | const RoutesLayout = ({ children }: { children: React.ReactNode }) => { 7 | const { userId } = auth(); 8 | 9 | if (!userId) { 10 | redirect('/'); 11 | } 12 | 13 | return ( 14 | <> 15 |
16 |
17 |
18 |
19 | {children} 20 |
21 | 22 | ); 23 | }; 24 | 25 | export default RoutesLayout; 26 | -------------------------------------------------------------------------------- /src/actions/fetchImages.ts: -------------------------------------------------------------------------------- 1 | import { eq } from 'drizzle-orm'; 2 | 3 | import { db } from '@/db'; 4 | import { users } from '@/db/schema'; 5 | import { ImagePromptWithImages } from '@/db/types'; 6 | 7 | export const fetchImages = async ( 8 | userId: string, 9 | ): Promise => { 10 | 'use server'; 11 | 12 | const user = await db.query.users.findFirst({ 13 | where: eq(users.userId, userId), 14 | with: { imagePrompts: { with: { images: true } } }, 15 | }); 16 | 17 | if (!user) { 18 | throw new Error('User not found'); 19 | } 20 | 21 | const { imagePrompts } = user; 22 | 23 | return imagePrompts; 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/(routes)/music/page.tsx: -------------------------------------------------------------------------------- 1 | import { MusicGeneration as music } from '@/constants'; 2 | 3 | import Heading from '@/components/heading'; 4 | import MusicBody from './components/music-body'; 5 | 6 | const MusicPage = () => { 7 | const { title, api, showReset, icon, bgColor, textColor } = music; 8 | 9 | return ( 10 |
11 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default MusicPage; 25 | -------------------------------------------------------------------------------- /src/app/(routes)/video/page.tsx: -------------------------------------------------------------------------------- 1 | import { VideoGeneration as video } from '@/constants'; 2 | 3 | import Heading from '@/components/heading'; 4 | import VideoBody from './components/video-body'; 5 | 6 | const VideoPage = () => { 7 | const { title, api, showReset, icon, bgColor, textColor } = video; 8 | 9 | return ( 10 |
11 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default VideoPage; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # postgres 40 | data/db/ 41 | 42 | # drizzle 43 | drizzle/ 44 | 45 | # sst 46 | .sst 47 | 48 | # open-next 49 | .open-next -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: string; 4 | CLERK_SECRET_KEY: string; 5 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: string; 6 | NEXT_PUBLIC_CLERK_SIGN_UP_URL: string; 7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL: string; 8 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL: string; 9 | DB_HOST: string; 10 | DB_PORT: number; 11 | DB_NAME: string; 12 | DB_USER: string; 13 | DB_PASSWORD: string; 14 | OPENAI_API_KEY: string; 15 | REPLICATE_API_TOKEN: string; 16 | AZURE_STORAGE_ACCOUNT: string; 17 | AZURE_STORAGE_ACCOUNT_KEY: string; 18 | AZURE_STORAGE_CONTAINER: string; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/tooltip-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tooltip, 3 | TooltipContent, 4 | TooltipProvider, 5 | TooltipTrigger, 6 | } from '@/components/ui/tooltip'; 7 | import React, { PropsWithChildren } from 'react'; 8 | 9 | interface TooltipWrapperProps extends PropsWithChildren { 10 | tooltip: string; 11 | children: React.ReactNode; 12 | } 13 | 14 | const TooltipWrapper = ({ tooltip, children }: TooltipWrapperProps) => { 15 | return ( 16 | 17 | 18 | {children} 19 | 20 |

{tooltip}

21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default TooltipWrapper; 28 | -------------------------------------------------------------------------------- /src/components/reset-form-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { useResetFormModal } from '@/hooks/useResetFormModal'; 5 | 6 | interface ResetFormButtonProps { 7 | title: string; 8 | api: string; 9 | showReset: boolean; 10 | } 11 | 12 | const ResetFormButton = ({ title, api, showReset }: ResetFormButtonProps) => { 13 | const { onOpen } = useResetFormModal(); 14 | 15 | return ( 16 | showReset && ( 17 | 25 | ) 26 | ); 27 | }; 28 | 29 | export default ResetFormButton; 30 | -------------------------------------------------------------------------------- /src/db/migration.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import { drizzle } from 'drizzle-orm/node-postgres'; 4 | import { migrate } from 'drizzle-orm/node-postgres/migrator'; 5 | import { Pool } from 'pg'; 6 | 7 | const pool = new Pool({ 8 | host: process.env.DB_HOST, 9 | port: process.env.DB_PORT, 10 | database: process.env.DB_NAME, 11 | user: process.env.DB_USER, 12 | password: process.env.DB_PASSWORD, 13 | }); 14 | 15 | const db = drizzle(pool); 16 | 17 | const main = async () => { 18 | console.log('Migration started'); 19 | await migrate(db, { migrationsFolder: './drizzle' }); 20 | console.log('Migration finished'); 21 | process.exit(0); 22 | }; 23 | 24 | main().catch(err => { 25 | console.log(err); 26 | process.exit(0); 27 | }); 28 | -------------------------------------------------------------------------------- /src/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react'; 2 | 3 | type ScrollEventHandler = (event: Event) => void; 4 | 5 | const useScroll = ( 6 | ref: RefObject, 7 | handler: ScrollEventHandler, 8 | ) => { 9 | useEffect(() => { 10 | const element = ref.current; 11 | 12 | if (element) { 13 | // Add the scroll event listener to the provided ref when the component mounts 14 | element.addEventListener('scroll', handler); 15 | } 16 | 17 | // Remove the scroll event listener when the component unmounts 18 | return () => { 19 | if (element) { 20 | element.removeEventListener('scroll', handler); 21 | } 22 | }; 23 | }, [ref, handler]); 24 | }; 25 | 26 | export default useScroll; 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | **/charts 23 | **/docker-compose* 24 | **/compose* 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE 34 | README.md 35 | /data 36 | /screenshots 37 | -------------------------------------------------------------------------------- /src/app/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchUser } from '@/actions/fetchUser'; 2 | import { Settings as settings } from '@/constants'; 3 | 4 | import Heading from '@/components/heading'; 5 | import SettingsForm from './components/settings-form'; 6 | 7 | export const revalidate = 0; 8 | 9 | const SettingsPage = async () => { 10 | const { title, icon, showReset, bgColor, textColor } = settings; 11 | 12 | const user = await fetchUser(); 13 | 14 | return ( 15 |
16 | 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default SettingsPage; 29 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/node-postgres'; 2 | import { Client } from 'pg'; 3 | 4 | import * as schema from '@/db/schema'; 5 | 6 | declare global { 7 | var drizzle: Client | undefined; 8 | } 9 | 10 | const createNewClient = () => { 11 | const client = new Client({ 12 | host: process.env.DB_HOST, 13 | port: process.env.DB_PORT, 14 | database: process.env.DB_NAME, 15 | user: process.env.DB_USER, 16 | password: process.env.DB_PASSWORD, 17 | }); 18 | client.connect(); 19 | return client; 20 | }; 21 | 22 | const client = globalThis.drizzle ? globalThis.drizzle : createNewClient(); 23 | 24 | if (process.env.NODE_ENV === 'development') { 25 | globalThis.drizzle = client; 26 | } 27 | 28 | // Can only use db in server components 29 | export const db = drizzle(client, { schema }); 30 | -------------------------------------------------------------------------------- /src/app/(landing)/components/landing-footer.tsx: -------------------------------------------------------------------------------- 1 | import { ComputerIcon } from 'lucide-react'; 2 | 3 | import { Separator } from '@/components/ui/separator'; 4 | 5 | const LandingFooter = () => { 6 | return ( 7 |
8 |
9 | 10 |

AI SAAS

11 |
12 |
13 |

Terms of use

14 | 18 |

Privacy policy

19 |
20 |
21 | ); 22 | }; 23 | 24 | export default LandingFooter; 25 | -------------------------------------------------------------------------------- /src/app/(routes)/music/components/music-body.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | import MusicContent from './music-content'; 8 | import MusicForm from './music-form'; 9 | 10 | const MusicBody = () => { 11 | const [music, setMusic] = useState(); 12 | 13 | return ( 14 |
15 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default MusicBody; 31 | -------------------------------------------------------------------------------- /src/app/(routes)/video/components/video-body.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | import VideoContent from './video-content'; 8 | import VideoForm from './video-form'; 9 | 10 | const VideoBody = () => { 11 | const [video, setVideo] = useState(); 12 | 13 | return ( 14 |
15 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default VideoBody; 31 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | import * as React from 'react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/app/(routes)/music/components/music-content.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | import Empty from '@/components/empty'; 6 | 7 | interface MusicContentProps { 8 | music: string | undefined; 9 | } 10 | 11 | const MusicContent = ({ music }: MusicContentProps) => { 12 | return ( 13 |
19 | {!music ? ( 20 | 21 | ) : ( 22 |
23 | 26 |
27 | )} 28 |
29 | ); 30 | }; 31 | 32 | export default MusicContent; 33 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |