Skip to content

rhazal/LLM-AI-Toolbox

Repository files navigation

Fullstack AI Toolbox

(Portfolio Project)

built with Next.js & hosted by Vercel

Demo


Welcome & Introductory

Brief Introduction:


🚀 Introducing "LLM AI Toolbox: SaaS Suite," your ultimate weapon in the AI universe! 🤖🌌 This GitHub repository is a powerhouse of Language Model-based AI tools that'll take your projects to the next level! 💯.

This all-in-one platform offers seamless access to multiple LLM AI capabilities, including;

  • 📝 Master natural language processing,
  • 😃 Explore sentiment analysis, and
  • 🧠 Dive into text generation.

All within a single webpage. No fuss, no hassle, just pure AI wizardry at your fingertips! 🤖

Embrace the power of AI with my SaaS-based approach, enabling effortless integration and utilization of these cutting-edge tools for your projects and applications. Revolutionize your data processing and analysis with the versatility and convenience of this all-inclusive toolbox, empowering you to level up your AI game and gain deeper insights from textual data.

Don't miss the AI hype train! 🚂 Jump on board and embark on an extraordinary AI adventure with my powerhouse toolbox! 🚀 Embrace the future of AI and tech greatness! 💪🌟

🔑 Key Features of this project:


Click here to see all the features:

Let's dive into the key features that make this project shine! 💡

  • 💼 Proficiently structured the folder organization in Next 13 App Router, optimizing code management and maintainability.
  • 🖼️ Demonstrated expertise in building a robust Image Generation Tool using Open AI, enhancing visual content creation.
  • 🎥 Successfully developed an advanced Video Generation Tool powered by Replicate AI, delivering high-quality multimedia content.
  • 💬 Skillfully implemented a sophisticated Conversation Generation Tool with Open AI, enabling seamless and engaging user interactions.
  • 🎵 Engineered a state-of-the-art Music Generation Tool using Replicate AI, enriching the platform with personalized audio experiences.
  • 🌐 Optimized page loading state to ensure a smooth and responsive user experience across all devices and network conditions.
  • 🆓 Strategically designed and integrated a free tier with API limiting, attracting a broader user base while ensuring sustainable growth.
  • 🔄 Leveraged advanced techniques to handle relations between Server and Child components, ensuring real-time data synchronization and efficiency.
  • 🔄 Effectively utilized layout reusability to streamline development and maintain consistency throughout the application.
  • 💳 Stripe integration: Seamlessly handle secure payment transactions for premium subscriptions.
  • 💎 Sleek UI with Tailwind design: Enjoy a visually stunning and modern user interface.
  • 🌟 Tailwind animations and transition effects: Enhance the user experience with smooth and captivating animations.
  • 📱 Full responsiveness for all devices: The application adapts flawlessly to various screen sizes and devices.
  • 🔐 Credential authentication with Clerk: Safeguard user data and ensure secure access to the platform.
  • 🚀 Github, Google and email authentication integration: Simplify the registration and login process using GitHub/Google credentials.
  • 🚦 Server error handling with react-toast: Display meaningful error messages and ensure smooth error handling.
  • 💰 Stripe recurring payment integration: Enable seamless subscription billing and automate payment handling.
  • 🔄 Using POST, GET, and DELETE routes in route handlers (app/api): Implement a robust backend API to handle data operations.
  • 🌐 Fetch data with server React components: Optimize performance by directly accessing the database without relying on API calls.
  • ⚡️ Handling relations between Server and Child components in a real-time environment: Ensure consistent data synchronization and real-time updates.
  • 🛑 Cancelling Stripe subscriptions: Allow users to easily cancel their subscription plans.
  • </ul> 
    

📚 Libraries used in this project:


Click here to see all the packages:

Dependencies:

  1. @clerk/nextjs: Integrates the Clerk authentication and user management system with Next.js applications.

  2. @hookform/resolvers: Provides resolver functions for React Hook Form, enabling advanced form validation.

  3. @prisma/client: Prisma's database client, used for seamless database access and query generation.

  4. @radix-ui/react-avatar: Offers a customizable avatar component with built-in styles for React applications.

  5. @radix-ui/react-dialog: Provides a flexible and accessible dialog component to create modal dialogs.

  6. @radix-ui/react-label: A collection of label components to improve accessibility and interaction in forms.

  7. @radix-ui/react-progress: Renders a progress bar with customizable styles and animation options.

  8. @radix-ui/react-select: A powerful and accessible select dropdown component with various features.

  9. @radix-ui/react-slot: Enables slot-based content composition for components in a React application.

  10. @types/node, @types/react, @types/react-dom: TypeScript declaration files for Node.js and React.

  11. autoprefixer: A PostCSS plugin that automatically adds vendor prefixes to CSS styles.

  12. axios: A widely-used HTTP client for making asynchronous requests to APIs.

  13. class-variance-authority: A library for class variance analysis and authority checks.

  14. clsx: A utility to conditionally join CSS class names together.

  15. crisp-sdk-web: Crisp chat SDK for web applications, enabling live chat support.

  16. eslint: A pluggable linting utility to enforce coding standards and detect errors in JavaScript/TypeScript code.

  17. eslint-config-next: ESLint configuration for Next.js projects.

  18. framer-motion: A motion library for creating smooth animations and transitions in React applications.

  19. lucide-react: A collection of crisp and clear SVG icons as React components.

  20. next: A popular React framework for building server-rendered applications.

  21. openai: A Python library for working with the OpenAI API.

  22. postcss: A tool for transforming CSS styles using JavaScript plugins.

  23. react: A JavaScript library for building user interfaces.

  24. react-dom: The DOM-specific entry point for React applications.

  25. react-hook-form: A library for flexible and efficient form validation and handling.

  26. react-hot-toast: A minimalistic and customizable toast library for React.

  27. react-icons: A library containing popular icon packs as React components.

  28. react-markdown: Renders Markdown content as React components, useful for displaying formatted text.

  29. react-parallax-tilt: Enables the creation of parallax tilt effects for React components.

  30. replicate: A package for replicating data structures with modifications.

  31. stripe: Stripe SDK for handling payment processing and transactions.

  32. tailwind-merge: A utility for merging tailwind classes together.

  33. tailwindcss: A highly customizable CSS framework for rapid UI development.

  34. tailwindcss-animate: Tailwind CSS plugin for adding animation utilities.

  35. typescript: A typed superset of JavaScript that enhances code quality and tooling support.

  36. typewriter-effect: Creates a typewriter-like effect for text rendering in React applications.

  37. zod: A TypeScript-first schema validation library for building robust data structures.

  38. zustand: A state management library based on hooks, making it easy to manage global application state.

Dev Dependencies:

  1. prisma: Prisma CLI for database schema management and migrations.



Development Journey


1. Setting up the Environment & Clerk Authentication

Click here to expand:

Setting Up the Environment


Click here to expand:

To kickstart the project, I created a new Next.js app using the create-next-app command with additional configurations:

npx create-next-app@latest my-app --typescript --tailwind --eslint



Ran into a little issue:

Issue Description: Resolving 'Cannot find module' Error While working on the LLM AI Toolbox project, I encountered the following error:

Error: Cannot find module 'F:\My documents\VSCodeFiles\my_React-projects\LLM AI Toolbox\.next\server\app\(landing)\page_client-reference-manifest.js'
Require stack:



Next, I used the shadcn-ui CLI to set up the project:

npx shadcn-ui@latest init

During the initialization process, I configured the components.json file to define various project settings, such as style, colors, global CSS file location, and import aliases.



App Structure

I organized my Next.js app into the following structure:

