Build Full Stack Next.js AI SaaS App
Build Full Stack Next.js AI SaaS App
Requirements
You should have basic programming knowledge. But if you have some
React or NextJs experience, it will be very helpful. Contact support@i-
[Link] to receive a copy of my NextJs introductory book.
Code Examples
You can obtain the source code of the completed project by contacting
support@[Link]
Chapter 1: Introduction
In this book, we will build an AI room and home interior design application
using React, [Link], and AI. In this full-stack application, we'll learn how
to transform any room into a beautiful interior space with the help of AI.
Let's first walk through the application, and then we'll discuss the tech stack
we'll use to build it. On the landing screen, you'll see how AI can elevate
your room's interior to a new level.
When you click on ‘Get Started’, you'll be directed to the login or signup
screen.
We will use both social authentication and email/password authentication.
Once you log into the app, you'll be redirected to the dashboard. At the top,
you can view your available credits and the user profile section:
Clicking on ‘Generate AI Interior’ will take you to a screen where you can
upload an image of your room:
To change the interior of this particular room, select the room type -
whether it's a bedroom, kitchen, or office, and choose an interior design
style.
If you have any specific requirements, you can specify in ‘Enter Additional
Requirements’:
When you click ‘Generate’, a loading indicator will appear and we'll wait
for the AI to generate the interior design for your room. Once the image is
ready, you'll see both the original image and the AI-generated design side
by side in a slider which you can drag to the left and right for comparison:
Generating an AI image uses one credit, and you'll see your credit balance
subtract by one accordingly:
Back in the dashboard, you can see your AI generated room design
displayed. The dashboard is where you can view all your previous AI
generations. We store each image on a server, ensuring you can access your
designs anytime:
If you need more credits, simply click the "Buy More Credits" option at the
top:
When you land on this page, you'll see options to select the number of
credits you want to add to your account:
Let's say you want to select 25 credits, which costs $3.99. Once selected,
the PayPal payment gateway will activate. You can choose to pay either
through PayPal or with a debit/credit card.
This is a complete SaaS application with integrated PayPal payments -
allowing you to start monetizing right away. We will guide you through the
complete workflow and show you how to integrate the PayPal payment
gateway.
This is a full-stack application, and it would be great if you have some basic
experience with React or [Link]. But if not, you should still be able to get
by this book, although it would be a greater challenge. I have a beginner’s
[Link] book that you can get by contacting support@[Link].
Tech Stack
For this application’s tech stack. we'll use the React framework [Link]. For
the database, we'll use Neon PostgreSQL along with Drizzle ORM, which
allows you to perform CRUD operations seamlessly.
For authentication, we'll use Clerk Authentication, which is easy to
integrate and provides multiple authentication options like email and social
media accounts. We'll use Firebase Storage to store our images and assets.
We'll use [Link] to access AI APIs. To access this project's entire
source code, drop a mail to support@[Link].
Let’s move on to the next chapter to set up our project!
Chapter 2: Project Setup
For those new to [Link], we'll build everything from scratch. First, go to
[Link] and navigate to the docs section. There you'll find installation
instructions.
Navigate to the folder where you want to create your [Link] application.
Open a terminal in that folder (or Command Prompt if you're using
Windows). Run the command: npx create-next-app@latest
This command will install the latest version of [Link].
Press Enter, and when asked "Do you want to proceed?", type "yes". Next,
provide your project name - in this case, let's use "interior-ai " and press
Enter:
For the following prompts:
- Would you like to use TypeScript? → Select "no" (we won't be using
TypeScript in this tutorial) - Would you like to use ESLint? → Select "no"
- Would you like to use Tailwind CSS? → Select "yes"
Code extension:
The React Redux snippet tool will provide code suggestions and help you
write code more efficiently by importing default templates. We will see this
in action later.
Folder Structure Walkthrough
Let's first explore the folder structure. The most important directory is the
app folder, where we'll write all our routing, components, and code:
}
export default Home
When you save the file, you'll see the changes immediately rendered in the
browser:
Thanks to hot reload functionality, you don't need to manually refresh –
code changes automatically reflect as you save. This makes development
much more efficient.
Next, we'll install DaisyUI, a plugin for Tailwind CSS that provides a
collection of pre-built components and utility classes. It helps you build
user interfaces more quickly while still using Tailwind's utility-first
approach. Go to [Link]:
Copy the provided npm command: npm i -D daisyui@latest
In a new Terminal, navigate to your project folder and run the command to
integrate DaisyUI into your [Link] project.
Next, add daisyUI to [Link] in your project: export default {
…
plugins: [
require('daisyui'), ],
};
To use DaisyUI components, scroll through the DaisyUI documentation to
browse available components such as, Buttons, Navbar, Cards and many
others:
To use any component, copy its JSX code. For example, to use the button
component:
Copy its JSX code and in /app/[Link], paste it in your code as shown in
bold:
…
function Home() {
return (
<div>
<button className="btn">Button</button> </div>
)
}
…
When you save the changes, you'll see a beautifully styled button appear:
Next, for simplicity and to focus on the Saas aspects in this book, let’s
remove the font related code from [Link]. [Link] will thus just consist
of: import "./[Link]";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app", };
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
Name your project (e.g., "interior-ai") and click "Create" to set up your
database with the default account settings:
Within seconds, your connection string will be ready to use:
Copy the connection string, return to your application and paste it in your
.env file:
NEXT_PUBLIC_DATABASE_URL=your_connection_string_here E.g.:
NEXT_PUBLIC_DATABASE_URL=postgresql://neondb_owner:NI54ClyLAsHU@ep-broad-tooth-
[Link]/interior-ai?sslmode=require
});
A schema defines your database tables and their columns, which we'll
reference throughout our application. Let's create the users table with its
columns by adding in bold: import { pgTable, serial, varchar, integer } from 'drizzle-
orm/pg-core'
});
Code Explanation
In this schema, we have columns:
id: Auto-incrementing primary key (1, 2, 3, ...)
name: Required text field
email: Required text field
imageUrl: Required text field for user's profile image
credits: Required integer field for user's credit balance (with
default value of 3)
All the column types are imported from pg-core (pg stands for Postgres):
import { pgTable, serial, varchar, integer } from 'drizzle-orm/pg-core'
After saving the schema, we need to update our database. But first, let's set
up our [Link] file.
});
We specify the path to our schema file, i.e. './config/[Link]' – this points
to the [Link] file inside the config folder:
The system will confirm by showing 'Pulling schema from database…' and
'Changes applied':
To verify if everything is set up correctly, you can access your Neon
dashboard and navigate to the Tables section, and you'll see the newly
created 'users' table:
You should now see all the columns you defined in your schema:
You can run Drizzle Studio on your local machine with: npx drizzle-kit
studio
Once you run the command, Drizzle Studio will start locally:
Open the provided URL in your browser, and you'll see your database with
the 'users' table displayed.
You'll find all the same features here that are available in the Neon
dashboard. Additionally, Drizzle Studio offers the ability to run both SQL
queries and Drizzle queries. If you click on the schema tab, you'll see the
schema file we just created:
With this, we've set up the backend for your application. You'll gain more
hands-on experience as we develop the application further. When we start
implementing database operations like adding and fetching records, you'll
get a better understanding of how everything works together.
Chapter 4: Authentication
Let's implement authentication for our application. When users attempt to
access the dashboard, we'll verify their authentication status. If a user is not
authenticated, they will be redirected to the login/signup screen, where they
can authenticate using Google, Facebook or email.
Let's return to our application. First, we'll create a new route for the
dashboard. Inside the 'app' folder, create a new directory called 'dashboard',
and within it, create a file named '[Link]'.
rcfe stands for React Function Export Component. Rename the component
to 'Dashboard' as shown in bold: import React from 'react'
function Dashboard() {
return (
<div>Dashboard</div> )
This shows how easily you can create a route in [Link] by simply adding a
new folder.
Using Clerk for Authentication
There are several reasons for this choice: First, Clerk is free to start using.
Second, they provide a predefined UI kit, eliminating the need to write
boilerplate code since Clerk handles that for you. Additionally, Clerk offers
multiple authentication features including:
Multi-factor authentication
Email signup
SMS authentication
Password authentication
Magic link login
They also provide various social authentication options and other security
features.
To get started, go to [Link]. If you don't have an account, create one and
sign in - you'll be redirected to the dashboard. From there, you can create a
new application:
Click 'Create Application' and enter your application name – e.g., 'Interior-
AI'. You can enable any sign-in options you want by toggling them:
For example, if you want to enable Facebook login, toggle the Facebook
option. The preview on the right-hand side shows how these options will
appear on your screen.
"Click 'Create Application.' After creating the application, select the [Link]
framework:
Copy these environment variable keys. Go to your project and in the .env
file, paste the environment keys there - you'll need the Client Publisher Key
and the Client Secret Key:
Note: Right now you are in development mode. When you move to
production, you will need to change these keys.
Create a new file called [Link] in the project’s root directory and
paste the following code from the documentation:
if (isProtectedRoute(req)) {
await [Link]()
})
],
import {
ClerkProvider,
} from '@clerk/nextjs'
};
return (
<ClerkProvider>
<html lang="en">
<body>
{children}
</body>
</html>
</ClerkProvider>
);
To keep users within our application, we have to create custom sign-in and
sign-up pages. In the documentation, scroll down and click on this link:
The documentation guides us on how to add the sign-up and sign-in pages.
Copy the highlighted path for the sign-up page from the documentation:
And in (auth), create a new folder and paste the path you copied:
Copy and paste the documentation’s code into the file as shown below:
Repeat the same process for the sign-in page:
In the (auth) folder. Create the folder structure, and the [Link] file:
In the next step ‘Make the sign-up and sign-in routes public’:
We can skip this step and move on to copy the environment variables:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_vW1Uwz6E7y…
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
return (
</div>
This centers your component on the screen. You can continue customizing
the page with additional styling as needed:
If you want to modify the sign-in card layout, visit the Clerk documentation
and search for Clerk Elements:
UserButton
return (
<div>
Dashboard
<UserButton />
</div>
You will see the user profile icon. When you click on it, you'll see a menu
with options to manage your account or sign out from the application:
When a user signs into our application, we want to add them to our database
if they're new. For existing users, we'll check if they're already in our
database. To implement this, create a new file inside the app folder called
[Link]:
function Provider({children}) {
return (
<div>{children}</div>
import "./[Link]";
return (
<ClerkProvider>
<html lang="en">
<body>
<Provider>
{children}
</Provider>
</body>
</html>
</ClerkProvider>
);
Currently, nothing will change visually - everything will remain the same.
We're rendering these children through the Provider to enable sharing of data
and functionality application wide.
Because this provider is having user interactions when they log in, the
provider needs to run on the client side. Thus, add the 'use client' directive at
the top of the file in /app/[Link]: "use client"
function Provider({children}) {
return (
<div>{children}</div>
Next, when a user logs in, we need to verify them against our database. In
[Link], add in the codes in bold: "use client"
function Provider({children}) {
const {user} = useUser();
useEffect(()=>{
user&&VerifyUser();
},[user])
return (
Code Explanation
We use the useUser hook from @clerk/nextjs, which provides us with the
logged-in user's information.
useEffect(()=>{
user&&VerifyUser();
},[user])
useEffect will call the verifyUser function when the user object is available
and contains information: const VerifyUser = () => {
}
We create a new method called verifyUser which we will implement soon.
Now we need to create a new API since we'll be making a database call and
interacting with the database on the server side. Inside the app folder, create
a new folder called 'api'. Inside this api folder, we'll create a new folder
called 'verify-user' to create a new ‘verify-user’ endpoint:
return [Link]({'result':user})
useEffect(()=>{
user&&VerifyUser();
},[user])
user:user
})
[Link]([Link])
Code Explanation
First, we declare a dataResult constant and use axios to make the HTTP API
call. Make sure you installed axios by running in the Terminal:
npm i axios
}) [Link]([Link])
We pass the user information as the request body by setting the body to 'user'
and [Link]([Link]) to see the response.
If you save and return to the browser, refreshing won't show any changes
since we're not signed into the application. Navigate to the dashboard
(localhost:3000/dashboard) and sign in by selecting your account. Once
signed in, you'll see the user information in the console result:
Next, we'll check if the user already exists in our database. If not, we'll add a
new user. Let's wrap this logic in a try-catch block. Add in bold: import {
NextResponse } from "next/server";
import { db } from "../../../config/db"; import { Users }from
"../../../config/schema";
try{
from(Users).
where(
eq([Link],
user?.[Link]) )
catch(e){
return [Link]({'result':user})
Code Explanation
from(Users).
where(
eq([Link],
user?.[Link]) )
user:user
})
Save the changes and let's add a [Link] to display userInfo to verify
whether we're getting any results back from the database query. In apiverify-
user/[Link], add in bold: export async function POST(req){
try{
from(Users).
where(
eq([Link],
user?.[Link]) )
[Link]("User:", userInfo); }
catch(e){
return [Link]({'result':user})
Let's check the terminal since the console output appears there, as this is
running server-side. Looking at the console, we can see the user object is
empty, which means this user doesn't exist in our database yet:
Let's write a conditional check that if [Link] is empty (meaning the
user doesn't exist), we'll save the user information. Add in the codes in bold:
try{
from(Users).
where(
eq([Link],
user?.[Link])
[Link]("User:", userInfo);
if(userInfo?.length == 0){
name: user?.fullName,
email: user?.[Link],
imageUrl: user?.imageUrl
}).returning({Users})
return [Link]({'result':SaveResult[0].Users})
return [Link]({'result':userInfo[0]})
}catch(e){
return [Link]({error:e})
}
Code Explanation
}).returning({Users})
After inserting, we use .returning() to return the newly inserted record after
the insertion operation is completed. i.e. SaveResult will contain the
complete user record that was just inserted into the database.
if(userInfo?.length == 0){
return [Link]({'result':SaveResult[0].Users})
return [Link]({'result':userInfo[0]})
}catch(e){
return [Link]({error:e})
Let's save and run the application. Looking at the result now, we can see the
complete user information being returned:
If we check Drizzle Studio and refresh, you'll see the new user record has
been added:
This demonstrates how we can insert a new record and verify if a user exists.
Now when we refresh, it returns the existing user data since it finds the user
in the database and executes the return statement instead of the insert logic.
When it's a new user, it returns the newly inserted user.
Now that we've verified the user, we need to store their information so it can
be shared throughout the application. This way, we won't need to make an
API call every time user accesses a new page, and we'll have access to
important fields like 'credits' that our application requires:
called '[Link]':
We use an underscore prefix ‘_’ for the _context folder name to tell [Link]
not to treat this as a route, as it would automatically create a route for folders
without underscores.
function Provider({children}) {
})
setUserDetail([Link]);
return (
</[Link]>
Code Explanation
user:user
})
setUserDetail([Link]);
}
When we receive user information, we update userDetail by setting it to
[Link].
</[Link]>
We pass the userDetail state as a default value to the provider and wrap our
application content inside it.
Now we're ready to use this context. Throughout the application, you can
now access the user details by using UserDetailContext. We will illustrate
this later.
This completes our setup of authentication, saving users to the database, and
making user information accessible across the application.
Chapter 5: Dashboard
Let's implement the dashboard with these sections: - Header section
- Welcome section that displays the username and provides a button to
generate AI Interior - Listing section to list the AI generated room images
Inside the dashboard directory, create a layout file [Link]. Use a default
‘rcfe’ template and name it 'DashboardLayout':
This layout will be specific to the dashboard and render its children
components. We add the children prop to handle nested content.
Let's add a header to our layout. Create a new folder called '_components'
inside the dashboard directory - this will store all dashboard-related
components. First, create the header component [Link] in this folder.
Have a ‘rcfe’ default template, and save it: import React from 'react'
function Header({ children }) {
return (
<div>
Header
</div>
);
}
export default Header
Then we'll import the header into our /dashboard/[Link]: import React
from 'react'
import Header from './_components/Header'
function DashboardLayout({ children }) {
return (
<div>
<Header />
{children}
</div>
);
}
…
Now, let's look at the app. The header will be visible:
In the header, we want to add the app name on the left side:
And on the right, we will have an option to buy more credits, display the
user's available credit balance, and show the user's profile image.
We will use the Navbar component from DaisyUI for this. Go to
[Link]/components/navbar/:
}
export default Header
We changed the text to "Interior AI". You'll see “Interior AI” on the screen:
Next in [Link], add a new div for the right-hand side content and in it,
we'll add a UserButton. Add in bold: "use client"
import React from 'react'
import { UserButton } from '@clerk/nextjs'
function Header() {
return (
<div className="navbar bg-base-100"> <div className="flex-1">
<a className="btn btn-ghost text-xl">Interior AI</a> </div>
<div className="flex-none">
<UserButton></UserButton>
</div>
</div>
)
}
Now you can see the badge displaying the user's credits (currently
hardcoded to 3):
Code Explanation
const {userDetail,setUserDetail}=useContext(UserDetailContext); First, define userDetail in
curly braces, then use the useContext hook with UserDetailContext. Remember that useContext
is a React Hook that allows us to share context values (e.g. user details) from a parent
component anywhere in your component tree.
Save the changes and when you refresh the page, you can see it now shows
five credits:
This demonstrates how we can share information through context.
Listing Section
Now our header is complete. Next, we'll implement the Listing section
component:
Go to the _components folder and create a new file for our Listing
component, [Link]:
Give it a default template:
import React from 'react'
function Listing() {
return (
<div>Listing</div>
)
}
export default Listing
}
export default Dashboard
);
}
…
We wrap the children prop of the DashboardLayout component to apply the
same styling to all routes rendered through this dashboard layout.
With className="p-4 md:p-6 lg:p-8 max-w-7xl mx-auto", the content has progressively
more padding as the screen gets larger and is centered. The layout thus
adjusts to different screen sizes.
}
export default Listing
Code Explanation
const {user} = useUser();
Create a constant called 'user' and set it equal to the useUser hook.
Remember to convert this to a client component with "use client" because
you're using client-side functionality i.e. the useUser hook from Clerk.
Hello, {user?.fullName}
function Listing() {
const {user} = useUser();
const [userRoomList, setUserRoomList] = useState([]); return (
<div>
<div className="flex justify-between items-center text-xl font-bold"> Hello,
{user?.fullName}
<button className="btn btn-primary">
+ Generate AI Interior
</button>
</div>
{userRoomList?.length == 0 ?
<div>
No Interior AI Designs Generated Yet
</div>
:
<div>
</div>
}
</div>
)
}
export default Listing
Code Explanation
const [userRoomList, setUserRoomList] = useState([]); We create a state array for storing
generated interior data.
{userRoomList?.length == 0 ?
<div>
No Interior AI Designs Generated Yet
</div>
:
<div>
</div>
When users first visit the dashboard, they'll see this screen since they
haven't created anything yet. They can start generating interiors by clicking
the button. We will work on displaying generating interiors soon. But
before that, we need to have a form to specify interior options before
generation. Let’s work on that in the next chapter.
Chapter 6: Form UI
When users click on ‘+ Generate AI Interior’, they'll be redirected to a new
route called 'Create New.' On this screen, users can upload an image of the
room they want to redesign. They'll also have options to: - select the room
type from a dropdown menu (e.g. Living Room, Bedroom, Kitchen) -
choose a design style (e.g. modern, industrial)
It will look something like:
When you click 'Generate', our AI will begin working on redesigning the
uploaded room.
Now, let's return to our application. First, we create a new route ‘createnew’
under the dashboard by creating a 'createnew' folder. Inside it, create a
'[Link]' file:
Fill it with default code:
import React from 'react'
function CreateNew() {
return (
<div>Create New</div>
)
We use the Link tag imported from 'next/link'. The Link component is
equivalent to the anchor tag in HTML.
<Link href={'dashboardcreatenew'}>
Link requires an href attribute where you need to provide the path. In this
case, the path is "dashboard/createnew". We simply wrap our button with
this Link component.
}
export default CreateNew
In CreateNew, let’s add a div that will contain our image upload section by
adding in bold: "use client"
import React from 'react'
import ImageSelection from './_components/ImageSelection'
function CreateNew() {
return (
<div>
<h2 style={{
color: 'purple',
fontWeight: 'bold',
fontSize: '2.5rem', // Makes it much bigger
textAlign: 'center' // Centers the text
}}>
Create AI Interior
</h2>
<div>
<ImageSelection />
</div>
</div>
)
}
Inside the createnew directory, let's create a new folder called _components
to store all components related to this page:
This would create a two-column layout where the columns have equal
width, some space between them, and the entire grid has padding around its
edges.
In …/createnew/_components/[Link], fill in the below codes:
"use client"
import React from 'react'
function ImageSelection() {
return (
<div>
<label>Select Image of your room</label> <div>
<input type="file"
accept="image/*"
className="file-input file-input-bordered w-full max-w-xs"
/>
</div>
</div>
)
}
export default ImageSelection
Code Explanation
We add a label that says "Select image of your room." Below that, we add a
File Input taken from [Link]/components/file-input/:
<input type="file"
accept="image/*"
className="file-input file-input-bordered w-full max-w-xs"
/>
We want to restrict users to selecting only image files, not other file types
like PDFs, thus we add an accept parameter to only allow image files.
}
return (
<div>
<label>Select Image of your room</label> <div>
<input type="file"
accept="image/*"
className="file-input file-input-bordered w-full max-w-xs"
onChange={onFileSelected}
/>
</div>
</div>
)
We attach this method to our input using the onChange event handler.
When you run the app now and select an image, in the developer console,
you can see the file information logged:
function ImageSelection() {
const [selectedImage, setSelectedImage] = useState(null); const onFileSelected =
(event) =>{
const file = [Link][0];
if (file) {
const imageUrl = [Link](file);
setSelectedImage(imageUrl);
}
}
return (
<div>
<label>Select Image of your room</label> <div>
<input type="file"
accept="image/*"
className="file-input file-input-bordered w-full max-w-xs"
onChange={onFileSelected}
/>
</div>
{selectedImage && (
<div style={{
marginTop: '20px',
maxWidth: '500px',
width: '100%'
}}>
<img
src={selectedImage}
alt="Selected room"
style={{
width: '100%',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}
/>
</div>
)}
</div>
)
Code Explanation
"use client"
import React, {useState} from 'react'
We make this component client-side because we will use useState to store
the selected file.
function ImageSelection() {
const [selectedImage, setSelectedImage] = useState(null); const onFileSelected =
(event) =>{
const file = [Link][0];
if (file) {
const imageUrl = [Link](file);
setSelectedImage(imageUrl);
We create an image file state. When a file is selected, we'll store it using
setSelectedImage(imageUrl) .
}}
/>
</div>
)}
When an image file is selected, you can see the preview appears:
}
…
}
return (
<div>
<h2 style={{
…
}}>
Create AI Interior
</h2>
<div className="grid grid-cols-2 gap-8 p-6">
<div>
<ImageSelection
selectedFile={(value) =>
onHandleInputChange(value,'image')}
/>
</div>
…
And in ImageSelection, we'll call this method with the file value and 'image'
as the field name. We will revisit and understand better how it works.
}
export default RoomType
Code Explanation
<div>
<label>Select Room Type</label>
<div>
<select
…
className="select select-bordered w-full max-w-xs"
required
defaultValue="" // Set default value to empty string > <option
value="" disabled>Select Room Type</option> <option value="Living Room">Living
Room</option> <option value="Bedroom">Bedroom</option> …
</select>
</div>
</div>
We add a label "Select Room Type" and add the below room types as
options: - Living Room
- Bedroom
- Kitchen
- Office
- Bathroom
We set the initial option value to "Select Room Type" and disable it to make
it mandatory for a user to select a drop down value:
DesignType
Let's create a new component for the design type. We'll add a file in
{
name: 'Modern',
image: '/[Link]',
},
{
name: 'Industrial',
image: '/[Link]',
},
{
name: 'Bohemian',
image: '/[Link]',
},
{
name: 'Traditional',
image: '/[Link]',
},
{
name: 'Rustic',
image: '/[Link]',
}, {
name: 'Minimalist',
image: '/[Link]',
]
return (
<div>
<label>Select Interior Design Type</label> </div>
)
}
export default DesignType
The first one will be 'modern' with its corresponding image path. I've
included some images (available in the source code – contact support@i-
[Link]) for different styles like modern, minimalistic, and industrial.
So, make sure you have these images in your public folder:
Then we'll map through our designs array in
…/createnew/_components/[Link], by adding in bold: function
DesignType() {
const designs = [
…
]
return (
<div>
<label>Select Interior Design Type</label> <div className="grid grid-cols-4 gap-4">
{[Link]((design, index) => (
<div key={index}>
<img src={[Link]} />
<h2 className="text-center mt-2 font-medium"> {[Link]}
</h2>
</div>
))}
</div>
</div>
)
}
export default DesignType
Code Explanation
We also arrange the images in a grid format with className="grid grid-cols-4 gap-
4". This creates a row with 4 equally-sized and equally-spaced grid items.
<h2 className="text-center mt-2 font-medium"> {[Link]}
</h2>
Below each image, we display the design name using [Link]. Now we
can see all the different styles: Bohemian, Industrial, and the others.
Handling Hovering Effects
To handle hovering effects, add in bold: function DesignType() {
…
…
<div className="grid grid-cols-4 gap-4">
{[Link]((design, index) => (
<div key={index} className="cursor-pointer hover:opacity-80 transition-
opacity"> <img src={[Link]} />
<h2 className="text-center mt-2 font-medium"> {[Link]}
</h2>
…
Whenever you hover over any image, it becomes slight transparent with a
smooth transition animation and shows a pointer cursor to indicate it's
clickable.
State Management
Now, when you select any of these design types, we need to save that
selection in state. So add in bold below: import React,{ useState } from 'react'
function DesignType() {
const designs = [
…
]
const [selectedOption, setSelectedOption] = useState(); return (
<div>
<label>Select Interior Design Type</label> <div className="grid grid-cols-4 gap-4">
{[Link]((design, index) => (
<div key={index}
onClick={()=>{
setSelectedOption([Link])
}}
className="cursor-pointer hover:opacity-80 transit…"> <img src={[Link]}
/>
<h2 className="text-center mt-2 font-medium"> {[Link]}
</h2>
</div>
}}
className="cursor-pointer hover:opacity-80 … "> <div className={`aspect-
square w-full
relative overflow-hidden rounded-lg
${selectedOption === [Link] ?
'ring-4 ring-blue-500' : ''}`}>
<img src={[Link]} className="absolute inset-0
w-full h-full object-cover rounded-lg" /> </div>
<h2 className="text-center mt-2 font-medium"> {[Link]}
</h2>
</div>
))}
</div>
…
Code Explanation
We add a blue ring to the selected design image if [Link] matches the
selectedOption: ${selectedOption === [Link] ? 'ring-4 ring-blue-500'
: ''}
Let's see how it looks. When I select an item, you can see the border being
added.
Now when you select anything, you can see the selection clearly.
But we still need to pass this selection up to the parent component. Add in
bold: function DesignType({selectedDesignType}) {
…
return (
<div>
<label>Select Interior Design Type</label> <div className="grid grid-cols-4 gap-4">
{[Link]((design, index) => (
<div key={index}
onClick={()=>{
setSelectedOption([Link]);
selectedDesignType([Link])
}}
…
In the onClick, we make another call to selectedDesignType and pass
[Link]. We'll pass selectedDesignType up to the parent component in
../dashboard/createnew/[Link]. Add in bold:
../dashboard/createnew/[Link] …
function CreateNew() {
…
return (
…
<div>
<RoomType
selectedRoomType={(value)=>
onHandleInputChange(value,'roomType')}
/>
<DesignType
selectedDesignType={(value)=>
onHandleInputChange(value,'designType')}
/>
</div>
…
Inside the parent component where we render DesignType, we call
onHandleInputChange, passing both the value and the field name
("designType").
}
export default AdditionalReq
To capture the user entered value and pass it back to the parent component,
add the following: function AdditionalReq({additionalReqInput}) {
return (
<div>
…
…
<textarea className="textarea textarea-bordered h-24 w-full"
onChange={(e) => additionalReqInput ([Link])}> </textarea>
</div>
)
}));
[Link](formData)
}
…
return (…)
Code Explanation
const [formData, setFormData] = useState([]);
}));
[Link](formData) }
We update formData by using the previous state value and assign the new
value to the corresponding field name.
To see the data, we [Link](formData) to check what's being saved.
Setup Firebase
Let's first set up Firebase to store our images. Go to [Link]. If
you don't have an account, create one - Firebase is free to use and provides
5GB of free storage, plus it's very easy to integrate.
Click on ‘Go to console’:
};
const app = initializeApp(firebaseConfig);
Make sure you replace the above with your own config code.
Then, we'll export a storage reference by adding:
import { initializeApp } from "firebase/app";
import { getStorage } from "firebase/storage";
const firebaseConfig = {
…
…
};
Storage
Now, let's go to the Firebase console and navigate to Storage (you will be
asked a serious of questions, just reply with the default options). Inside
Storage, we'll create a new folder called "interior-ai".
Once you create the folder, you'll see it in your storage. You can upload
files manually here, but we want to use the Firebase SDK to handle uploads
programmatically from code.
});
}));
}
const generateAIImage = async () => {
}
return (
…
<RoomType …/>
<DesignType …/>
<AdditionalReq …/>
<button onClick={generateAIImage} …>Generate</button> <p
className="…">Each generation costs one credit</p> …
We' add the onClick handler for the ‘Generate’ button to call the
generateAIImage method.
Inside generateAIImage, we'll make an API call using axios: const
generateAIImage = async () => {
const result = await [Link]('apiinterior-ai', formData); [Link]("result",result);
When we do this, you'll see the response in the console, with the data
showing "result: hello" - confirming our API endpoint is working.
Saving our Image to Firebase
Now before making the API call, we need to save our image to Firebase. In
../create-new/[Link], let's create a new method called
saveRawImageToFirebase. Add the codes: …
…
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'
import { storage } from '../../..configfirebaseConfig'
function CreateNew() {
…
const onHandleInputChange=(value,fieldName)=>{
…
}
const generateAIImage = async () => {
…
}
const saveRawImageToFirebase = async () => {
const fileName = `${[Link]()}_raw.png`;
const imageRef = ref(storage, `interior-ai/${fileName}`); await
uploadBytes(imageRef,[Link]).then(resp=>{
[Link]('File Uploaded...')
})
const downloadUrl=await getDownloadURL(imageRef);
[Link](downloadUrl);
return downloadUrl;
}
return (
…
Code Explanation
const saveRawImageToFirebase = async () => {
const fileName = `${[Link]()}_raw.png`;
You might notice we're not showing any loading state right now, so users
can't tell if it's actually uploading or not. We will implement that loading
functionality later.
The URL is fetched from Firebase Storage, and is the URL of the picture
we just uploaded.
Calling ‘interior-ai’ API
After getting the URL, we need to pass it to the interior-ai API call we
implemented earlier. In ../create-new/[Link], add the below: const
generateAIImage = async () => {
const rawImageUrl=await saveRawImageToFirebase();
const result = await [Link]('apiinterior-ai', {
imageUrl:rawImageUrl,
roomType:formData?.roomType,
designType:formData?.designType,
additionalReq:formData?.additionalReq,
})
[Link]("result",result);
We pass in the raw image URL, room type, design type and additional
requirements from the form data. Now that we pass this information to the
route, we need to retrieve it next.
In ../api/interior-ai/[Link], we'll destructure the request data by add:
export async function POST(request) {
const {imageUrl, roomType, designType, additionalReq} = await [Link]() return
[Link]({
result: "hello"
});
}
This is how we can destructure all the fields from whatever you passed in
the request body.
Convert Image using AI
The next step is to convert the image using AI. We're going to use
Replicate, which is a comprehensive source of AI APIs.
First, if you don't have an account, create a free one at [Link]. Click
on "Get Started".
Go to ‘Account Settings’:
For this particular model’s cost, you can generate up to 185 images for just
one dollar, which is quite generous:
Note: So don’t worry about incurring huge API costs while going through
this book. It will probably just cost a few cents.
Keep in mind that different models in Replicate have different price tags.
Now, to use this model, let's go to the API section and select [Link]:
Next, we copy the Replicate API Key:
and specify it in our .env file (remember to add ‘NEXT_PUBLIC’): …
…
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_REPLICATE_API_TOKEN=r8_F9Hq…
});
export async function POST(request) {
});
Our Replicate instance is now ready to use. We next need to define both the
input and output sections:
Copy these lines from the documentation and paste them in /api/interior-
ai/[Link]: …
const replicate = new Replicate({
auth: [Link].NEXT_PUBLIC_REPLICATE_API_TOKEN
});
export async function POST(request) {
}
catch(e){
}
return [Link]({
result: "hello"
});
}
Code Explanation
const resp=await [Link](imageUrl,{responseType:'arraybuffer'}); We use [Link] with
imageURL and set the response type as 'arraybuffer' which tells axios to return the raw binary data as
an ArrayBuffer instead of parsing it as JSON or text.
const base64ImageRaw=[Link]([Link]).toString('base64'); We take the binary data
([Link]) and create a Buffer from it. We convert the buffer to a base64 string encoding. Base64
encoding represents binary data using a set of 64 ASCII characters, making it safe to transmit in text-
based protocols return "data:image/png;base64,"+base64ImageRaw;
Call ConvertImageToBase64
Let's call ConvertImageToBase64 by adding the below: import { storage } from
'../../..configfirebaseConfig'
import { getDownloadURL, ref, uploadString } from "firebase/storage"; …
…
export async function POST(request) {
…
try{
const input = {
image: imageUrl,
prompt: 'A ' + roomType + " with a " + designType + " style interior " + additionalReq };
const output = await [Link]("adirik/interior-design:766…38",{ input });
const base64Image=await ConvertImageToBase64(output); const
fileName=[Link]()+'.png';
const storageRef=ref(storage,'interior-ai/'+fileName); await
uploadString(storageRef,base64Image,'data_url'); const downloadUrl=await
getDownloadURL(storageRef);
[Link](downloadUrl);
return [Link]({'result':downloadUrl});
}
catch(e){
return [Link]({
error: e
});
Code Explanation
const base64Image=await ConvertImageToBase64(output); We create a base64Image
constant that awaits our ConvertImageToBase64 function, passing in our URL as the parameter.
const fileName=[Link]()+'.png';
Now we'll get the downloadURL and log downloadURL to the console and
return it in our response.
});
export const AiGeneratedImage = pgTable('aiGeneratedImage', {
id: serial('id').primaryKey(),
roomType: varchar('roomType').notNull(),
designType: varchar('designType').notNull(),
orgImage: varchar('orgImage').notNull(),
aiImage: varchar('aiImage').notNull(),
userEmail: varchar('userEmail')
})
This updates the schema. After the changes are applied, stop and run
drizzle-kit studio again: npx drizzle-kit studio
In Drizzle Kit Studio, we should see our new table called
'aiGeneratedImage'.
In our /api/interior-ai/[Link], let's add the database insert. Add the below:
…
…
import { db } from "../../..configdb";
import { AiGeneratedImage } from "../../..configschema"; export async function POST(request)
{
…
try{
const input = {
image: imageUrl,
prompt: …
};
…
const storageRef=ref(storage,'interior-ai/'+fileName); await
uploadString(storageRef,base64Image,'data_url'); const downloadUrl=await
getDownloadURL(storageRef);
[Link](downloadUrl);
const dbResult=await [Link](AiGeneratedImage).values({
roomType:roomType,
designType:designType,
orgImage:imageUrl,
aiImage:downloadUrl,
userEmail:userEmail
}).returning({id:[Link]});
[Link](dbResult);
return [Link]({'result': downloadUrl}); }
catch(e){
return [Link]({
error: e
});
});
…
…
Back inside /api/interior-ai/[Link], we'll destructure the user email from
the request: …
export async function POST(request) {
const { imageUrl, roomType, designType, additionalReq, userEmail } =
await [Link]()
…
In Drizzle Studio, we've got the email address along with both the original
and AI-generated images URLs. That's how you can save the results to our
database.
The next thing we need to do is add a loading indicator by the AI generation
is going on. This will let users know when the system is making API calls
in the background. Let’s implement this in the next chapter.
Chapter 8: Custom Loading
When we click ‘Generate’, while waiting for the AI Image generation on
the server-side, we'll show a loading indicator using a DaisyUI Loading
component ([Link]/components/loading/):
}
export default CustomLoading We'll display the loading indicator only when there's a server-side API
call happening. For this, in /create-new/[Link], we'll maintain a state called 'loading'. Add in bold:
…
import CustomLoading from './_components/CustomLoading'
function CreateNew() {
const {user}=useUser(); const [formData, setFormData] = useState([]); const [loading,
setLoading] = useState(false); …
We'll set its initial value to false. When the image starts generating, we'll set
the loading state to true. Add in bold: const generateAIImage = async () =>
{
setLoading(true);
const rawImageUrl=await saveRawImageToFirebase(); const result = await
[Link]('apiinterior-ai', {
…
});
setLoading(false); [Link]("result",[Link]); }
…
When we get the generated result, we'll set loading back to false.
In ../create-new/[Link], we'll render the custom loading indicator when
loading is true, else - render the form as before. Add in bold: function
CreateNew() {
...
const generateAIImage = async () => {
setLoading(true);
...
...
setLoading(false); [Link]("result",[Link]); }
return (
<div>
<h2 style={{
...
}}>
Create AI Interior </h2>
{loading ? (
<CustomLoading /> ) : (
<div className="grid grid-cols-2 gap-8 p-6"> <div className="p-4 ...">
<ImageSelection .../> </div>
<div className="rounded-lg ..."> <RoomType .../> <DesignType .../>
<AdditionalReq .../> ...
</div>
</div>
)}
</div>
)
Let's save it and try generating an image. We have this loading animation
When we get the result, we'll set the AI output image by adding: const
generateAIImage = async () => {
setLoading(true);
const rawImageUrl=await saveRawImageToFirebase(); const result = await
[Link]('apiinterior-ai', {
…
…
});
setAiOutputImage([Link]) setLoading(false);
[Link]("result",[Link]);
}
});
setAiOutputImage([Link]) setOpenOutputDialog(true);
setLoading(false);
We don't need the button, so I'll remove it. We'll just show the dialog. Fill in
[Link] with the below (you can get the source code from
support@[Link]): import React, {useEffect} from 'react'
import ReactBeforeSliderComponent from 'react-before-after-slider-component'; import 'react-
before-after-slider-component/dist/[Link]'; function AiOutputDialog({openDialog, setOpenDialog,
orgImage,aiImage}) {
useEffect(() => {
if (openDialog) {
[Link]('my_modal_1').showModal(); }
}
, [openDialog]);
const handleClose = () => {
setOpenDialog(false);
};
return (
<div>
<dialog id="my_modal_1" className="modal"> <div className="modal-box">
<h3 className="font-bold text-lg">Result:</h3> <ReactBeforeSliderComponent
firstImage={{
imageUrl:aiImage
}}
secondImage={{
imageUrl:orgImage
}}
/>
<div className="modal-action"> <form method="dialog">
<button className="btn" onClick={handleClose} >Close</button> </form>
</div>
</div>
</dialog>
</div>
)
}
export default AiOutputDialog
Code Explanation
import ReactBeforeSliderComponent from 'react-before-after-slider-component'; import 'react-
before-after-slider-component/dist/[Link]'; Here we need to import ReactBeforeSliderComponent
first. We copy the CSS along with the import for that component.
}}
secondImage={{
imageUrl:orgImage
}}
/>
We need to ensure that when passing the image URL, you pass it inside a
field called imageUrl. We have the first image and second image to pass.
We will talk about how to get aiImage and orgImage later.
<ReactBeforeSliderComponent
firstImage={{
…
secondImage={{
…
/>
<div className="modal-action"> <form method="dialog">
<button className="btn"
onClick={handleClose} >Close</button> </form>
</div>
After that, we'll add a Close button. When the user clicks close, we'll set
openDialog to false.
function AiOutputDialog({openDialog, setOpenDialog, orgImage,aiImage}) {
useEffect(() => {
if (openDialog) {
[Link]('my_modal_1').showModal(); }
};
We'll pass the openDialog prop above. When the user clicks close, we'll set
openDialog to false. useEffect detects if there’s a change in value in
openDialog, and if openDialog is true, show the modal.
…
{loading ? (
<CustomLoading />
):(
<div className="grid grid-cols-2 gap-8 p-6"> …
<div className="rounded-lg shadow-sm space-y-6"> <RoomType …/>
<DesignType …/>
<AdditionalReq …/>
…
<p className="text-gray-500"> Each generation costs one credit
</p>
</div>
<AiOutputDialog
openDialog={openOutputDialog}
setOpenDialog={setOpenOutputDialog}
orgImage={orgImage}
aiImage={aiOutputImage}
/>
</div>
)}
With this, we now can pass orgImage and the aiImage, to the
AiOutputDialog component: <AiOutputDialog
openDialog={openOutputDialog}
setOpenDialog={setOpenOutputDialog}
orgImage={orgImage}
aiImage={aiOutputImage}
/>And that’s why back in /create-new/_components/AiOutputDialog, we
can pass in aiImage for the first image, and oriImage for the second image:
function AiOutputDialog({openDialog, setOpenDialog, orgImage, aiImage}) {
…
return (
<div>
…
<ReactBeforeSliderComponent
firstImage={{
imageUrl:aiImage
}}
secondImage={{
imageUrl:orgImage
}}
/>
We have the details like the original image, AI image, room type, and user
information.
This chapter shows how you can generate AI images and display them in a
professional looking way. This react-before-after-slider helps us present a
comparison between the old and new images.
Chapter 10: Display User’s Images
Now we'll display all the rooms the user has created on their dashboard:
:
<div>
</div>
}
</div>
)
}
}
…
function Listing() {
…
useEffect(()=>{
user&&GetUserRoomList(); },[user])
const GetUserRoomList=async()=>{
…
Let’s now test our app. Go to the dashboard and check the console, and
we've got some records:
Now when I save this and refresh the screen, you'll see we're not displaying
“No Interior AI Designs Generated Yet” anymore because we have data.
Since we have userRoomList, it will jump to this new block in bold:
{userRoomList?.length == 0 ?
<div className="flex justify-center items-center h-full text-2xl text-gray-500 mt-32">
No Interior AI Designs Generated Yet </div>
:
<div>
…{/* Listing */}
</div>
Let's now iterate through the Room List information. We'll map through
userRoomList as shown in bold: …
…
import RoomDesignCard from './RoomDesignCard'; function Listing() {
…
…
{userRoomList?.length == 0 ?
<div className="…">
No Interior AI Designs Generated Yet </div>
:
<div>
{[Link]((room,index)=>(
<div key={index}>
<RoomDesignCard room={room}></RoomDesignCard> </div>
))}
</div>
}
export default RoomDesignCard When you run the app now, you can see on the screen, we are
rendering the text “RoomDesignCard”:
}}
secondImage={{
imageUrl:room?.orgImage,
}}
/>
</div>
)
We also add emoji icons to these labels. Feel free to make any additional
styling changes you'd like:
))}
</div>
When a user clicks on a room, it selects the current selected room and sets
openDialog to true which render the AiOutputDialog component (similar to
what we did earlier when we generated the image). Add in bold: …
…
import AiOutputDialog from '../create-new/_components/AiOutputDialog'
function Listing() {
…
return (
<div>
…
{userRoomList?.length == 0 ?
<div className="…">
No Interior AI Designs Generated Yet </div>
:
<div className="grid grid-cols-3 gap-4"> {[Link]((room,index)=>(
…
))}
</div>
}
<AiOutputDialog openDialog={openDialog}
setOpenDialog={setOpenDialog}
aiImage={selectedRoom?.aiImage}
orgImage={selectedRoom?.orgImage}
/>
</div>
)
We will add different credit purchase options. Users can choose to buy
anywhere from 5 to 100 credits, with pricing shown below each option.
In dashboard, create a new folder buy-credits, and in it, create [Link].
Let’s add different credit package options, each showing the number of
credits and their corresponding price by adding the below codes: "use
client"
import React, {useState } from 'react'
function BuyCredits() {
const [selectedOption,setSelectedOption]=useState([]); const creditsOption=[
{
credits:5,
amount:0.99
},
{
credits:10,
amount:1.99
},
{
credits:25,
amount:3.99
},
{
credits:50,
amount:6.99
},
{
credits:100,
amount:9.99
},
]
return (
<div>
<div className="text-2xl font-bold text-center mb-6"> Buy More Credits
</div>
<div className="flex flex-row gap-4 justify-center"> {[Link]((item,index)=>(
<div key={index} className="card bg-base-100 w-48 shadow-xl"> <div
className="card-body p-4 place-items-center"> <h2 className="card-title">
{[Link]} credits
</h2>
<p>for ${[Link]}</p>
<button className="btn btn-primary"
onClick={()=>setSelectedOption(item)}> Buy
</button>
</div>
</div>
))}
</div>
</div>
)
}
export default BuyCredits
Note: you can get the source codes from support@[Link] Code
Explanation
const [selectedOption,setSelectedOption]=useState([]); We have the selectedOption state which
holds the selected option.
We then iterate through the creditOptions array to display the various credit
purchase options: {[Link]((item,index)=>(
<div key={index} className="card bg-base-100 w-48 shadow-xl"> <div
className="card-body p-4 place-items-center"> <h2 className="card-title">
{[Link]} credits
</h2>
<p>for ${[Link]}</p>
<button className="btn btn-primary"
onClick={()=>setSelectedOption(item)}> Buy
</button>
</div>
</div>
))}
Payment Gateway
Next, we're going to integrate a payment gateway for this screen. We'll be
using PayPal as our payment solution because it: - is available worldwide
- is free to use
- has easy integration
Go to [Link] to get started:
First, log in or create a new account if you don't have one. For development
purposes, we'll use sandbox mode. Once you register a business, you can
return (
<[Link] value={{ userDetail, setUserDetail }}> <PayPalScriptProvider
options={{ clientId: [Link].NEXT_PUBLIC_PAYPAL_CLIENT_ID }}> <div>{children}
</div>
</PayPalScriptProvider>
</[Link]>
)
}
export default Provider
]
return (
<div>
<div className="text-2xl font-bold text-center mb-6"> Buy More Credits
</div>
<div className="flex flex-row gap-4 justify-center"> {[Link]((item,index)=>(
<div key={index} className="…">
…
…
</div>
))}
</div>
<div className="max-w-3xl mx-auto mt-4 px-4"> {selectedOption?.amount&&
<PayPalButtons style={{ layout: "horizontal", width: "100%"}}
/>
}
</div>
</div>
)
}
We check if selectedOption?.amount exists, meaning user has selected an
option, we render the Paypal button.
If we run our application and go to localhost:3000dashboardbuy-credits,
select any option and you can see we render the PayPal button:
When you click on PayPal, you'll see all available payment methods. You
can either pay with PayPal or use a debit/credit card without creating a
PayPal account:
When you log in, you'll be directed to a page displaying the amount and
other details. Currently, the amount shows as $0.01:
We haven't provided the selected option’s amount yet, but we'll do that
shortly. Now, let's customize it further.
Customizing PayPal Payment
To process a payment, you first need to create an order in the PayPal button.
In /dashboard/buy-credits/[Link], the Paypal button has a 'createOrder'
property that takes an arrow function to handle the purchasing. Add in bold:
{selectedOption?.amount&&
<PayPalButtons style={{ layout: "horizontal", width: "100%"}}
createOrder={(data,actions)=>{
return actions?.[Link]({
purchase_units:[
{
amount:{
value:selectedOption?
.amount?.toFixed(2),
currency_code:'USD'
]
})
}}
/>
]
const onPaymentSuccess=async()=>{
[Link]("payment Success...")
}
…
…
…
{selectedOption?.amount&&
<PayPalButtons style={{ layout: "horizontal", width: "100%"}}
onApprove={()=>onPaymentSuccess()}
onCancel={()=>[Link]("Payment Cancelled")}
createOrder={(data,actions)=>{
return actions?.[Link]({
…
})
}}
/>
onPaymentSuccess will be called only when the payment is successful. For now,
we just [Link]("payment success").
For handling payment cancellation or failure, we add an 'onCancel' handler
that logs a 'payment cancelled' message.
Let's test our app. Trying purchasing an option using PayPal's test card
numbers, which you can find by searching for 'PayPal test cards' on Google
or going to: [Link]/tools/sandbox/card-testing/
Under ‘Card Testing’, you can generate test cards from different countries
(you're not limited to US cards). Select a card type e.g. Visa. Simply click
'Generate' and it will create a test card number for you:
logged:
This confirms that after a successful payment, the code executed our
callback method and logged the message to the console. That completes the
basic payment setup.
Code Explanation
To get the current credits, we'll use the user context.
const {userDetail,setUserDetail}=useContext(UserDetailContext); We destructure 'userDetail'
from our context using 'useContext(UserDetailContext)'. Remember that we have setup the
UserDetailContext in our [Link]: function Provider({children}) {
…
return (
<[Link] value={{ userDetail, setUserDetail }}>
<PayPalScriptProvider options={{ clientId: … }}> <div>{children}</div>
</PayPalScriptProvider>
</[Link]>
)
We call 'await [Link](Users)' to modify the User schema. We'll use '.set'
to update the credits field: const result=await [Link](Users)
.set({
credits:userDetail?.credits+selectedOption?.credits }).returning({id:[Link]}); We add
to the existing credits using 'userDetail?.credit' plus the credit amount from 'selectedOption?.credit'.
After updating the credits, we'll return the user's ID. That completes our update logic.
{
[Link]('/dashboard');
}
…
Code Explanation
import { useRouter } from 'next/navigation'; function BuyCredits() {
…
const router=useRouter();
{
[Link]('/dashboard');
{
setUserDetail(prev=>({
...prev,
credits:userDetail?.credits+selectedOption?.credits }))
[Link]('/dashboard'); }
We use a spread operator ‘…’ to keep all previous values (...prev), then
update the credits field with the new amount.
With this context update, you'll see the changes instantly without refreshing.
You now have the new credits available for creating interiors.
Deduct One Credit from Account
We need to deduct one credit from the account whenever you generate a
design. Let's go to the ../create-new/[Link]. In the generateAiImage
function, we add a call to a new method: const generateAIImage = async ()
=> {
…
const result = await [Link]('apiinterior-ai', {
…
});
setAiOutputImage([Link])
await updateUserCredits();
setOpenOutputDialog(true);
setLoading(false);
if(result)
{
setUserDetail(prev=>({
...prev,
credits:userDetail?.credits-1
}))
return result[0].id
When a user generates an image, we'll deduct one credit from their total We
first call 'await [Link]' to modify the User schema. We set credits to
userDetail?.credits – 1. For the return value, we'll return the ID (this is
optional and can be customized based on your needs).
If the result is true, update the userDetail context using the same technique
we discussed earlier. Then return just result[0].id.
To test this, let’s try generating a room and if you return to the home screen,
you'll notice the credits have reduced by 1.
Chapter 12: Deploy App
Now let's deploy this application to the cloud, making it a true SaaS
(Software as a Service) that can be offered to users. I've added a simple yet
beautiful landing screen to the application:
The code for this landing screen is placed in [Link] (you can get
the source code from suppor@[Link]): import Image from
"next/image";
import Link from "next/link";
import React from 'react'
export default function Home() {
return (
<div>
<div className="hero min-h-screen bg-base-200"> <div className="hero-content text-
center"> <div className="max-w-[85rem]"> {/* Main Title */}
<div className="mb-8">
<h1 className="text-5xl font-bold mb-4"> AI Room and Home
<span className="text-primary"> Interior AI</span> </h1>
<p className="text-lg">
Transform Your Space with AI
</p>
</div>
{/* Get Started Button */}
<div className="flex justify-center mb-8"> <Link href="/dashboard" className="btn
btn-primary gap-2"> Get started
</Link>
</div>
{/* Main Image */}
<div className="flex justify-center mb-16"> <Image src={'[Link]'} alt="mockup"
width={1000} height={600} > </div>
</div>
</div>
</div>
</div>
);
For cloud deployment, we'll use Vercel - a cloud service provider that's
specifically optimized for [Link] applications. Our deployment process
will involve: - Pushing our code to GitHub
- Deploying from GitHub to Vercel Let's first begin the deployment
process. First, initialize Git in the Terminal by running (in your
project folder): git init
After initializing Git, let's create a new repository on GitHub:
Click on ‘New’ and give your repository a name e.g.'interior-ai’:
Repository':
After creating the repository, add the remote origin by copying and pasting
the provided URL command in the Terminal:
This command sets your remote origin, telling the Terminal where to push
your code repository.
Next, run 'git add .' to stage all your files for commitment to GitHub: git
add .
Create your first commit by running: git commit -m "initial commit"
For the first push to your repository, use: git push -u origin main
After this initial push, you can simply use 'git push' for future updates.
Once you've initiated the push, wait for the process to complete. After it
finishes, refresh your GitHub page and you'll see your code in the
repository:
Now that we've pushed our code, let's return to the Vercel dashboard
([Link]). You'll see a view similar to this one, which shows all
deployed projects:
Click 'Add New' to begin deploying your project.
Select 'Project' and connect your GitHub repository. You can also connect
repositories from GitLab or Bitbucket. Once connected, you'll see your
recently pushed project - simply click 'Import' to proceed."
After importing, enter your project name. Vercel will automatically detect
that you're using [Link]:
Leave the default framework settings unchanged. However, you need to add
your environment variables to Vercel. Copy all the variables from your .env
file and paste them here:
Vercel will automatically detect the keys and their corresponding values.
Click 'Deploy' to start the process:
Final Words
We have gone through quite a lot of content to equip you with the skills to
create your own AI Saas apps.
Hopefully, you have enjoyed this book and would like to learn more from
me. I would love to get your feedback, learning what you liked and didn't
for us to improve.
Please feel free to email me at support@[Link] to get updated
versions of this book.
If you didn't like the book, or if you feel that I should have covered certain
additional topics, please email us to let us know. This book can only get
better thanks to readers like you.
If you like the book, I would appreciate if you could leave us a review too.
Thank you and all the best for your learning journey!
ABOUT THE AUTHOR
Greg Lim is a technologist and author of several programming books. Greg
has many years in teaching programming in tertiary institutions and he
places special emphasis on learning by doing.
Contact Greg at support@[Link]