🚀 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! 💪🌟
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>
Click here to see all the packages:
-
@clerk/nextjs: Integrates the Clerk authentication and user management system with Next.js applications.
-
@hookform/resolvers: Provides resolver functions for React Hook Form, enabling advanced form validation.
-
@prisma/client: Prisma's database client, used for seamless database access and query generation.
-
@radix-ui/react-avatar: Offers a customizable avatar component with built-in styles for React applications.
-
@radix-ui/react-dialog: Provides a flexible and accessible dialog component to create modal dialogs.
-
@radix-ui/react-label: A collection of label components to improve accessibility and interaction in forms.
-
@radix-ui/react-progress: Renders a progress bar with customizable styles and animation options.
-
@radix-ui/react-select: A powerful and accessible select dropdown component with various features.
-
@radix-ui/react-slot: Enables slot-based content composition for components in a React application.
-
@types/node, @types/react, @types/react-dom: TypeScript declaration files for Node.js and React.
-
autoprefixer: A PostCSS plugin that automatically adds vendor prefixes to CSS styles.
-
axios: A widely-used HTTP client for making asynchronous requests to APIs.
-
class-variance-authority: A library for class variance analysis and authority checks.
-
clsx: A utility to conditionally join CSS class names together.
-
crisp-sdk-web: Crisp chat SDK for web applications, enabling live chat support.
-
eslint: A pluggable linting utility to enforce coding standards and detect errors in JavaScript/TypeScript code.
-
eslint-config-next: ESLint configuration for Next.js projects.
-
framer-motion: A motion library for creating smooth animations and transitions in React applications.
-
lucide-react: A collection of crisp and clear SVG icons as React components.
-
next: A popular React framework for building server-rendered applications.
-
openai: A Python library for working with the OpenAI API.
-
postcss: A tool for transforming CSS styles using JavaScript plugins.
-
react: A JavaScript library for building user interfaces.
-
react-dom: The DOM-specific entry point for React applications.
-
react-hook-form: A library for flexible and efficient form validation and handling.
-
react-hot-toast: A minimalistic and customizable toast library for React.
-
react-icons: A library containing popular icon packs as React components.
-
react-markdown: Renders Markdown content as React components, useful for displaying formatted text.
-
react-parallax-tilt: Enables the creation of parallax tilt effects for React components.
-
replicate: A package for replicating data structures with modifications.
-
stripe: Stripe SDK for handling payment processing and transactions.
-
tailwind-merge: A utility for merging tailwind classes together.
-
tailwindcss: A highly customizable CSS framework for rapid UI development.
-
tailwindcss-animate: Tailwind CSS plugin for adding animation utilities.
-
typescript: A typed superset of JavaScript that enhances code quality and tooling support.
-
typewriter-effect: Creates a typewriter-like effect for text rendering in React applications.
-
zod: A TypeScript-first schema validation library for building robust data structures.
-
zustand: A state management library based on hooks, making it easy to manage global application state.
- prisma: Prisma CLI for database schema management and migrations.
Click here to expand:
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 initDuring 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.jsonIn 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 buttonOnce 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.
Click here to expand:
-
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.
-
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.
-
Creating .env Environment I created a .env file to store sensitive information, such as API keys and environment-specific variables, securely.
-
Adding Clerk Keys I added the Clerk API keys to the .env file to connect my application to the Clerk authentication service.
-
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-
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.
-
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.
-
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.
-
Updating Environment Variables for Clerk Paths I added environment variables for the paths required by Clerk, such as signIn, signUp, afterSignUp, and afterSignIn.
-
Creating General Layout File for Styling I created a general layout file to style the and pages/components consistently.
-
Adding Buttons to Home Screen I added buttons to the home screen to link users to the and pages/components for easy navigation.
-
Adding to the Dashboard I included the component on the dashboard, allowing users to access their account details and perform user-related actions.
-
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.
Click here to expand:
Working on the dashbord and creating a new sidebar (mobile and desktop versions)
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
cnandtwMergelibrary - ensuring proper way to add additonal dynamic classnames without risk of being overriddenimport { 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
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
)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
useRouterfrom 'next/navigation<Card onClick={() => router.push(tool.href)}
- using
-
Click here to expand:
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
zodResolverfrom@hookform/resolvers/zodto integrate zod withreact-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-formandzodResolverto handle form validation based on the providedformSchema. The form also has a default value for the prompt field. -
Defined a variable
isLoadingto track the form submission state, which will be used later to disable form inputs during the submission process. -
Defined an
onSubmitfunction to handle form submissions. However, the actual API call implementation is yet to be done.
Currently, theonSubmitfunction 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)
- Installing the Input component form shadcn
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 openaicreating 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
userIdusingauth()from@clerk/nextjs. I also parsed the incoming request body usingreq.json()to extract the messages field. -
Created variouse checks with responses using NextResponse:
- To ensure user authentication, I checked if
userIdwas present. If not, I returned a 401 Unauthorized response using NextResponse. - To verify that the OpenAI API key was properly configured, I checked the
apiKeyfield 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.
- To ensure user authentication, I checked if
-
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
useRouterinto to the file to refresh the browser page (note, from next/navigation) - Needed to implement
useStatefor 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.logany 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 ! 🥳🤖 - catch; and
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 />
- created a new component
-
adding a simple loading state, while message is loading
- created a new component
<loader />
- created a new component
-
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 />
Click here to expand:
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-markdownImplementation:
{/* 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>Click here to expand:
started creating skeletons for the below and developed accordingly
Click here to expand:
-
I created a new TypeScript file named constants.ts inside the image folder of the routes directory under dashboard.
-
I imported the required modules by adding the following line at the beginning of the file:
import * as z from "zod";- 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),
});- 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.
];- 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.
];Click here to expand:
-
I imported the required modules and components
-
I defined the initial state using the
useStatehook to store the generated photos in the photos state variable.
const [photos, setPhotos] = useState<string[]>([]);- 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
zodResolverto validate the form data against theformSchema:
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
prompt: "",
amount: "1",
resolution: "512x512"
}
});-
I defined the options for the amount and resolution fields, which were used in dropdowns in the form. The options were stored in the
amountOptionsandresolutionOptionsarrays -
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.
-
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.
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.
Click here to expand:
- simply just added the domain for the images received from openAI
const nextConfig = {
images: {
domains: [
"oaidalleapiprodscus.blob.core.windows.net",
]
}
}Click here to expand:
I went to the Replicate website and signed up for an account.
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.
In the Replicate dashboard, I configured my spend limits to prevent unexpected usage costs.
Installing the replicate package into the project
npm i replicateInside 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:
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.
Click here to expand:
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 initSetting 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
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;
-
Imported Dependencies: I began by importing the necessary dependencies
-
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().
-
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 codeClick here to expand:
Adding a new action to the lib/api-limits.ts
-
Imported Dependencies:
I importedauthfrom@clerk/nextjsandprismadbfrom@/lib/prismadb. -
Defined the Function:
I created thegetApiLimitCountfunction with theasynckeyword. -
Retrieved User ID:
Usingauth(), I obtained theuserIdof the authenticated user. -
Checked User Authentication:
To ensure the user was authenticated, I checked for the presence ofuserId. If it wasn't available, I returned 0. -
Fetched User API Limit:
Usingprismadb.userApiLimit.findUnique(), I retrieved the user's API limit based on theiruserId. -
Handled User Not Found:
In case the user was not found in the database, I returned 0. -
Returned API Usage Count:
Finally, if the user was found, I returned thecountproperty from theuserApiLimitobject, 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
apiLimiCountalong 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-
File Creation:
I started by creating a new file namedfree-counter.tsxinside thecomponentsfolder. -
Import Dependencies:
Next, I imported the necessary dependencies, including React hooks and various components from the project's UI library, as well as the constantMAX_FREE_COUNTSfrom theconstantsfile. -
Interface Definition:
I defined an interface calledFreeCounterPropsto describe the props that theFreeCountercomponent would receive. Specifically, it had aapiLimitCountproperty of type number. -
Functional Component:
Using theFreeCounterPropsinterface, I created the functional componentFreeCounter. Within this component, I destructured theapiLimitCountprop. -
Hydration Trick:
To prevent hydration issues, I used theuseStateanduseEffecthooks. I created a state variable calledmountedand set it totrueafter the component was mounted. -
Conditional Rendering:
I implemented a conditional rendering check using anifstatement. It allowed me to render the component contents only when themountedstate wastrue. Otherwise, I returnednull. -
Rendering Component Contents:
Inside thereturnstatement, I structured the UI by rendering theCard,CardContent,Button,Progress, andZapcomponents, along with the necessary content. -
Component Export:
Finally, I exported theFreeCountercomponent to make it accessible for use in other parts of the application.
Click here to expand:
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
zustandstore to manage the modal state, allowing other components to easily interact with the modal and control its visibility.
npm i zustand -
Zustand Store:
I used thezustandlibrary to create a store calleduseProModalStore. This store manages the state for theProModalcomponent, including whether it is open or closed. -
Interface Definition:
I defined an interface calleduseProModalStore, 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 functionsonOpenandonClose(to open and close the modal, respectively). -
Store Creation:
I used thecreatefunction fromzustandto initialize the store. Thecreatefunction takes a function as its argument, which receives asetfunction as its parameter. -
State Management:
Inside thecreatefunction, I used thesetfunction to manage the state of the store. The initial state is defined withisOpenset tofalse. -
Event Handlers:
I defined two event handler functions,onOpenandonClose, which use thesetfunction to update theisOpenstate. WhenonOpenis called, it setsisOpentotrue, and whenonCloseis called, it setsisOpentofalse. -
Store Export:
Finally, I exported theuseProModalstore, 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.
Click here to expand:
created a
ModalProvidercomponent that takes care of rendering theProModalcomponent 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.
-
Modal Provider:
I created aModalProvidercomponent responsible for rendering theProModalcomponent, which is used to display a modal in the application. -
"use client":
The component starts with the"use client"import, indicating that it is used in the client-side of the application. -
State Management:
Inside theModalProvider, I used theuseStatehook to manage the state ofisMounted.
This state determines whether the component is mounted or not. -
Mounting Detection:
I used theuseEffecthook with an empty dependency array to detect when the component is mounted.
When the component mounts, theisMountedstate is set totrue. -
Conditional Rendering:
Before rendering theProModalcomponent, there's a conditional check to ensure the component is mounted (isMounted === true).
If it is not mounted,nullis returned, effectively preventing rendering until the component is mounted. -
ProModal Component:
After the conditional check, theProModalcomponent is rendered. TheProModalcomponent likely handles displaying the modal content and its functionality.
Click here to expand:
created a
ProModalcomponent 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.
-
ProModal Component:
I created aProModalcomponent that is used to display a subscription upgrade dialog to users. -
"use client":
The component starts with the"use client"import, indicating that it is used in the client-side of the application. -
State Management:
Inside theProModal, I used theuseStatehook to manage the state ofloading
indicates whether the subscription button is in a loading state or not. -
Subscription Function:
I defined theonSubscribefunction, which is called when the user clicks the "Upgrade" button.
This function sends a request to the/api/stripeendpoint to initiate the subscription process using Axios.
If successful, the user is redirected to the returned URL. -
Dialog Component:
TheProModalcomponent utilizes theDialogcomponent from the@/components/ui/dialogmodule.
The dialog displays the subscription details to the user. -
Dialog Header:
The dialog header contains the title "Upgrade to Genius" with a "pro" badge indicating the premium subscription. -
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 thetoolsconstant. -
Dialog Footer:
The dialog footer contains the "Upgrade" button, which calls theonSubscribefunction when clicked. The button is also disabled when theloadingstate istrue.
Click here to expand:
-
Firstly, updated and imported the
<ModalProvider />into the rootlayout.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(); }
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} />Click here to expand:
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
Click here to expand:
In the app\api\stripe\route.ts
the
GETfunction 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.
-
GET Function:
defined aGETfunction that handles the server-side logic for creating and managing Stripe subscriptions. -
Auth and User:
The function importsauthandcurrentUserfrom@clerk/nextjsto authenticate the user making the request and fetch the current user data. -
Authorization Check:
The function checks if there is a validuserIdand a correspondinguser. If not, it returns a "Unauthorized" response with a status code of 401. -
Stripe Subscription Check:
The function then queries theprismadbto find a subscription associated with the currentuserId. -
Billing Portal or Checkout Session:
If a Stripe subscription exists (userSubscription.stripeCustomerIdis present)
- the function creates a billing portal session using Stripe'sbillingPortal.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.
-
Metadata:
Both the billing portal session and checkout session include theuserIdas 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. -
Error Handling:
The function includes error handling and logs any Stripe-related errors to the console.
Click here to expand:
Click here to expand:
-
Dashboard>Webhooks>Test in Local env
-
Download the Stripe CLI, navigate too (via terminal)
-
Run the stripe.exe. :
.\stripe.exe -
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!)
-
You should see login seciton turn green at step 1
-
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
-
You should get:
- Ready! + you webhook signing secret
- Ready! + you webhook signing secret
-
Copy the webhook into your .env file - STRIPE_WEBHOOK_SECRET
-
Keep the terminal open while developing
This
POSTfunction allows your server to handle incoming Stripe webhook events and update theprismadb.userSubscriptiontable accordingly. It ensures that your application remains synchronized with Stripe's billing and subscription events.
-
POST Function:
This function is a server-side webhook handler for processing incoming Stripe events. -
Request Body and Signature:
The function receives aPOSTrequest with the Stripe webhook payload in the request body.
It also retrieves the Stripe signature from the request headers. -
Verify Event:
The function verifies the authenticity of the Stripe webhook event using the Stripe SDK'swebhooks.constructEventmethod.
If the verification fails, it returns a response with a status code of 400, indicating a webhook error. -
Event Handling:
The function then processes different types of Stripe events based on the event type. -
checkout.session.completed Event:
If the event type is "checkout.session.completed", the function retrieves the subscription details from the Stripe event, including theuserIdfrom the session's metadata.
It then creates a new record in theprismadb.userSubscriptiontable to store the user's subscription information, including theuserId,stripeSubscriptionId,stripeCustomerId,stripePriceId, andstripeCurrentPeriodEnd. -
invoice.payment_succeeded Event:
If the event type is "invoice.payment_succeeded", the function retrieves the subscription details and updates the corresponding record in theprismadb.userSubscriptiontable with the lateststripePriceIdandstripeCurrentPeriodEnd. -
Successful Response:
The function returns a response with a status code of 200, indicating that the webhook event has been successfully processed. -
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.
Click here to expand:
The onSubscribe function (upgrade button) in components\pro-modal.tsx
The
onSubscribefunction 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.
-
Asynchronous Operation:
The function is defined as an asynchronous function with theasynckeyword, allowing it to useawaitto wait for the API response. -
State Management:
The function uses thesetLoadingfunction to set theloadingstate totruebefore making the API call. This is likely used to show a loading spinner or disable the "Upgrade" button during the API call. -
Stripe API Call:
The function usesaxios.getto make aGETrequest to the/api/stripeendpoint. This endpoint is responsible for handling the subscription process on the server-side. -
Response Handling:
If the API call is successful, it retrieves the response data containing theurlproperty, which represents the URL to redirect the user for subscription completion. -
Redirection:
The function useswindow.location.hrefto redirect the user to theurlreceived from the API response. This takes the user to the Stripe checkout page for subscription. -
Error Handling:
If an error occurs during the API call, the function catches the error in thecatchblock and displays a toast message with the error message "Something went wrong." -
Finally Block:
Thefinallyblock is used to set theloadingstate back tofalse, 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
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
ignoredRoutesor >publicRoutes. To prevent this behavior, choose one of:
- To make the route accessible to both signed in and signed out users, add "/api/webhook" to the
publicRoutesarray passed to authMiddleware- To prevent Clerk authentication from running at all, pass
ignoredRoutes: ["/((?!api|trpc))(_next|.+\..+)(.*)", "/api/webhook"]to authMiddleware- Pass a custom
afterAuthto authMiddleware, and replace Clerk's default behavior of redirecting unless a route is included in publicRoutesFor additional information about middleware, please visit https://clerk.com/docs/nextjs/ This log only appears in development mode, or if
debug: trueis 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
publicRoutesarray 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!!!
Click here to expand:
Creating a lib/subscriptions.ts
The
checkSubscriptionfunction 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.
-
checkSubscription Function:
This function is responsible for checking the subscription status of the current user. -
Asynchronous Operation:
The function is defined as an asynchronous function with theasynckeyword, allowing it to useawaitfor database queries. -
User Authentication:
The function uses theauthfunction from@clerk/nextjsto obtain the authenticated user's information, including theuserId. -
Check for User Authentication:
If theuserIdis not available (user is not authenticated), the function returnsfalse, indicating that the user does not have an active subscription. -
Database Query:
The function usesprismadb.userSubscription.findUniqueto query the database and retrieve the user's subscription details. -
Check for User Subscription:
If theuserSubscriptionis not available (no subscription entry found for the user), the function returnsfalse, indicating that the user does not have an active subscription. -
Subscription Validity Check:
The function checks whether thestripeCurrentPeriodEnddate (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. -
Return Value:
The function returnstrueif the user has an active and valid subscription. Otherwise, it returnsfalse.
Click here to expand:
Creating the new route app\(dashboard)\(routes)\settings\page.tsx;
Click here to expand:
The
SettingsPagecomponent 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.
-
SettingsPage Component:
This component represents the settings page of the application. -
Asynchronous Operation:
The component uses theasynckeyword to make an asynchronous call tocheckSubscription. -
Importing Dependencies:
The component imports necessary dependencies, including the Lucide React icon component (Settings), the customHeadingcomponent,SubscriptionButtoncomponent, and thecheckSubscriptionfunction from@/lib/subscription. -
Subscription Status Check:
The component calls thecheckSubscriptionfunction to determine if the current user has a valid subscription.
Theawaitkeyword is used to wait for the asynchronous operation to complete. -
Render Content:
The component renders the settings page content inside adivelement. -
Heading Component:
The component uses the customHeadingcomponent to display the page title, description, and an icon (the Settings icon from Lucide React). -
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 theisProvariable. -
SubscriptionButton Component:
The component renders theSubscriptionButtoncomponent, passing theisProvalue as a prop.
TheSubscriptionButtoncomponent 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
SubscriptionButtoncomponent is used in theSettingsPagecomponent (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.
-
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. -
Importing Dependencies:
The component imports necessary dependencies, includingaxiosfor making API calls,useStateto manage component state,Zapicon from Lucide React, andtoastfrom react-hot-toast for displaying error messages. -
State Management:
The component uses theuseStatehook to manage the loading state of the button. -
onClick Function:
The component defines anonClickfunction to handle the button click event.
When the button is clicked, this function makes an API call usingaxiosto/api/stripeto get the URL for subscription management or upgrade.
The user is then redirected to the returned URL usingwindow.location.href. -
Button Rendering:
The component renders a<Button>component with the appropriate variant (either "default" or "premium") based on theisProprop.
IfisProis true, the button text will be "Manage Subscription," and if it's false, the text will be "Upgrade."
Additionally, ifisProis false, a<Zap>icon will be displayed next to the button text. -
Loading State and Disabled Prop:
The button is disabled and shows a loading state while the API call is in progress (controlled by theloadingstate).
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
Click here to expand:
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.tsxcomponents\navbar.tsx
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
checkSubscriptionfunction 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(); }
Click here to expand:
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 theapp\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
Click here to expand:
-
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
"use client";
import { useEffect } from "react";
import { Crisp } from "crisp-sdk-web";
export const CrispChat = () => {
useEffect(() => {
Crisp.configure("USE CRISP WEBSITE ID");
}, []);
return null;
};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
Click here to expand:
Click here to expand:
The
LandingLayoutcomponent 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.
-
LandingLayout Component:
This component represents a layout wrapper for the landing page. -
Component Props:
The component takes a single prop namedchildren, which is of typeReact.ReactNode.
This prop represents the content that will be rendered inside the layout. -
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. -
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 (usingmx-auto) and limits its maximum width tomax-w-screen-xl. Theh-fullandw-fullclasses ensure that the container takes up the full height and width of its parent, making sure the content is contained within the viewport. -
Rendering Children:
Thechildrenprop is rendered inside the content container.
The content passed to theLandingLayoutcomponent will be inserted here.
Click here to expand:
The
LandingNavbarcomponent 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.
-
LandingNavbar Component:
This component represents the navigation bar for the landing page. -
Using Next.js Features:
The component imports various Next.js modules and hooks such asImage,Link, anduseAuthfrom@clerk/nextjs. -
Custom Font:
The component imports the "Montserrat" font usingMontserrat({ weight: '600', subsets: ['latin'] }). -
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. -
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 theImagecomponent and the title is styled with a custom font class and other CSS classes. -
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 (isSignedInis true), the button links to the dashboard ("/dashboard").
Otherwise, it links to the sign-up page ("/sign-up").
The button is styled using theButtoncomponent with the "outline" variant and a rounded border.
Click here to expand:
The
LandingContentcomponent 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.
-
LandingContent Component:
This component represents the content section on the landing page, specifically showcasing user testimonials. -
Testimonials Data:
The component defines an array calledtestimonials, which contains objects representing individual testimonials.
Each testimonial object has properties likename,avatar,title, anddescription. -
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. -
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. -
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. -
Testimonial Cards:
Inside the grid, the component maps through thetestimonialsarray and creates aCardcomponent for each testimonial.
EachCarddisplays the name, title, and description of the corresponding testimonial.
TheCardcomponent is styled with a dark background color, no border, and white text.
Click here to expand:
The
LandingHerocomponent 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.
-
LandingHero Component:
This component represents the hero section on the landing page, designed to grab the user's attention and encourage sign-ups. -
Typewriter Effect:
The component imports theTypewriterComponentfrom the "typewriter-effect" library.
This effect creates an animated typewriter-style text display. -
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. -
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." -
Call-to-Action Button:
Below the subheading, there's a<div>element containing aLinkcomponent 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 theLink, there's aButtoncomponent 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. -
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
Click here to expand:
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;
Click here to expand:
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;
}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 generatecommand during the build process.
Solution:
Inside the package.json need to add a postinstall command ```shell "postinstall": "prisma generate" ```
Click here to expand:
- getting the siging secret
- adding secret to the env variables in vercel:
STRIPE_WEBHOOK_SECRETSTRIPE_WEBHOOK_SECRET
- 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
All checks passed the app is now live and working 😁