.
├── app
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── ui
│   │   ├── alert-dialog.tsx
│   │   ├── button.tsx
│   │   ├── dropdown-menu.tsx
│   │   └── ...
│   ├── main-nav.tsx
│   ├── page-header.tsx
│   └── ...
├── lib
│   └── utils.ts
├── styles
│   └── globals.css
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.js
└── tsconfig.json

In this structure:

  • The app folder contains the layout.tsx and page.tsx files, providing a base layout for the app.
  • UI components are placed in the components/ui folder for better organization.
  • Other components, such as and , reside in the components folder.
  • Utility functions are stored in the lib folder, with utils.ts housing the cn helper.
  • Global CSS is located in the styles folder.



Adding Components

With the environment and project structure set up, I can easily add components to the project using the shadcn-ui CLI. For example, to add a Button component, I ran:

npx shadcn-ui@latest add button

Once added, I can import and use the Button component in my code:

import { Button } from "@/components/ui"
 
export default function Home() {
  return (
    <div>
      <Button>Click me</Button>
    </div>
  )
}

This setup and organization facilitate a clean and scalable codebase, making it easy to develop and maintain the Next.js app.



Setting up Clerk Authentication


Click here to expand:
  1. Signing up and Registering Account with Clerk.com I signed up and registered an account with Clerk.com to utilize their authentication services for my project.

  2. Enabling Github, Google, and Email Sign-In I enabled multiple sign-in methods, including Github, Google, and Email, to provide users with convenient authentication options.

  3. Creating .env Environment I created a .env file to store sensitive information, such as API keys and environment-specific variables, securely.

  4. Adding Clerk Keys I added the Clerk API keys to the .env file to connect my application to the Clerk authentication service.

  5. Adding Clerk Library to the Project To integrate Clerk authentication into my Next.js application, I installed the Clerk library using the following command:

npm install @clerk/nextjs
  1. Mounting Clerk Provider into the Layout I wrapped all children components inside the Clerk Provider in the layout.tsx file to make authentication available throughout the application.

  2. Setting Up Middleware for Authentication I implemented middleware to protect specific routes that should be accessible only to authenticated users. This allowed me to define which pages are public and which ones need authentication.

  3. Creating Auth Route with Clerk Components Using Clerk's prebuilt components, and , I set up an auth route to embed the sign-in and sign-up functionalities into my Next.js application.

  4. Updating Environment Variables for Clerk Paths I added environment variables for the paths required by Clerk, such as signIn, signUp, afterSignUp, and afterSignIn.

  5. Creating General Layout File for Styling I created a general layout file to style the and pages/components consistently.

  6. Adding Buttons to Home Screen I added buttons to the home screen to link users to the and pages/components for easy navigation.

  7. Adding to the Dashboard I included the component on the dashboard, allowing users to access their account details and perform user-related actions.

  8. Updating the signout Redirect back to the base path

<UserButton 
  afterSignOutUrl="/"
/>

By following these steps, I was able to integrate the Clerk authentication service seamlessly into my LLM AI Toolbox project, providing users with a secure and user-friendly authentication experience.





2. Creating the SideBar and Dashboard

Click here to expand:

Working on the dashbord and creating a new sidebar (mobile and desktop versions)

Sidebar funcitonality


Click here to expand:

created the sidebar and main page sections

created a navbar componenent

  • with hamburger menue that will appear when sidebar dissapears (media queries)
  • added the <userButton> to the navbar instead

creating a sidebar component

  • adding a logo
  • creating a name
  • using some creative styling using cn and twMerge library - ensuring proper way to add additonal dynamic classnames without risk of being overridden
    import { Montserrat } from "next/font/google";
    
    import { cn } from "@/lib/utils";
    
    const montserrat = Montserrat({ weight: "600", subsets: ["latin"]});  
    <h1 className={cn("text-2xl font-bold", montserrat.className)}>Ai Toolbox</h1>
    • creating an array of the various routs that will be in the app, for example
    const routes = [
      {
        label: 'Dashboard',
        icon: LayoutDashboard,
        href: '/dashboard',
        color: "text-sky-500"
  • creating a map funciton to map over the route objects and passing the details into <links> and <divs>

updating the sidebar and creating MobileSidebar component

  • extracting the <Button> & <Menu> into a new component: MobileSidebar
  • adding the sheet form shadcn - to slide the menue open
    npx shadcn-ui@latest add sheet
  • wrapping the entire component inside this sheet tool and creating a trigger
  • creating the sheet content and simply importing the sidebar component into the sheetcontent container


Fixing hydration and specific highlighting heading/route


Click here to expand:

Running into hydration issues with the MobileSidebar component

used a little useEffect and useState trick to fix this

const MobileSidebar = () => {
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
      setIsMounted(true);
  }, []);

  if (!isMounted) {
      return null;
  }



Highlighting effect

Creating a highlight effect for the sidebar component so that when on a certain path the sidebar route will be highlighted

  • used usePathname from the next/navigation library
import { usePathname } from "next/navigation";

const Sidebar = () => {
const pathname = usePathname();
return (
  //rest of code
  className={cn(
      "text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:text-white hover:bg-white/10 rounded-lg transition",
      pathname === route.href ? "text-white bg-white/10" : "text-zinc-400",
  )}
  //rest of code
)


Dashboard functionality


Click here to expand:
  • creating some headings and styling
  • creating a const of tools (eventually i will use some abstraction and put this elsewhere)
const tools = [
  {
    label: "Converstations",
    icon: MessageSquare,
    color: "text-violet-500",
    bgColor: "bg-ciolet-500/10",
    href: "/conversation",
  }
]
  • importing the card component from shadcn

    npx shadcn-ui@latest add card
  • creating a map funciton to map over the tools (there will be more soon)

    • passing details into a Card component

    • creating some styling for the card and passing the elements into them

    • creating an onclick function to take us to the correct page

      • using useRouter from 'next/navigation
        <Card
        onClick={() => router.push(tool.href)}




3. Conversation UI and Open AI API Integration

Click here to expand:

Conversation UI


Click here to expand:

created a conversation route under the (dashboard)/(routes)/conversation/page.tsx

creating a <Heading /> component and importing into the conversation/page.tsx

  • I defined the HeadingProps interface to specify the expected props for the Heading component, such as title, description, icon, iconColor, and bgColor.
  • I created the Heading component as a functional component, taking the HeadingProps as its props.
  • Inside the component, I structured the content by using the Icon prop and displaying it within a rounded container with the specified background color (bgColor) if provided.
  • I applied various styles to the component using Tailwind CSS classes to achieve the desired layout and visual presentation. The title was styled as a bold heading, while the description was styled as smaller text with a muted foreground color.
  • Lastly, I exported the Heading component at the end of the file, making it accessible for use in other parts of the project.



Fleshin out the page.tsx input section w/ forms

  • Importing the form from shadcn

    npx shadcn-ui@latest add form
  • Using the z library for handling schema validation with zod and the zodResolver from @hookform/resolvers/zod to integrate zod with react-hook-form

  • Creating a form schema in a new file constants.ts, where I will handle the form validation

  • Set up a form using react-hook-form and zodResolver to handle form validation based on the provided formSchema. The form also has a default value for the prompt field.

  • Defined a variable isLoading to track the form submission state, which will be used later to disable form inputs during the submission process.

  • Defined an onSubmit function to handle form submissions. However, the actual API call implementation is yet to be done.
    Currently, the onSubmit function logs the form values to the console.

  • Rendering the Form component

    • Installing the Input component form shadcn
      npx shadcn-ui@latest add input
    • creating a div with all the form requirements (not going to list out the steps for this)


Open AI API Integration


Click here to expand:

Set up

  • Creating an Open AI account open AI
  • Getting the API secret key and adding to the .env file
  • Installing the Open Ai package into the project
npm i openai



creating an api folder with: (app/api/conversation/route.ts)

  • I imported the required modules and libraries, including @clerk/nextjs, next/server, and openai.

  • Setting up the OpenAI configuration with my API key was the next step. I created a new Configuration instance and initialized the OpenAIApi with this configuration.

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});

const openai = new OpenAIApi(configuration);
  • I defined the POST function that would handle the API call. Inside this function, I retrieved the userId using auth() from @clerk/nextjs. I also parsed the incoming request body using req.json() to extract the messages field.

  • Created variouse checks with responses using NextResponse:

    • To ensure user authentication, I checked if userId was present. If not, I returned a 401 Unauthorized response using NextResponse.
    • To verify that the OpenAI API key was properly configured, I checked the apiKey field in the configuration. If it wasn't set, I returned a 500 Internal Server Error response.
    • I validated the presence of the messages field in the request body. If it was missing, I returned a 400 Bad Request response.
  • Using the openai.createChatCompletion method, I made the API call to OpenAI. I specified the model as "gpt-3.5-turbo" and provided the messages.

  • For further customization and handling of the response, I added a TODO comment in the code.

  • I implemented error handling by using a try-catch block. In case of an error, I logged the error and returned a 500 Internal Server Error response.

  • Lastly, I returned a success response with a 200 OK status.



Completing the response section in conversation ui

preamble;

  • installed and imported the packed axios for http requests
npm i axios
  • brought the package useRouter into to the file to refresh the browser page (note, from next/navigation)
  • Needed to implement useState for the setting of messages, with a specific type defined by openAI doc's and default being an empty array
const [messages, setMessages] = useState<ChatCompletionRequestMessage[]>([]);



In the onsubmit button

  • created a try, catch, finally block
    • catch; and console.log any error's
    • finally; router.refresh();
    • try;
     //define what the user message is    
     const userMessage: ChatCompletionRequestMessage = { role: "user", content: values.prompt };
     //an array of the user's message's 
     const newMessages = [...messages, userMessage];
     //api call
     const response = await axios.post('/api/conversation', { messages: newMessages });            
     //set the message 
     setMessages((current) => [...current, userMessage, response.data]);
     //reset the form back to default 
     form.reset();


    The Open AI Model is working ! 🥳🤖


Styling, adding loading states & empty states


Click here to expand:

Styling, adding loading states & empty states

  • adding empty state, while rendering will check if there are no messages

    • created a new component <empty />
  • adding a simple loading state, while message is loading

    • created a new component <loader />
  • styling the messages;

    • created conditional styling that will adjust depending on source of message (user or bot)
    {message.role === "user" ? <UserAvatar /> : <BotAvatar />}
    • with a bit of help from shadcn created two new components
      npx shadcn-ui@latest add avatar
    • created a new component: <UserAvatar />
    • created a new component: <BotAvatar />




4. Developing remaining AI Tools

Click here to expand:

Code Generation


Click here to expand:

Created a new route app\(dashboard)\(routes)\code\page.tsx

Creating the UI and styling

This was really simple to implement as it also uses the openAI model and just involved copy/pasting the conversation UI and tweaking few things;

  • Image
  • Main Heading
  • Sub Heading text
  • Basic colors
  • Display message



Creating new API route

creating a new route.tsx in the api folder under a new folder called code

Also very simple to implement as it is very similar to the conversation generator, so essentially copy paste and tweak

  • first we want to give the ai some instructions;

    const instructionMessage: ChatCompletionRequestMessage = {
      role: "system",
      content: "You are a code generator. You must answer only in markdown code snippets. Use code comments for explanations."
    };
  • then when calling the api we want to feed this instruction first and then the message from the user;

    const response = await openai.createChatCompletion({
      model: "gpt-3.5-turbo",
      messages: [instructionMessage, ...messages]
    });



Updating how we render the response from ai

Currently it will out put the response in a text format and that is hard to read and practically useless, therefore need to update the way we present this response Need to create a way for the code to come out in a react markdown format. Luckyly there is a package which can help alot with this

npm i react-markdown

Implementation:

{/* RETRUN IN MARKUP FORMAT */}
<ReactMarkdown 
  components={{
    pre: ({ node, ...props }) => (
        <div className="overflow-auto w-full my-2 bg-black/10 p-2 rounded-lg">
        <pre {...props} />
        </div>
    ),
    code: ({ node, ...props }) => (
        <code className="bg-black/10 rounded-lg p-1" {...props} />
    )
  }} 
  className="text-sm overflow-hidden leading-7"
>
  {message.content || ""}
</ReactMarkdown>


Image Generation


Click here to expand:

started creating skeletons for the below and developed accordingly

Created the constants.tsx

Click here to expand:
  1. I created a new TypeScript file named constants.ts inside the image folder of the routes directory under dashboard.

  2. I imported the required modules by adding the following line at the beginning of the file:

import * as z from "zod";
  1. I defined the form schema using Zod, which validated the form data for the image. It included the following fields:
  • prompt: A required string field with a minimum length of 1 character, used for the photo prompt.
  • amount: An optional string field with a minimum length of 1 character, used for the number of photos.
  • resolution: An optional string field with a minimum length of 1 character, used for the resolution of the photos.
export const formSchema = z.object({
  prompt: z.string().min(1, {
    message: "Photo prompt is required",
  }),
  amount: z.string().min(1),
  resolution: z.string().min(1),
});
  1. I defined the options for the amount field, which were displayed in a dropdown in the form. The options included the number of photos users could select, each represented as an object with value and label properties:
export const amountOptions = [
  {
    value: "1",
    label: "1 Photo",
  },
  {
    value: "2",
    label: "2 Photos",
  },
  // etc.
];
  1. I defined the options for the resolution field, which were displayed in another dropdown in the form. The options represented different image resolutions, each represented as an object with value and label properties:
export const resolutionOptions = [
  {
    value: "256x256",
    label: "256x256",
  },
  {
    value: "512x512",
    label: "512x512",
  },
  // etc.
];


Created page.tsx

Click here to expand:
  1. I imported the required modules and components

  2. I defined the initial state using the useState hook to store the generated photos in the photos state variable.

const [photos, setPhotos] = useState<string[]>([]);
  1. I set up the form using react-hook-form by creating a form instance with the useForm hook. The form had fields for prompt, amount, and resolution, and I used zodResolver to validate the form data against the formSchema:
const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: {
    prompt: "",
    amount: "1",
    resolution: "512x512"
  }
});
  1. I defined the options for the amount and resolution fields, which were used in dropdowns in the form. The options were stored in the amountOptions and resolutionOptions arrays

  2. I implemented the onSubmit function to handle form submission. It performed the following steps:

    • Reset the photos state to an empty array.
    • Extracted the form values from the form instance.
    • Made an API call using axios.post to the /api/image endpoint with the form values.
    • Retrieved the image URLs from the API response and stored them in the photos state.
    • Reset the form to its default values.

    • In case of an error during API call, logged the error to the console.
    • I used the router from next/navigation to refresh the page after the form submission, ensuring that the generated images were displayed.
  3. Finally for the render,:

    • The input section, the same as the code/conversation generator with the exception of added form controls to handle the resolution and amount of images.
    • The output section displayed the loading spinner when isLoading was true, an empty state message when no images were generated, and the generated images using the Card and Image components from shadcn.


Created the api call - api/images/routes.tsx

Click here to expand:

I created a new TypeScript file named route.ts inside the image folder of the api directory & imported the required modules and components.

I initialized a new Configuration object with the apiKey provided in the environment variable process.env.OPENAI_API_KEY.

I created an instance of OpenAIApi using the previously created configuration.

I exported an asynchronous function named POST, which handles POST requests to the /api/image endpoint. Inside the POST function, I ;

  • extracted the userId and the request body from the req object using destructuring.
  • extracted the prompt, amount, and resolution from the request body using default values.
  • checked for user authentication by verifying the existence of userId using auth() from @clerk/nextjs. If the user is not authenticated, I returned a NextResponse with the status code 401 (Unauthorized).
  • ensured that the apiKey is configured. If it is not available, I returned a NextResponse with the status code 500 (Internal Server Error) and a message stating that the OpenAI API key is not configured.
  • validated that the prompt, amount, and resolution fields are present in the request. If any of them are missing, I returned a NextResponse with the status code 400 (Bad Request) and an appropriate error message.
  • Then called the openai.createImage() method with the provided prompt, amount, and resolution to generate the required images from the OpenAI API.
  • Finally, I returned a JSON response with the data received from the OpenAI API.

In case of an error during the process, I caught the error, logged it to the console with a specific tag, and returned a NextResponse with the status code 500 (Internal Server Error) and a generic "Internal Error" message.



Updated teh next.config.js

Click here to expand:
  • simply just added the domain for the images received from openAI
const nextConfig = {
    images: {
        domains: [
            "oaidalleapiprodscus.blob.core.windows.net",
        ]
    }
}




Music & Video Generation (ReplicateAI)


Click here to expand:

Step 1: Setting up an Account with Replicate

I went to the Replicate website and signed up for an account.


Step 2: Getting the API Keys and Adding them to the .env File

After logging in to the Replicate dashboard, I navigated to the API section to generate my API keys.

I added the REPL_API_KEY and REPL_PROJECT_ID to the .env file:

I customized the model and other options as per the Replicate API documentation for music or video generation.


Step 3: Setting Up Spend Limits

In the Replicate dashboard, I configured my spend limits to prevent unexpected usage costs.

Step 4: Installing the Replicate Package into the Project

Installing the replicate package into the project

npm i replicate

Step 5: Setting Up the Route Skeleton for Music/Video Route

Inside the app/api directory, I created a new TypeScript file for the music and video route, such as video/route.ts and music/route.ts

I set up the basic structure of the route file:


Step 6: Setting Up the API Using Replicate Documentation

I referred to the Replicate API documentation to understand the endpoints and payloads required for music or video generation.

I implemented the necessary API call using the replicate package

I customized the model and other options as per the Replicate API documentation for music or video generation.

I handled the response and returned the generated music or video data as needed.





5. API Limiter - Functionality & UI

Click here to expand:

Setting up Prisma and PlanetScale


Click here to expand:

Installing Prisma into the project

Prisma unlocks a new level of developer experience when working with databases thanks to its intuitive data model, automated migrations, type-safety & auto-completion.

npm i -D prisma
npx prisma init

Setting up an account with Planet Scale

PlanetScale is the world’s most advanced serverless MySQL platform

npm i @prisma/client
  • creating a database

  • configure to a prisma

  • get the database url (into the .env file)

  • Update the scheme.prisma file

  • creating a new file prismadb.ts in the lib folder

    • preventing multiple prisma clients initialized in the dev environment
    • creating a model for our user api limit in the shcema.prisma
    model UserApiLimit {
      id        String      @id @default(cuid())
      userId    String   @unique
      count     Int      @default(0)
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }
  • pushing this to the db with

npx prisma db push
  • adding the types into the project with
npx prisma generate
  • Checking the data in the db with shell npx prisma studio


Protecting the maximum count in the project


Click here to expand:

Creating a constants.ts file

  • setting the limit to 5
export const MAX_FREE_COUNTS = 5;
  • adding the regularly used tools and lucide-react icons into this file too



Creating an api-limit.ts file in the /libs folder;

  1. Imported Dependencies: I began by importing the necessary dependencies

  2. Implemented incrementApiLimit Function:

    • I proceeded to define the incrementApiLimit function, which was responsible for increasing the API usage count for a specific user.
    • To ensure the user was signed in, I used auth() to check their authentication status.
    • Once confirmed, I retrieved the user's API usage count from the database using prismadb.userApiLimit.findUnique().
    • If the user existed in the database, I incremented their count by 1 using prismadb.userApiLimit.update().
    • For new users, I created a new entry with a count of 1 using prismadb.userApiLimit.create().
  3. Implemented checkApiLimit Function:

    • Next, I defined the checkApiLimit function, which was responsible for checking if a user was within the free API usage limit.
    • Similar to the incrementApiLimit function, I first checked if the user was signed in using auth().
    • Once confirmed, I retrieved the user's API usage count from the database using prismadb.userApiLimit.findUnique().
    • If the user did not exist in the database or their count was less than MAX_FREE_COUNTS, which represented the maximum allowed free counts, I returned true, indicating that the user was within the free limit.
    • Otherwise, I returned false, indicating that the user had exceeded the free limit.


Updating all the API calls to make use of api-limits

Into all API Tools: Conversation, Code , Video , Audio & Image api calls

Imorting the functions from api-limits.ts;

import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";

Inside the POST(request):

  //CHECK"S & INCREASING COUNT 
  export async function POST(req:Request) {
    try{

      //previouse code 

        const freeTrial = await checkApiLimit();
        //const isPro = await checkSubscription();
        if (!freeTrial) {
            return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
        }

        //api call

        //increaese the api limit 
        await incrementApiLimit();

      //remaining code


Building the Front-end Interface for Api limit tracker


Click here to expand:

Adding a new action to the lib/api-limits.ts

  1. Imported Dependencies:
    I imported auth from @clerk/nextjs and prismadb from @/lib/prismadb.

  2. Defined the Function:
    I created the getApiLimitCount function with the async keyword.

  3. Retrieved User ID:
    Using auth(), I obtained the userId of the authenticated user.

  4. Checked User Authentication:
    To ensure the user was authenticated, I checked for the presence of userId. If it wasn't available, I returned 0.

  5. Fetched User API Limit:
    Using prismadb.userApiLimit.findUnique(), I retrieved the user's API limit based on their userId.

  6. Handled User Not Found:
    In case the user was not found in the database, I returned 0.

  7. Returned API Usage Count:
    Finally, if the user was found, I returned the count property from the userApiLimit object, representing the API usage count.


Now have to run this action inside a server component so that I can pass it into the sidebar(which is a client side component)

In the app/(dashboard)/layouts.tsx file;

  • getting the apiCount
const apiLimitCount = await getApiLimitCount();
  • passing it into the <SideBar /> component;
<Sidebar apiLimitCount={apiLimitCount/>



In the components\sidebar.tsx file;

  • create an interface to accept the apiLimitCount as a prop
  • inject the prop apiLimiCount along with the interface
  • Creating a new component to display the count (called <FreeCounter />)



Creating a new component called <FreeCounter /> - passing in apiLimitCount ;

npx shadcn-ui@latest add progress
  1. File Creation:
    I started by creating a new file named free-counter.tsx inside the components folder.

  2. Import Dependencies:
    Next, I imported the necessary dependencies, including React hooks and various components from the project's UI library, as well as the constant MAX_FREE_COUNTS from the constants file.

  3. Interface Definition:
    I defined an interface called FreeCounterProps to describe the props that the FreeCounter component would receive. Specifically, it had a apiLimitCount property of type number.

  4. Functional Component:
    Using the FreeCounterProps interface, I created the functional component FreeCounter. Within this component, I destructured the apiLimitCount prop.

  5. Hydration Trick:
    To prevent hydration issues, I used the useState and useEffect hooks. I created a state variable called mounted and set it to true after the component was mounted.

  6. Conditional Rendering:
    I implemented a conditional rendering check using an if statement. It allowed me to render the component contents only when the mounted state was true. Otherwise, I returned null.

  7. Rendering Component Contents:
    Inside the return statement, I structured the UI by rendering the Card, CardContent, Button, Progress, and Zap components, along with the necessary content.

  8. Component Export:
    Finally, I exported the FreeCounter component to make it accessible for use in other parts of the application.





6. Premium/Pro Modal UI

Click here to expand:

Creating new hook: use-pro-modal.ts;


Click here to expand:

Give us global state controls to open and close the modal; Requires a state management tool, I have chosen to use zustand again: As this app doesnt have massive requirements for state control. created a zustand store to manage the modal state, allowing other components to easily interact with the modal and control its visibility.

npm i zustand 
  1. Zustand Store:
    I used the zustand library to create a store called useProModalStore. This store manages the state for the ProModal component, including whether it is open or closed.

  2. Interface Definition:
    I defined an interface called useProModalStore, which describes the shape of the state managed by the store. It consists of two properties: isOpen (a boolean indicating whether the modal is open) and two functions onOpen and onClose (to open and close the modal, respectively).

  3. Store Creation:
    I used the create function from zustand to initialize the store. The create function takes a function as its argument, which receives a set function as its parameter.

  4. State Management:
    Inside the create function, I used the set function to manage the state of the store. The initial state is defined with isOpen set to false.

  5. Event Handlers:
    I defined two event handler functions, onOpen and onClose, which use the set function to update the isOpen state. When onOpen is called, it sets isOpen to true, and when onClose is called, it sets isOpen to false.

  6. Store Export:
    Finally, I exported the useProModal store, making it available for use in other parts of the application. Components can access the state and functions provided by this store to manage the visibility of the modal.



Creating a provider for the modal in components folder: modal-provider.tsx ;


Click here to expand:

created a ModalProvider component that takes care of rendering the ProModal component once it is mounted. This ensures that the modal is only shown when the component is ready and avoids potential issues with rendering in SSR (Server-Side Rendering) environments.

  1. Modal Provider:
    I created a ModalProvider component responsible for rendering the ProModal component, which is used to display a modal in the application.

  2. "use client":
    The component starts with the "use client" import, indicating that it is used in the client-side of the application.

  3. State Management:
    Inside the ModalProvider, I used the useState hook to manage the state of isMounted.
    This state determines whether the component is mounted or not.

  4. Mounting Detection:
    I used the useEffect hook with an empty dependency array to detect when the component is mounted.
    When the component mounts, the isMounted state is set to true.

  5. Conditional Rendering:
    Before rendering the ProModal component, there's a conditional check to ensure the component is mounted (isMounted === true).
    If it is not mounted, null is returned, effectively preventing rendering until the component is mounted.

  6. ProModal Component:
    After the conditional check, the ProModal component is rendered. The ProModal component likely handles displaying the modal content and its functionality.



Creating the: pro-modal.tsx ;


Click here to expand:

created a ProModal component that provides users with the option to upgrade to a premium subscription. The modal displays the available premium tools and allows users to initiate the subscription process by clicking the "Upgrade" button.

  1. ProModal Component:
    I created a ProModal component that is used to display a subscription upgrade dialog to users.

  2. "use client":
    The component starts with the "use client" import, indicating that it is used in the client-side of the application.

  3. State Management:
    Inside the ProModal, I used the useState hook to manage the state of loading
    indicates whether the subscription button is in a loading state or not.

  4. Subscription Function:
    I defined the onSubscribe function, which is called when the user clicks the "Upgrade" button.
    This function sends a request to the /api/stripe endpoint to initiate the subscription process using Axios.
    If successful, the user is redirected to the returned URL.

  5. Dialog Component:
    The ProModal component utilizes the Dialog component from the @/components/ui/dialog module.
    The dialog displays the subscription details to the user.

  6. Dialog Header:
    The dialog header contains the title "Upgrade to Genius" with a "pro" badge indicating the premium subscription.

  7. Dialog Description:
    The dialog description section displays a list of available tools with corresponding icons and a checkmark indicating they are part of the premium package.
    The list of tools is dynamically generated from the tools constant.

  8. Dialog Footer:
    The dialog footer contains the "Upgrade" button, which calls the onSubscribe function when clicked. The button is also disabled when the loading state is true.



Implementing the pro-modal around the app


Click here to expand:
  • Firstly, updated and imported the <ModalProvider /> into the root layout.tsx

  • Added an onClick funciton to the SideBar -> FreeCounter -> Button, that will open the Modal

  • Every time we hit a 403 error (limit reached on API calls) I very cleverly put an if statement into all the api calls that will give a status of error 403

if (!freeTrial) {
        return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
    }
  • Now just have to update the variouse routs catch blocks to open the modal if encountering status: 403 in all the AI TOOLS.

    Specifically at the app\(dashboard)\(routes)\(EACH API TOOL)\page.tsx;

    //adding the imports
    import { useProModal } from "@/hooks/use-pro-modal";
    import toast from "react-hot-toast";
    
    //calling the useProModal inside the functional component
    const proModal = useProModal();
    
    //then in the onSubmit button - catch block 
    catch (error: any) {
        if (error?.response?.status === 403) {
          proModal.onOpen();
        } else {
          toast.error("Something went wrong.");
        }
      } finally {
        router.refresh();
      }


Catching a bug: Forgot to pass api-counter into mobilesidebar


Click here to expand:

This looks like prop drilling an to an extent it is, however take into cosideration the effort to create a state management tool for this project. As well as having to navigate server/client side components.

Inside the navbar component: passing in the api counter into the navbar

const apiLimitCount = await getApiLimitCount();

//...
<MobileSidebar apiLimitCount={apiLimitCount} />

Inside the mobile-sidebar.tsx component:

  • creating an interface to receive the apiLimitCounter
  • receiving the api-counter as props
  • pass into the Sidebar component (already set up to receive the props - inface required)
<Sidebar apiLimitCount={apiLimitCount} />




7. Stripe Integration

Click here to expand:

Setting up Stripe


Click here to expand:

Account setup;

  • signed in to my dashboard

  • Created a new store and located my publishable and secret key, injected into the .env

  • Importing the stripe package into the project

    npm i stripe



Instantiating stripe in lib/stripe.ts

responsible for setting up the necessary infrastructure to integrate Stripe: - creates a new instance of the Stripe class, - passing the Stripe secret key from the environment variables - and additional configuration options.

import Stripe from "stripe"

export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
  apiVersion: "2022-11-15",
  typescript: true,
});



creating a userSubscription modal in prisma:

Before we get going, I created a userSubscription modal in the schema.prisma

Responsible for storing all the customers data required to work with stripe

model UserSubscription {
  id        String      @id @default(cuid())
  userId    String   @unique
  stripeCustomerId       String?   @unique @map(name: "stripe_customer_id")
  stripeSubscriptionId   String?   @unique @map(name: "stripe_subscription_id")
  stripePriceId          String?   @map(name: "stripe_price_id")
  stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
}

Followed by:

  • Adding the types to our project

    npx prisma generate
  • Pushing modal to the prisma db

    npx prisma db push
  • Check everything worked in the studio

    npx prisma stuido


Setting up route for stripe - backend API endpoint


Click here to expand:

In the app\api\stripe\route.ts

the GET function serves as a backend API endpoint for handling user subscriptions. It checks if the user already has a Stripe subscription and creates the appropriate session for the user, allowing them to manage or initiate a subscription for the "AI Toolbox Pro" with unlimited AI generations.

  1. GET Function:
    defined a GET function that handles the server-side logic for creating and managing Stripe subscriptions.

  2. Auth and User:
    The function imports auth and currentUser from @clerk/nextjs to authenticate the user making the request and fetch the current user data.

  3. Authorization Check:
    The function checks if there is a valid userId and a corresponding user. If not, it returns a "Unauthorized" response with a status code of 401.

  4. Stripe Subscription Check:
    The function then queries the prismadb to find a subscription associated with the current userId.

  5. Billing Portal or Checkout Session:
    If a Stripe subscription exists (userSubscription.stripeCustomerId is present)
    - the function creates a billing portal session using Stripe's billingPortal.sessions.create.
    This allows the user to manage their subscription and cancel if needed. The URL for the billing portal session is returned in the response.


If there is no existing Stripe subscription
- the function creates a new checkout session using Stripe's `checkout.sessions.create`.
This creates a new subscription for the user with a price of $20 (in USD) per month for "AI Toolbox Pro" with unlimited AI generations.
The URL for the checkout session is returned in the response.
  1. Metadata:
    Both the billing portal session and checkout session include the userId as metadata, which can be useful for tracking the user's subscription status.
    This is also important to for post checkout-session if the user completes the transaction.

  2. Error Handling:
    The function includes error handling and logs any Stripe-related errors to the console.



Setting up server-side webhook handler for Stripe events


Click here to expand:

HELP GETTING STRIPE-WEBHOOK SETUP + RUNNING FOR DEV-ENV:

Click here to expand:

usefule video

  1. Dashboard>Webhooks>Test in Local env

  2. Download the Stripe CLI, navigate too (via terminal)

  3. Run the stripe.exe. :

    .\stripe.exe
  4. Login to the cli: ./stripe login

    • (should return your pairing code & a link - follow the link )
    • (allow access in browser - terminal should update with Done!)

  5. You should see login seciton turn green at step 1

  6. Then forward events to the webhook

    ./stripe listen --forward-to localhost:3000/api/webhook

    note to update accordingly, this is where my webhook is located

  7. You should get:

    • Ready! + you webhook signing secret

  8. Copy the webhook into your .env file - STRIPE_WEBHOOK_SECRET

  9. Keep the terminal open while developing




This POST function allows your server to handle incoming Stripe webhook events and update the prismadb.userSubscription table accordingly. It ensures that your application remains synchronized with Stripe's billing and subscription events.

  1. POST Function:
    This function is a server-side webhook handler for processing incoming Stripe events.

  2. Request Body and Signature:
    The function receives a POST request with the Stripe webhook payload in the request body.
    It also retrieves the Stripe signature from the request headers.

  3. Verify Event:
    The function verifies the authenticity of the Stripe webhook event using the Stripe SDK's webhooks.constructEvent method.
    If the verification fails, it returns a response with a status code of 400, indicating a webhook error.

  4. Event Handling:
    The function then processes different types of Stripe events based on the event type.

  5. checkout.session.completed Event:
    If the event type is "checkout.session.completed", the function retrieves the subscription details from the Stripe event, including the userId from the session's metadata.
    It then creates a new record in the prismadb.userSubscription table to store the user's subscription information, including the userId, stripeSubscriptionId, stripeCustomerId, stripePriceId, and stripeCurrentPeriodEnd.

  6. invoice.payment_succeeded Event:
    If the event type is "invoice.payment_succeeded", the function retrieves the subscription details and updates the corresponding record in the prismadb.userSubscription table with the latest stripePriceId and stripeCurrentPeriodEnd.

  7. Successful Response:
    The function returns a response with a status code of 200, indicating that the webhook event has been successfully processed.

  8. Error Handling:
    If there is an error in processing the webhook events or database operations, the function returns an appropriate response with an error message.



Implementing te stripe checkout around the app


Click here to expand:

The onSubscribe function (upgrade button) in components\pro-modal.tsx

The onSubscribe function is essential for initiating the subscription process with Stripe and handling potential errors during the process. This function is responsible for handling the subscription process when the user clicks on the "Upgrade" button.

  1. Asynchronous Operation:
    The function is defined as an asynchronous function with the async keyword, allowing it to use await to wait for the API response.

  2. State Management:
    The function uses the setLoading function to set the loading state to true before making the API call. This is likely used to show a loading spinner or disable the "Upgrade" button during the API call.

  3. Stripe API Call:
    The function uses axios.get to make a GET request to the /api/stripe endpoint. This endpoint is responsible for handling the subscription process on the server-side.

  4. Response Handling:
    If the API call is successful, it retrieves the response data containing the url property, which represents the URL to redirect the user for subscription completion.

  5. Redirection:
    The function uses window.location.href to redirect the user to the url received from the API response. This takes the user to the Stripe checkout page for subscription.

  6. Error Handling:
    If an error occurs during the API call, the function catches the error in the catch block and displays a toast message with the error message "Something went wrong."

  7. Finally Block:
    The finally block is used to set the loading state back to false, ensuring that the loading state is updated correctly, regardless of whether the API call succeeds or fails.

//making stripe api call 
const onSubscribe = async () => {
  try {
    setLoading(true);
    
    const response = await axios.get("/api/stripe");

    window.location.href = response.data.url;
  } catch (error) {
    toast.error("Something went wrong");
  } finally {
    setLoading(false);
  }
}

Note to self: had an error with the db - somehow the npx prisma db push only half worked (literally) Had to reset and push again I think I may have been starting to run the dev server at the same time and it got cut off halfway



Bug Fix: Authentication issue - Middleware.tsx


Click here to expand:

The error:
in the development console:

INFO: Clerk: The request to /api/webhook is being redirected because there is no signed-in user, and the path is not included in ignoredRoutes or >publicRoutes. To prevent this behavior, choose one of:

  1. To make the route accessible to both signed in and signed out users, add "/api/webhook" to the publicRoutes array passed to authMiddleware
  2. To prevent Clerk authentication from running at all, pass ignoredRoutes: ["/((?!api|trpc))(_next|.+\..+)(.*)", "/api/webhook"] to authMiddleware
  3. Pass a custom afterAuth to authMiddleware, and replace Clerk's default behavior of redirecting unless a route is included in publicRoutes

For additional information about middleware, please visit https://clerk.com/docs/nextjs/ This log only appears in development mode, or if debug: true is passed to authMiddleware)



The reason for this:

It's trying to go to our webhook correctly however it's telling us we are not authenticated and giving us a 401 error. Which generally means, (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.

The developer console gave some really good advice and reminded me why I am getting this error, I forgot to add the webhook route into the publicRoute.



The solution:

Just as the developer console said:

To make the route accessible to both signed in and signed out users, add "/api/webhook" to the publicRoutes array passed to authMiddleware

So, in the middleware.ts:

  • simply need to add our webhook to our public routes.
export default authMiddleware({
  publicRoutes: ["/", "/api/webhook"]
});

Testing things again and it seems to be working. Got a 404 error only because I have set the return url to take the user to the xxx/settings page and I have not yet createed that However, dev console gave no errors and checking the prisma db and the user has been created. Success!!!



Creating a little helper function/util - check status of subscription


Click here to expand:

Creating a lib/subscriptions.ts

The checkSubscription function is useful for verifying whether the current user has a valid subscription, which can be used for access control or displaying subscription-related content or features within the application.

  1. checkSubscription Function:
    This function is responsible for checking the subscription status of the current user.

  2. Asynchronous Operation:
    The function is defined as an asynchronous function with the async keyword, allowing it to use await for database queries.

  3. User Authentication:
    The function uses the auth function from @clerk/nextjs to obtain the authenticated user's information, including the userId.

  4. Check for User Authentication:
    If the userId is not available (user is not authenticated), the function returns false, indicating that the user does not have an active subscription.

  5. Database Query:
    The function uses prismadb.userSubscription.findUnique to query the database and retrieve the user's subscription details.

  6. Check for User Subscription:
    If the userSubscription is not available (no subscription entry found for the user), the function returns false, indicating that the user does not have an active subscription.

  7. Subscription Validity Check:
    The function checks whether the stripeCurrentPeriodEnd date (end of the current subscription period) plus one day (DAY_IN_MS) is greater than the current date (Date.now()). This check ensures that the subscription is still valid and has not expired.

  8. Return Value:
    The function returns true if the user has an active and valid subscription. Otherwise, it returns false.



Creating route/page for settings


Click here to expand:

Creating the new route app\(dashboard)\(routes)\settings\page.tsx;

Click here to expand:

The SettingsPage component provides users with information about their subscription plan and allows them to manage their account settings. Depending on their subscription status, they can access different features or take actions such as upgrading to a Pro plan or managing their current subscription.

  1. SettingsPage Component:
    This component represents the settings page of the application.

  2. Asynchronous Operation:
    The component uses the async keyword to make an asynchronous call to checkSubscription.

  3. Importing Dependencies:
    The component imports necessary dependencies, including the Lucide React icon component (Settings), the custom Heading component, SubscriptionButton component, and the checkSubscription function from @/lib/subscription.

  4. Subscription Status Check:
    The component calls the checkSubscription function to determine if the current user has a valid subscription.
    The await keyword is used to wait for the asynchronous operation to complete.

  5. Render Content:
    The component renders the settings page content inside a div element.

  6. Heading Component:
    The component uses the custom Heading component to display the page title, description, and an icon (the Settings icon from Lucide React).

  7. Subscription Status Display:
    The component displays a message indicating whether the user is on a Pro plan or a free plan, based on the value of the isPro variable.

  8. SubscriptionButton Component:
    The component renders the SubscriptionButton component, passing the isPro value as a prop.
    The SubscriptionButton component is responsible for rendering the appropriate button based on the user's subscription status.



creating the new component components\subscription-button.tsx;

Click here to expand:

The SubscriptionButton component is used in the SettingsPage component (as seen in the previous code snippet) to allow users to manage their subscription or upgrade to a Pro plan, depending on their current subscription status.

  1. SubscriptionButton Component:
    This component represents a button that allows the user to manage their subscription or upgrade to a Pro plan based on their subscription status.

  2. Importing Dependencies:
    The component imports necessary dependencies, including axios for making API calls, useState to manage component state, Zap icon from Lucide React, and toast from react-hot-toast for displaying error messages.

  3. State Management:
    The component uses the useState hook to manage the loading state of the button.

  4. onClick Function:
    The component defines an onClick function to handle the button click event.
    When the button is clicked, this function makes an API call using axios to /api/stripe to get the URL for subscription management or upgrade.
    The user is then redirected to the returned URL using window.location.href.

  5. Button Rendering:
    The component renders a <Button> component with the appropriate variant (either "default" or "premium") based on the isPro prop.
    If isPro is true, the button text will be "Manage Subscription," and if it's false, the text will be "Upgrade."
    Additionally, if isPro is false, a <Zap> icon will be displayed next to the button text.

  6. Loading State and Disabled Prop:
    The button is disabled and shows a loading state while the API call is in progress (controlled by the loading state).





BUG FIX: Enabling the customer Portal for Test mode


Click here to expand:

The error:
in the development console:

[STRIPE_ERROR] StripeInvalidRequestError: You can’t create a portal session in test mode until you save your customer portal settings in test mode at https://dashboard.stripe.com/test/settings/billing/portal. at StripeError.generate (webpack-internal:///(sc_server)/./node_modules/stripe/esm/Error.js:22:20) at res.toJSON.then.Error_js__WEBPACK_IMPORTED_MODULE_0_.StripeAPIError.message (webpack-internal:///(sc_server)/./node_modules/stripe/esm/RequestSender.js:102:82)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
type: 'StripeInvalidRequestError', raw: { message: 'You can’t create a portal session in test mode until you save your customer portal settings in test mode at https://dashboard.stripe.com/test/settings/billing/portal.', request_log_url: 'https://dashboard.stripe.com/test/logs/req_4MCyG4JckpYwPs?t=1689939254',



The reason for this:

Stripe doesn't know if you have given the permissions for testing this in the development mode. This helps prevent accidently arraiving on this page.

Therefore need to check the settings and enable it first.



The solution:

You can follow the link given in the error message: https://dashboard.stripe.com/test/settings/billing/portal Alternativly, you can go Dashboard and search settings>billing>customer portal

Then activate the test link button - refresh the app and try again



Updating the app for premium users


Click here to expand:

Updating the upgrade button / generation count

Click here to expand:

In the components\free-counter.tsx

  • updating interface with an isPro
  • passing as props into the app, false by default
  • create an if staement that returns null
    if (isPro) {
      return null;
    }



In the components\sidebar.tsx

  • updating interface with an isPro
  • passing as props into the app, false by default
  • passing the isPro into the free-counter component
    <FreeCounter
      isPro={isPro}
      apiLimitCount={apiLimitCount} 
    />



In the app\(dashboard)\layout.tsx

  • calling the checkSubscription()
    const DashboardLayout = async ({ children }: { children: React.ReactNode }) => {
      const isPro = await checkSubscription();
  • passing into the <Sidebar isPro={isPro} ... />



Doin the exact same pattern as above with both the:

  • components\mobile-sidebar.tsx
  • components\navbar.tsx


Updating the AI Tools API calls to allow more if user is premium

Click here to expand:

For all the AI TOOLS API call will need to update the code with the following

✅ Conversation
✅ Code Generaton
✅ Audio Generation
✅ Video Generation
✅ Music Gerneration

  • add the checkSubscription function into the file;

    import { checkSubscription } from "@/lib/subscription";
  • in the checks, check if user is subscribed/premium user;

    const isPro = await checkSubscription();
  • amed the the incrementApiLimit function to only run if not a prem user;

    if (!isPro) {
        await incrementApiLimit();
    }






8. Error Handeling & adding Customer-Support bot

Click here to expand:

Error handeling with react-hot-toast notifs


Click here to expand:
  • Import the hot-toast package

    npm i react-hot-toast
  • Created a toaster-provider.tsx

    "use client";
    
    import { Toaster } from "react-hot-toast"
    
    export const ToasterProvider = () => {
      return <Toaster />
    };
  • Added the <ToasterProvider /> above the children in the app\layout.tsx

  • Replace all the .catch - console.logs with toast error instead

I actually have already been doing this, however I had forgotten to create the provider and pass it into the layout.tsx So all my erros have already been written using the hot-toast-format



Customer Support-bot with Crisp


Click here to expand:

Setting up CRISP

  • Created an account with Crisp Chat

    • signed in and made an account
    • saved the HTML head section - will need the data
    • dashboard explore and find the documentation
  • Installing the sdk into the project

    npm i crisp-sdk-web



Creating a a crisp-chat.tsx component

"use client";

import { useEffect } from "react";
import { Crisp } from "crisp-sdk-web";

export const CrispChat = () => {
  useEffect(() => {
    Crisp.configure("USE CRISP WEBSITE ID");
  }, []);

  return null;
};



Creating a a crisp-provider.tsx component and adding to app/layout.tsx

created the crisp-provider.tsx

"use client";

import { CrispChat } from "@/components/crisp-chat";

export const CrispProvider = () => {
  return <CrispChat />
};



Add just above the body in the app/layout.tsx

 <html lang="en">
  <CrispProvider />
  <body className={inter.className}>

✅ That's it, all setup - there are some integrations such as AI or bots - for now I am not going to import anything like that





9. Creating Landing Page

Click here to expand:

app\(landing)\layout.tsx


Click here to expand:

The LandingLayout component is designed to wrap the content of the landing page and apply consistent styling, such as the background color and width limitations. It allows the landing page's content to be centered and scrollable when necessary, providing a clean and visually appealing layout.

  1. LandingLayout Component:
    This component represents a layout wrapper for the landing page.

  2. Component Props:
    The component takes a single prop named children, which is of type React.ReactNode.
    This prop represents the content that will be rendered inside the layout.

  3. Main Container:
    The component returns a <main> element with a custom CSS class name "h-full bg-[#111827] overflow-auto". This sets the main container's background color to a dark gray (#111827) and allows it to scroll if the content overflows.

  4. Content Container:
    Inside the main container, there's a <div> element with the CSS classes "mx-auto max-w-screen-xl h-full w-full".
    This container centers the content horizontally within the page (using mx-auto) and limits its maximum width to max-w-screen-xl. The h-full and w-full classes ensure that the container takes up the full height and width of its parent, making sure the content is contained within the viewport.

  5. Rendering Children:
    The children prop is rendered inside the content container.
    The content passed to the LandingLayout component will be inserted here.



components\landing-navbar.tsx


Click here to expand:

The LandingNavbar component is designed to provide a clean and responsive navigation experience for the landing page. It dynamically shows the "Get Started" button based on the user's authentication status, encouraging users to take the appropriate action based on whether they are signed in or not.

  1. LandingNavbar Component:
    This component represents the navigation bar for the landing page.

  2. Using Next.js Features:
    The component imports various Next.js modules and hooks such as Image, Link, and useAuth from @clerk/nextjs.

  3. Custom Font:
    The component imports the "Montserrat" font using Montserrat({ weight: '600', subsets: ['latin'] }).

  4. Navigation Bar:
    The component returns a <nav> element with the CSS classes "p-4 bg-transparent flex items-center justify-between".
    This sets the navigation bar's padding, background color as transparent, and arranges its content in a flex container with items aligned at the center and justified between.

  5. Logo and Title:
    Inside the navigation bar, there's a link to the home page ("/") with the logo and title of the application.
    The logo is displayed using the Image component and the title is styled with a custom font class and other CSS classes.

  6. Get Started Button:
    The navigation bar includes a "Get Started" button, which is conditionally rendered based on the user's authentication status.
    If the user is signed in (isSignedIn is true), the button links to the dashboard ("/dashboard").
    Otherwise, it links to the sign-up page ("/sign-up").
    The button is styled using the Button component with the "outline" variant and a rounded border.



components\landing-content.tsx


Click here to expand:

The LandingContent component is designed to showcase user testimonials in an organized and visually appealing grid layout. The testimonials are displayed in a responsive manner, ensuring they look great across various screen sizes.

  1. LandingContent Component:
    This component represents the content section on the landing page, specifically showcasing user testimonials.

  2. Testimonials Data:
    The component defines an array called testimonials, which contains objects representing individual testimonials.
    Each testimonial object has properties like name, avatar, title, and description.

  3. Content Section:
    The component returns a <div> element with the CSS classes "px-10 pb-20".
    This sets the content section's horizontal padding and bottom padding.

  4. Testimonials Header:
    Inside the content section, there's an <h2> element with the CSS classes "text-center text-4xl text-white font-extrabold mb-10".
    This header displays the text "Testimonials" and is styled to be centered, with large text size and bold font.

  5. Testimonials Grid:
    Below the header, there's a <div> element with CSS classes that define a grid layout with responsive column numbers: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4".
    The grid will display one column on small screens, two columns on small to medium screens, three columns on medium to large screens, and four columns on large screens.
    There is a gap of 4 units between each grid item.

  6. Testimonial Cards:
    Inside the grid, the component maps through the testimonials array and creates a Card component for each testimonial.
    Each Card displays the name, title, and description of the corresponding testimonial.
    The Card component is styled with a dark background color, no border, and white text.



components\landing-hero.tsx


Click here to expand:

The LandingHero component effectively showcases dynamic text using the typewriter effect, inviting users to experience the AI tools and encouraging them to sign up for free without requiring a credit card.

  1. LandingHero Component:
    This component represents the hero section on the landing page, designed to grab the user's attention and encourage sign-ups.

  2. Typewriter Effect:
    The component imports the TypewriterComponent from the "typewriter-effect" library.
    This effect creates an animated typewriter-style text display.

  3. Dynamic Text:
    Inside the hero section, there's a <div> element with the CSS classes "text-4xl sm:text-5xl md:text-6xl lg:text-7xl space-y-5 font-extrabold".
    This element contains a heading with the text "The Best AI Tool for" and a nested <div> with the CSS classes "text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600".
    This nested div displays the typewriter effect with dynamic AI tool names ("Chatbot.", "Photo Generation.", "Blog Writing.", "Mail Writing.").
    The typewriter effect starts automatically and loops indefinitely.

  4. Subheading:
    Below the dynamic text, there's a <div> element with the CSS classes "text-sm md:text-xl font-light text-zinc-400".
    This element displays the subheading text "Create content using AI 10x faster."

  5. Call-to-Action Button:
    Below the subheading, there's a <div> element containing a Link component from Next.js that points to either the dashboard page (if the user is signed in) or the sign-up page (if the user is not signed in).
    Inside the Link, there's a Button component with the CSS class "md:text-lg p-4 md:p-6 rounded-full font-semibold" and the text "Start Generating For Free".
    The button is styled with the premium variant, indicating a premium feature but available for free.

  6. Additional Information:
    Below the call-to-action button, there's a <div> element with the CSS class "text-zinc-400 text-xs md:text-sm font-normal" that displays the text "No credit card required."




Click here to expand:

TEXT TEXT





10. Hosting on Vercel

Click here to expand:

Preparing to build and host


Click here to expand:

Pushing the latest updates to git

Signing into vercel account & creating new project

Hosting from github repo - main branch

Adding all env variables - will have to update webhooks/public url in a minute

Deploy and check if passed - encountering some errors;



Fixing deployment/build errros


Click here to expand:

Error:

Type error: '"lucide-react"' has no exported member named 'Icon'. Did you mean 'XIcon'?


Solution:

Updat to use the Xicon instead (Icon had been depricated mid project build)

//updated
import { XIcon } from "lucide-react";

interface HeadingProps {
  //updated
  icon: typeof XIcon;
}



Error:

PrismaClientInitializationError: Prisma has detected that this project was built on Vercel, which caches dependencies. This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered. To fix this, make sure to run the prisma generate command during the build process.


Solution:


Inside the package.json need to add a postinstall command ```shell "postinstall": "prisma generate" ```


Adding hosted stripe endpoint and QA testing


Click here to expand:

Adding stripe webhook listener to the hosted endpoint

  • getting the siging secret
  • adding secret to the env variables in vercel: STRIPE_WEBHOOK_SECRETSTRIPE_WEBHOOK_SECRET

Adding hte webhook url:

  • on the same page we got the webhook secret (webhook/hosted-endpoint) - copy the url
  • add it to the env variables in vercel:
    NEXT_PUBLIC_APP_URL

QA Testing functionality:

All checks passed the app is now live and working 😁





About

A Comprehensive Suite of LLM AI Tools as a SaaS

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors