- Importance of e-commerce web apps having dynamic content
- Benefits of dynamic e-commerce web apps
- Which aspects of e-commerce web apps should be dynamic?
- Why is React useful for dynamic e-commerce?
- Why we're using a headless CMS
- Tutorial prerequisites
- Setting up
- What we’ll be building
- Creating the navigation component
- Creating the hero section
- Products page
- Single product page
- Adding and deleting products from cart
- Adding cart notifications
- Quick recap
- Conclusion
GSD
Building a Dynamic React Ecommerce Application
Posted by Chineta Adinnu on May 13, 2022
As we all know e-commerce simply means buying and selling over the internet and it has become a popular concept among business owners. Online shopping is one of the most popular online activities worldwide. The global e-retail sales reached 4.2million U.S. dollars in 2020 and this figure is forecast to increase by 50% over the next four years, reaching 7.4 trillion dollars by 2025.
E-commerce also helps business owners reach a broader range of audiences. It is a convenient way for consumers to buy products from anywhere without leaving the comfort of their homes.
During the 2020 lockdowns due to COVID-19, many physical retail shops closed down, causing them to resort to online selling. They did this mainly through social media and e-commerce websites. The lockdowns also resulted in over 80% of consumers across the globe shopping online. In short, the pandemic increased the need for e-commerce transactions.
In this article we will discuss why building a dynamic e-commerce application is essential and how we can leverage technologies like React and a headless React CMS to create an e-commerce app.
By the end of this article, you will have a fully dynamic e-commerce app built using React for the user interface, Redux for state management, ButterCMS for content management, and TailwindCSS for style.
Table of contents
Importance of e-commerce web apps having dynamic content
A web app can either be static, dynamic, or a mixture of both.
Static pages are just as their name describes: static. They are HTML files containing content across all pages of your website. They do not interact with the server and cannot be dynamically altered or manipulated by the server.
For instance, let’s say you have a products section that can be seen across four pages of your website, and there is a need to update a particular product. If you were using static pages you’d have to painstakingly edit these pages one by one to update that product. This also means you’d have to have the technical know-how to update content.
On the other hand, dynamic web pages are built dynamically on the fly—they are rendered by the server based on the user’s demand. The server can also do interactions and manipulations on the pages.
Business owners can also carry out updates very easily on dynamic pages because an update to content made on a particular page reflects on all pages that content is shown. These changes are also seen instantly by customers.
Dynamic pages can integrate with a content management system (CMS) to easily manage content—especially for non-technical people. They can simply log in to a CMS and change page contents, then watch it be reflected on all affected pages.
More on content management systems
ButterCMS defines a CMS as a software application that enables website management easily by technical and non-technical people. The main idea behind a CMS is to separate the content layer from the presentation layer.
A CMS, in general, can be likened to a telephone. A traditional CMS is like a wired phone—the handset is always connected to the phone base. To be functional, you need both parts for it to work.
In this type of CMS, content gets mingled with code, and it gets challenging to reuse content when you don’t fully understand or know the code.
WordPress is one of the most popular traditional CMSs on the market. It currently runs over 43% of the entire internet.
WordPress provides an editing interface where we can easily drag and drop widgets and modify our content right on the platform. To make significant changes to a WordPress site, you have to be conversant with PHP. This language barrier turns out to be a massive restraint as not all developers know or use PHP. Frankly, most are not even interested in learning PHP.
Headless CMS
A headless CMS on the other hand CMS can be used with any language or development stack. It decouples the frontend from the backend, making it possible to serve content seamlessly through APIs across different devices. Content stored in a headless CMS can be accessed by technical and non-technical people and used on different devices and channels.
Assuming you have multiple websites that require the same content, a headless CMS can serve up one content for these websites across the board so that changes made from one point reflect across all the websites with just a few clicks.
A headless CMS is like a wireless earpiece: Different devices connect to the earpiece without needing to be plugged in. It doesn’t rely on a particular device type to function.
ButterCMS is the #1 rated headless CMS that allows you to serve content to your application with just a few clicks. It works with any tech stack and handles scalability, maintenance, and security. All you have to do is upload your content and watch it reflect instantly on your app.
With a headless CMS, you don’t have to stress about the backend or maintenance, as most providers will take care of this for you. Later on, I’ll demonstrate how we can serve content to our app using ButterCMS.
Benefits of dynamic e-commerce web apps
Picture this. Steven is a business owner with a dynamic e-commerce app built with ButterCMS and Eric is a business owner with a static e-commerce app.
They both sell product A and make orders from the same wholesale company. Product A is in high demand, which causes an influx in its price.
Steven uses a dynamic web app integrated with ButterCMS. This makes it easy for him to quickly log in to his ButterCMS dashboard and adjust prices that instantly reflect on all pages and are also instantly seen by customers. This leads to massive sales.
On the other hand, Eric uses a static page without a headless CMS and doesn’t have the technical know-how to update prices on the static pages that make up his website. So, he takes up days recruiting technical staff. This technical staff also has to update prices page by page, and before he is done, the price of product A is already back to normal.
Eric spent money recruiting and didn't make a profit from sales, whereas Steven made a huge chunk of profit because of how easily he could update his prices.
So why was Steven able to succeed where Eric failed? Here are some advantages of a headless CMS that Steven had:
Content management
Dynamic pages make it easy for business owners to have complete control over the content on their web apps. It lets them manage and make changes swiftly. And the best part is that, when using a headless CMS, they don't have to be developers to be able to manage their apps.
Faster updates
Dynamic pages integrated with a headless CMS make accessibility swift. Business owners can access and make changes from anywhere and on any device. If Steven were on vacation, he would still be able to quickly update prices across his e-commerce site instantly and make a profit.
Saves time
As a business owner, you want to adapt quickly to changes. A change made on a particular piece of content is updated wherever the content is referenced on all pages of the site. This saves a great deal of time from having to make changes page by page, as Eric had to do.
Improved user experience
Changes are seen instantly by customers. They don’t have to keep refreshing pages for information or updates on products. This is mostly possible when dynamic pages are integrated with a headless CMS.
Personalization
Business owners can display dynamic content for customers based on demographics and psychographics. They can categorize products, frequently purchased products, and related products to serve personalized content based on user needs.
Which aspects of e-commerce web apps should be dynamic?
Products page
An e-commerce's primary focus should be to display products. You want your users to be able to view your products, make a purchase, and return to repeat the same process.
Single product page
You also want them to be informed about the products they want to purchase, like showing them descriptive information on products and discount offers.
Images
Images are very important because they are a preview of your product. When images take longer to load or don’t load at all, it leads to a bad user experience and chases customers away. Saving your images on a content delivery network optimizes images, causing them to load faster.
Hero or announcement board
The hero section displays new information on products and keeps customers informed of discounts, updates, sales, and general information. A business owner using an e-commerce site integrated with a headless React CMS would have no hassles updating new information in this hero section.
Why is React useful for dynamic e-commerce?
React is a popular JavaScript library created by the Facebook team to build user interfaces. They are mostly used in creating dynamic apps because of the following:
- Reusable components: React breaks your website down into reusable components—an update on one component updates all the other pages they are in.
- React support: React is easy to maintain and update as it has a large support community.
- Server-side rendering: It supports server-side rendering and state management and enhances site performance.
- State management: React can handle continuously changing data and large interfaces due to its ability to handle and manage state.
- Optimization: React can easily plug in third-party services and provides a virtual DOM that improves site performance by optimizing rendering time.
Later in this article, we will use React to build our dynamic React ecommerce app.
Why we're using a headless CMS
There are a lot of reasons why a headless CMS surpasses a traditional CMS
- Multi-channel: A headless CMS offers omnichannel selling; your content can be shared on multiple platforms or channels
- Customizable: A headless CMS offers more customization than a traditional CMS. Developers can change or tweak the user interface as they want and business owners can change content types and models.
- Separation of concerns: With a headless CMS, you don’t need to be technically inclined to make changes and update contents; you don’t have to worry about the code aspect, as the CMS is entirely separate from the code.
- API integration: Integrating a headless CMS to any head(frontend) is easy because it is an API and just needs to be called and your content gets loaded.
ButterCMS is a headless CMS that allows you to set up content for your application in minutes. It's popular amongst developers and non-developers alike because of its excellent user interface. It has a great user experience and on-screen tutorials that help you navigate the platform. And the best part? It melts into any tech stack.
This article will use ButterCMS to create a hero slider, upload new products, modify products, and delete products. We will also allow users to view a single product page, add a product to the cart, remove a product from the cart, and see the total cost of products in the cart.
Let's get started.
Tutorial prerequisites
- Basic knowledge of JavaScript and React
- Basic understanding of Redux
- Code editor / IDE (this tutorial uses VS Code, but any IDE will work)
- NPM or yarn installed (this tutorial uses yarn)
- Basic understanding of Tailwind CSS (not necessary to have)
Setting up
React setup
We use the CLI to create a new React app for our project.
yarn create react-app butter-ecommerce
Delete some of the startup files from our project folder, leaving just index.js
, app.js
, and index.css
in our src
folder.
In the app.js
file, we clear out the template code and insert a simple hello world
to ensure our app is still working.
import React from 'react'
const app = () => {
return(
<>
<p>Hello world!</p>
</>
)
}
export default App
Now, we can install the other tools we need for this project.
We install react-router
for handling all page routing.
yarn add react-router-dom
Tailwind configuration
We will style our app with Tailwind CSS. Tailwind CSS is a convenient CSS utility-based framework that requires us to write little or no CSS.
We install Tailwind CSS using postcss. You can read more about it in the documentation.
We add the -D
flag to our command to install Tailwind CSS as a development dependency.
yarn add tailwindcss postcss autoprefixer -D
We then initialize tailwind by running npx tailwindcss init
. This spawns a tailwind.config.js
file in our root folder.
In the tailwind.config.js
file, we replace the existing code with this:
module.exports = {
content: ["./src/**/*.{html,js}"],
theme: {
extend: {},
},
plugins: [],
}
We also create a postcss.config.js
file in the root folder and add the following:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
Lastly, in our index.css
file, we add this:
@tailwind base;
@tailwind components;
@tailwind utilities;
We have completed the tailwind configuration and can now test it out.
In app.js
, add some tailwind classes to our hello world
text.
import React from 'react'
const app = () => {
return(
<>
<p className='text-xl font-bold'>Hello world!</p>
</>
)
}
export default App
Then run yarn start
on our terminal.
Our app opens up on http://localhost:3000/
, and we can see a boldly written Hello world!
ButterCMS configuration.
We install our ButterCMS dependency and log in to our ButterCMS account to retrieve our API key:
yarn add ButterCMS
You can create one here if you don't have an account. Signing up is easy and straightforward, and you get a 30-day free trial with no credit cards required.
Select the stack we are developing with—React—on the Butter dashboard.
The API token is displayed, and we copy it to our react app.
We create a .env
file in our root
folder and paste our API token into this environmental variable to protect our token.
REACT_APP_BUTTER_CMS_API_KEY = 'enteryourapitoken'
We add /env
to our .gitignore
file, so it doesn't get pushed to GitHub, making our API token safe from the public eye.
In the src folder, we create a butter-client.js
file, then we import Butter and pass our API token to it, saving it in a variable called butter
.
We use the butter
variable to retrieve content from ButterCMS into different components on our app. We will use this later in the tutorial.
import Butter from 'ButterCMS';
const apikey = process.env.REACT_APP_BUTTER_CMS_API_KEY;
const butter = Butter(apikey);
export default butter;
Now we are done with some basic setup. I’ll explain the different components we will build-out.
What we’ll be building
Our e-commerce app will have the following:
- Navigation component: This can be hardcoded or served using ButterCMS and shown on all pages.
- Hero component: This component can be a static image or a dynamic slider
- Products component: This will list all our products with their prices and a button to view a single product.
- Single product component: This will display an individual product, a description of that product, and a button to add the product to the cart.
- Cart component: This is where we can see all added items, modify them by increasing or reducing their quantities, and see the total price for all products added to the cart.
Content types
The two main content types we’ll be focusing on are Page Types and Collections.
Page Type is a page builder that defines the structure of our pages. It has some powerful tools like the components, component picker, and repeater.
- Components allow us to create a group of fields reused on different pages.
- A component picker enables us to have more than one component grouped on a page.
- A repeater is used to group one or more content fields that need to be repeated multiple times, just like our slider.
According to the ButterCMS documentation, Collections are tables of data to be referenced by Pages, extending the use cases that you can achieve with ButterCMS. Collections are reusable components, making them perfect for creating our navigation and products page. We’ll see them in action later on.
Creating the navigation component
Setting up navigation collections on ButterCMS
As I mentioned earlier, this component can either be hardcoded or served using ButterCMS. I’ll be using ButterCMS to set up our navigation component.
The Navigation collection will contain our nav names and the links they will be routed to, and we can access this navigation content through the Butter API.
One of the ways to create a collection from our ButterCMS dashboard is by:
- Clicking on the content type icon
- Click new content type
- Select collection from the dropdown menu.
A second way is by clicking directly on the plus sign on collections from the content type drop-down menu, as shown in the image below.
ButterCMS is great because it provides a brief description and examples of what each collection content field is used for when you hover around their names.
We create a nav menu name
and a nav URL
field on our new collections page
- Add a
short text
content field and save the name aslabel
- Add another
short text
content field and save the name asurl
- Then save the collection name as
navigation
Now, we can click on collections and add our first nav menu Home
and URL /home
and then hit publish.
We can create the remaining nav menus:
- Click on
collections
- Click
new item
- Select the
Navigation
collection from the drop-down menu.
You can add any menu label or URL you want. Don’t forget to hit publish when you are done.
We will use three menus for this tutorial: Home, About, and Products.
After publishing these menus, we need to import them to our e-commerce app.
Click on the three dots in the upper right on our navigation collection and select API Explorer from the dropdown menu.
Our navigation collection can be seen in JSON format in the API explorer and can be integrated into our app. ButterCMS makes it easy for us by providing a code we can copy and paste into our app.
var params = {
"page": "1",
"page_size": "10"
};
butter.content.retrieve(['navigation'], params)
.then(function(resp) {
console.log(resp.data)
})
.catch(function(resp) {
console.log(resp)
});
Now we can create our navbar component to insert this code inside.
Integrating ButterCMS to the React app
Create a folder called components
in the src
folder and add a file called Navbar.js
.
In Navbar.js
, we import React, useEffect
, and useState
.
The UseEffect
hook is used to perform side effects inside a react component. An example of a side effect is fetching data from the API.
useState
is used to hold and set data used in the component. We will use this later.
We also import butter from the butter-client.js
file we created earlier and insert the ButterCMS generated code retrieved from our API explorer inside the useEffect
hook.
import React, { useEffect, useState } from 'react';
import butter from '../butter-client';
const Navbar = () => {
useEffect(() => {
var params = {
page: '1',
page_size: '10',
};
butter.content
.retrieve(['navigation'], params)
.then(function (resp) {
console.log(resp.data);
})
.catch(function (resp) {
console.log(resp);
});
}, []);
return <></>;
};
export default Navbar;
To see our data in our console, we have to import the Navbar
component into App.js
:
import Navbar from './components/Navbar';
const App = () => {
return (
<>
<Navbar />
</>
);
};
export default App;
On windows hold ctrl + shift + J
or on Mac hold Command + Option + J
to open the console.
Now we can see our API response on our console.
{meta: {...}, data: {...}}
data:
navigation: Array(3)
0: {meta: {...}, nav-label: 'Home', nav-url: '/home'}
1: {meta: {...}, nav-label: 'About', nav-url: '/about'}
2: {meta: {...}, nav-label: 'Products', nav-url: '/products'}
length: 3
[[Prototype]]: Array(0)
[[Prototype]]: Object
meta: {count: 3, next_page: null, previous_page: null}
[[Prototype]]: Object
Creating state
On Navbar.js
, we create a navList
state to hold our API data.
When we console.log(navList)
, we still see all our data retrieved from ButterCMS intact.
import React, { useEffect, useState } from 'react';
import butter from '../butter-client';
const Navbar = () => {
const [navList, setNavList] = useState([]);
useEffect(() => {
var params = {
page: '1',
page_size: '10',
};
butter.content
.retrieve(['navigation'], params)
.then((res) => {
setNav(res.data.data.navigation);
})
.catch((err) => err.message);
}, []);
console.log(navList);
return <></>;
};
export default Navbar;
Now, we can map through our navList
data to display the nav on our application.
return (
<>
{navList.map((nav) => nav.label)}
</>
)
The nav list displays successfully!
Styling nav with Tailwind CSS
Now, let’s style it up a little using tailwindcss
, which we already installed.
return (
<>
<div className='container px-36'>
<nav className='bg-white border-gray-200 px-2 sm:px-4 py-6 rounded'>
<div className=' flex justify-between items-center flex-wrap mx-auto '>
<p className='text-3xl font-bold text-teal-700'>NetFashion</p>
<div className='flex w-5/12 justify-between items-center
flex-col mt-4 md:flex-row md:space-x-8 md:mt-0
md:text-base md:font-medium'>
{navList.map((nav) => (
<p className=' py-2 pr-4 pl-3 text-gray-700 border-b
border-gray-100 hover:bg-gray-50
md:hover:bg-transparent md:border-0
md:hover:text-teal-700 md:p-0
'>
{nav.label}
</p>
))}
</div>
</div>
</nav>
</div>
</>
);
We have a nice-looking navbar for our application.
We can also add more nav options. All we have to do is go back to our ButterCMS collection dashboard and add a new item, and it automatically updates in our application.
Add routing
Our nav links are p tags, so they don’t route anywhere. We change that by wrapping our App
in BrowserRouter
in index.js
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Don’t be alarmed if my index.js
is different from yours; I’m simply using the React 18 new root
API.
The legacy root
API still works fine, so you can do this:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
Back in the Navbar.js
file, we import NavLink
and replace the p tags with NavLink
:
{navList.map((nav) => (
<NavLink
key={nav.meta.id}
to={nav.url}
className=' py-2 pr-4 pl-3 text-gray-700
border-b border-gray-100
hover:bg-gray-50 md:hover:bg-transparent
md:border-0 md:hover:text-teal-700 md:p-0'
>
{nav.label}
</NavLink>
))}
When we click on a menu, we see the URL change to the URL that was added in our navigation collection in ButterCMS.
Creating the hero section
The hero section is significant in an e-commerce app because it showcases new arrivals, discounts, and products that the customer might not be aware of.
For this section, we will create an image slider using Swiper.js and, of course, we will serve content from ButterCMS.
Earlier, we talked about Page types and how they define the structure of a page. We also talked about the repeater field.
We will create a page type content type and use the repeater content field to create our slider component.
Creating a repeater
Let’s go back to our Butter dashboard.
- Click on the content type icon
- Click on the + sign beside page type from the dropdown menu.
- Select the
repeater
content field on the new page type and name itSlider
- Fill in the content fields we need for Slider
The content fields include:
- A short text for the title
- An image field
- A short text for button label
- A short text for button URL
- A short text for description (long text could work here too)
Now that our Hero page type has been created, we can populate it with images for our Hero slider.
- Select the Hero page type on the Page icon
- Name the page
home
- Click on the slider icon
This brings out the content fields we created earlier to populate, as shown in the image below:
After publishing, we can hit the slider icon many times to add more slides. This is why the repeater is so valuable. I’ll go ahead and add two more slides to my slider.
Now, we can copy our generated Butter API code from the API explorer just like we did with the Nav links and integrate it with our app.
Creating a hero slider using Swiper.js
We create a Hero.js
component in our components folder and import react, useEffect, useState, and butter from butter-client.js
.
We plug our butter-generated API code into our app using useEffect to receive our slider data. We also create a state called hero and set the data from butter to this state.
import React, { useEffect, useState } from 'react';
import butter from '../butter-client';
const Hero = () => {
const [hero, setHero] = useState([]);
useEffect(() => {
butter.page
.retrieve('*', 'home')
.then(function (resp) {
setHero(resp.data.data.fields.hero.slider);
})
.catch(function (resp) {
console.log(resp);
});
}, []);
console.log(hero);
When we console.log(hero)
we can see all the content of our slides in an array on the console.
Now, let’s install Swiper for our Slider.
Swiper is a JavaScript library used for creating responsive sliders and carousels.
yarn add swiper
We import swiper and the dependencies needed for our slide to function:
import 'swiper/css/bundle';
import 'swiper/css';
import 'swiper/css/pagination';
import { Autoplay, Pagination, Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
We create the Swiper component and then map through our Hero state to return each slide.
return (
<>
<Swiper>
{hero.map((x) => (
<SwiperSlide>
<div className='flex flex-row'>
<div className='border border-gray-200 shadow-md
dark:bg-gray-800 dark:border-gray-700 relative '>
<img className='w-full ' src={x.image} alt='' />
<div className='p-5 absolute top-10 right-10 pr-20'>
<a href='#f'>
<h5 className='mb-2 lg:text-6xl font-bold
tracking-tight text-gray-900 dark:text-white '>
{x.title}
</h5>
</a>
<p className='mb-3 mt-5 font-normal
text-gray-700 dark:text-gray-400'>
{x.description}
</p>
<a
href='#f'
className='inline-flex items-center py-2 px-3
text-sm font-medium text-center text-white
bg-rose-700 rounded-lg hover:bg-rose-800
focus:ring-4 focus:ring-rose-300 mt-3 transition-all
ease-in duration-400'
>
{x.btn_label}
<svg
className='ml-2 -mr-1 w-4 h-4'
fill='currentColor'
viewBox='0 0 20 20'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
d='M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010
1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1
1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z'
clipRule='evenodd'
></path>
</svg>
</a>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</>
);
Swiper is pretty easy to use and requires little setup to get your slide running. You can configure your Swiper based on your needs.
<Swiper
slidesPerView={1}
loop={true}
slideNextClass='swiper-slide-next'
slidePrevClass='swiper-slide-prev'
autoplay={{
delay: 2500,
disableOnInteraction: false,
pauseOnMouseEnter: true,
}}
pagination={{
clickable: true,
}}
modules={[Autoplay, Pagination, Navigation]}
className='mySwiper'
>
For more information, check out the Swiper documentation.
After adding more slides, our site can be seen in the image below:
You can add more slides through your ButterCMS dashboard and see results instantly in the app. The best part is that no other form of coding is required for continuous updates.
Products page
The essence of having an e-commerce app is to showcase products and ease the process of buying and selling. An e-commerce app should be user-friendly so as not to chase potential customers away.
Our products can be grouped in categories to aid users in easily locating a product.
We’ll create simple tabs to filter our product categories. We’ll also create a button that will view an individual product and enable us to add the product to the cart.
To start with, we’ll head back to our ButterCMS dashboard to create a collection of categories and products.
Collection of categories
On our ButterCMS dashboard:
- Click on content type and create new collections
- Add a
short text
content field on the collections page and name itcategory
Also name the collection category and click save.
Now we can add the different categories we want.
This article will use four categories: Men’s clothing, Women’s clothing, Children’s clothing, and Jewelry.
We can see our four created categories when filtering and selecting category
on our collection dashboard.
These categories are to be referenced in each of our products. I’ll show you how we can achieve this.
Now let’s create our products collection.
Collection of products
Still on our dashboard, we create another collection content type called products.
The products collection will have several content fields:
- Image: Image content field
- Title: short text content field
- Description: short or long content field
- Price: Number content field
- Product ID: Number field (This is not necessary, and I'll explain why)
- Category: Reference content field.
The product id
is not required because ButterCMS generates an id
for each collection. So we don’t have to worry about generating an id
.
We select category from the dropdown in the reference content field and choose a method for linking it.
There are two methods for connecting references:
- One-to-Many: linking multiple categories to one product.
- One-to-One: for linking one category to one product.
Since each product can have only one category, we select One-to-One.
Now that we have created this collection schema, we can upload our products.
Uploading products
- Click the collections icon
- Select Products
- Populate the provided fields
- Select the category, and hit publish
I’ll go ahead and add more products to my collection.
After uploading a few starter products, we can copy our ButterCMS- generated code from the API explorer as we have done before. Then we’ll integrate it into our app.
Integrating Products API to app
Let’s create a products
component.
We import react
, useEffect
, useState
, and butter
into the component.
In useEffect
, we create a function to get our products from ButterCMS. This function is passed in as a callback function to useEffect.
We define a state that would hold the data we get from ButterCMS.
We also define another state—Loading
—that would monitor when our data from the API has loaded. This is set to false by default.
This Loading
will be used later when we want to implement something called Skeleton
loading.
import React, { useEffect, useState } from 'react';
import butter from '../butter-client';
const Products = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const getProducts = async () => {
setLoading(true);
var params = {
page: '1',
page_size: '10',
};
const res = await butter.content.retrieve(['products'], params);
setData(await res.data.data.Products);
setLoading(false);
};
getProducts();
}, []);
return <></>;
};
export default Products;
The Product component can be added inside the Hero component but to adhere to best practices, let’s create a Home component and import Hero and Products. Then in app.js, we’ll replace the Hero component with the Home component.
import React from 'react';
import Hero from './Hero';
import Products from './Products';
const Home = () => {
return (
<>
<Hero />
<Products />
</>
);
};
export default Home;
Remember we already have a link set for /products
in our navbar
so we also create and set a route for it in app.js
:
import Navbar from './components/Navbar';
import Home from './components/Home';
import { Route, Routes } from 'react-router-dom';
import Products from './components/Products';
const App = () => {
return (
<>
<Navbar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/products' element={<Products/>}/>
</Routes>
</>
);
};
export default App;
Back to the Products component, if we console.log(data)
we can see all the products we created on the ButterCMS dashboard.
We can map through data
and return products
to our app.
return (
<>
<div className='flex flex-wrap flex-row justify-center mx-auto'>
{data.map((product) => (
<div
className='max-w-sm mr-9 mb-5 bg-white rounded-lg
shadow-md dark:bg-gray-800 dark:border-gray-700'
key={product.meta.id}
>
<img
className='p-8 rounded-t-lg w-full'
src={product.image}
alt={product.name}
/>
<div className='px-5 pb-5 text-center'>
<h5 className='text-xl font-semibold tracking-tight
text-gray-900 dark:text-white'>
{product.title}
</h5>
</div>
<div className='flex justify-around pb-5 items-center'>
<span className='text-3xl font-bold text-gray-900 dark:text-white'>
${product.price}
</span>
<button className='text-white bg-rose-700
hover:bg-rose-800 focus:ring-4 focus:ring-rose-300
font-medium rounded-lg text-sm px-5 py-2.5 text-center '>
View Product
</button>
</div>
</div>
))}
</div>
</>
);
Add a tab component
We can see our products on our page. Let's create a Tabs component that will filter all products based on their category.
We create a new state called filter
. We’ll use the filter
array method to filter
product categories and then save the result to this filter state.
const [filter, setFilter] = useState([]);
const filterProducts = (pr) => {
const filterProduct = data.filter((product) => product.category.category
=== pr);
setFilter(filterProduct);
};
This function above will accept the categories as a parameter, then compare it with the products category and return when it matches.
We import the Tabs
component and pass this function as props
. We also pass in data
and setfilter
as we will use that later on the Tabs
component file.
<Tabs filterProduct={filterProducts} data={data} setFilter={setFilter}/>
This is not the best way to get access to these props. Context API is a much better approach to use but, for the purpose of this tutorial, we will use this approach.
Earlier, we mapped through data
to return our products. For the filter to work, we have to map through filter
instead.
{filter.map((product) => (
<div
className='max-w-sm mr-9 mb-5 bg-white rounded-lg
shadow-md dark:bg-gray-800 dark:border-gray-700'
key={product.meta.id}
>
<img
className='p-8 rounded-t-lg w-full'
src={product.image}
alt={product.name}
/>
<div className='px-5 pb-5 text-center'>
<h5 className='text-xl font-semibold tracking-tight
text-gray-900 dark:text-white'>
{product.title}
</h5>
</div>
<div className='flex justify-around pb-5 items-center'>
<span className='text-3xl font-bold text-gray-900 dark:text-white'>
${product.price}
</span>
<button className='text-white bg-rose-700
hover:bg-rose-800 focus:ring-4 focus:ring-rose-300
font-medium rounded-lg text-sm px-5 py-2.5 text-center '>
View Product
</button>
</div>
</div>
))}
The filter
state is an empty array, so the products section will be blank.
To fix this, let's also set the filter
state to use data
retrieved from ButterCMS inside our useEffect:
setFilter(await res.data.data.products);
Now, we can see our products.
In the Tabs component file, we destructure filterProducts
, data
, and setFilter
, and create buttons for each Tab category.
The All
category will display all products, so we set the filter to data when we click on this tab.
The category name
is passed into the filterProducts
function as an argument when we click on the remaining category tabs.
<div className='flex flex-wrap mt-5 border-b
border-gray-200 dark:border-gray-700 justify-center'>
<button
className='mr-2 inline-block py-4 px-4 text-sm font-medium
text-center hover:text-teal-600 hover:bg-gray-50'
onClick={() => setFilter(data)}
>
All
</button>
<button
className='mr-2 inline-block py-4 px-4 text-sm font-medium
text-center text-gray-500 rounded-t-lg
hover:text-teal-600 hover:bg-gray-50'
onClick={() => {
filterProducts("men's clothing");
}}
>
Men's Clothing
</button>
<button
className='mr-2 inline-block py-4 px-4 text-sm font-medium
text-center text-gray-500 rounded-t-lg
hover:text-teal-600 hover:bg-gray-50 '
onClick={() => {
filterProducts("women's clothing");
}}
>
Women's Clothing
</button>
<button
className='mr-2 inline-block py-4 px-4 text-sm font-medium
text-center text-gray-500 rounded-t-lg
hover:text-teal-600 hover:bg-gray-50 '
onClick={() => {
filterProducts("children's clothing");
}}
>
Children's clothing
</button>
<button
className=’mr-2 inline-block py-4 px-4 text-sm font-medium
text-center text-gray-500 rounded-t-lg
hover:text-teal-600 hover:bg-gray-5’
onClick={() => {
filterProducts('jewelry');
}}
>
Jewelry
</button>
</div>
Add skeleton loading
Skeleton loading is a good user experience feature that lets your users know that data is being fetched from the server.
Let’s install react-loading-skeleton:
yarn add react-loading-skeleton
Create a component called Loading
and import Skeleton
and its necessary dependencies.
import React from 'react'
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
const Loading=()=> {
return (
<>
<div className='flex flex-wrap justify-center mx-auto mt-4 '>
<div className=' w-3/12 mr-3 '>
<Skeleton height={350} />
</div>
<div className=' w-3/12 mr-3 '>
<Skeleton height={350} />
</div>
<div className='w-3/12 mr-3'>
<Skeleton height={350} />
</div>
</div>
</>
)
}
export default Loading
We import this Loading component into our Products.js
, remember that our loading
state is set to false
by default, updates to true
inside useEffect, and sets back to false
after data has been loaded.
We’ll use conditional rendering to render our Loading
component when loading
is true
.
{loading ? (
<Loading />
) : (
<div className='flex flex-wrap flex-row justify-center mx-auto'>
{filter.map((product) => (
<div
className='max-w-sm mr-9 mb-5 bg-white
rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700'
key={product.meta.id}
>
<img
className='p-8 rounded-t-lg w-full'
src={product.image}
alt={product.name}
/>
<div className='px-5 pb-5 text-center'>
<h5 className='text-xl font-semibold tracking-tight
text-gray-900 dark:text-white'>
{product.title}
</h5>
</div>
<div className='flex justify-around pb-5 items-center'>
<span className='text-3xl font-bold
text-gray-900 dark:text-white'>
${product.price}
</span>
<button className='text-white bg-rose-700
hover:bg-rose-800 focus:ring-4 focus:ring-rose-300
font-medium rounded-lg text-sm px-5 py-2.5 text-center '>
View Product
</button>
</div>
</div>
))}
</div>
)}
This simply means that if loading
is true, it will render skeleton loading
. Otherwise, it will display products.
Now when our page loads, we see a nice skeletal interface that lets the user know data is being loaded.
Single product page
Adding single product route
A single product page will have product descriptions, a price, and action buttons.
Before we build out our single products page, let's add a Navlink
to our product card, so that when users click on the card it takes them to the single product page.
In the Products
component, import Navlink
from react-router and wrap our product with Navlink
.
Navlink
uses a to
attribute which points to a URL which will contain our product ID.
<NavLink to={`/products/${product.meta.id}`}>
<div
className='max-w-sm mr-9 mb-5 bg-white
rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700'
key={product.meta.id}
>
<img
className='p-8 rounded-t-lg w-full'
src={product.image}
alt={product.name}
/>
<div className='px-5 pb-5 text-center'>
<h5 className='text-xl font-semibold
tracking-tight text-gray-900 dark:text-white'>
{product.title}
</h5>
</div>
<div className='flex justify-around pb-5
items-center'>
<span className='text-3xl font-bold
text-gray-900 dark:text-white'>
${product.price}
</span>
<button className='text-white bg-rose-700
hover:bg-rose-800 focus:ring-4 focus:ring-rose-300
font-medium rounded-lg text-sm px-5 py-2.5 text-center '>
View Product
</button>
</div>
</div>
</NavLink>
When we click on any product, it takes us to a URL with the product ID at the end. This will be the route for our single product page.
Let’s create a product
component.
Single product component
We import react
, useEffect
, useState
, and butter
.
We’ll still use our butter generated API code in useEffect to retrieve data.
Create a product
state and loading
state—I still intend on using Skeleton
loading—and pass the data retrieved into our product
state.
The React router uses useParams
to access URL parameters. We’ll use that to get our URL id
.
import React, { useEffect, useState } from 'react';
import butter from '../butter-client';
import { useParams } from 'react-router-dom';
const Product = () => {
const { id } = useParams();
const [product, setProduct] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const getProduct = async () => {
setLoading(true);
var params = {
page: '1',
page_size: '10',
};
const res = await butter.content.retrieve(['products'], params);
setProduct(res.data.data.products);
setLoading(false);
};
getProduct();
}, []);
return <div>Product</div>;
};
export default Product;
We also need to create a route for Product in app.js
<Route path='/products/:id' element={<Product/>}/>
Back in Product.js
we can filter through products
and return only the individual product
whose id
matches our URL
id
.
const singleProduct = product.filter((pro) => pro.meta.id === Number(id));
If we console.log(singleProduct)
, we’ll see our single product.
Now, we can map through singleProduct
and render the content to our app.
return (
<>
{singleProduct.map((prod) => (
<div key={prod.meta.id} className='mx-auto'>
{console.log(prod)}
<div className='flex flex-col md:flex-row justify-center
text-center md:justify-around px-20 py-10'>
<img
src={prod.image}
alt='product'
className='p-8 rounded-t-lg md:w-4/12'
/>
<div className='md:w-5/12'>
<h1 className='uppercase text-4xl font-light'>
{prod.category && prod.category.category}
</h1>
<p className='mt-4'>{prod.description}</p>
<p className='font-bold text-xl mt-5 mb-5'>${prod.price}</p>
<button className='mt-5 text-teal-800 border
border-teal-700 hover:bg-teal-800 hover:text-white
transition-all ease-in duration-300 focus:ring-4
focus:ring-teal-300 font-medium rounded-lg text-sm px-5
py-2.5 text-center mr-4'>
Add to cart
</button>
<button className='mt-5 text-white bg-teal-700
hover:bg-teal-800 focus:ring-4 focus:ring-teal-300
font-medium transition-all ease-in duration-300
rounded-lg text-sm px-5 py-2.5 text-center '>
Go to cart
</button>
</div>
</div>
<div className='px-24 text-center md:text-left'>
<h1 className=' font-semibold text-2xl'>{prod.title}</h1>
</div>
</div>
))}
</>
);
The rendered product is displayed in our app. We have the category, description, and price. We also have two buttons: one to add the product to the cart and the other to go to the cart.
Let’s import Navlink
to replace our go to cart
button.
<NavLink
to='/cart'
className='mt-5 text-white bg-teal-700
hover:bg-teal-800 focus:ring-4 focus:ring-teal-300
font-medium transition-all ease-in duration-300
rounded-lg text-sm px-5 py-2.5 text-center '
>
Go to cart
</NavLink>
Now we can create a cart component and properly route to our cart
. We’ll come back later for the add to cart
functionality.
In app.js
, we route our cart component. For now, we have nothing on the component, so it displays a blank page. We’ll populate it later.
<Route path='/cart' element={<Cart/>}/>
A good thing to have would be a cart icon on the navbar, so let’s create that.
I’ll use iconify
to import icons:
yarn add @iconify/react -D
In Navbar.js, import iconify:
import { Icon } from '@iconify/react';
We wrap it in a Navlink
and place it after our Navlist
:
<NavLink to='/cart' className='cursor-pointer'>
<Icon icon='el:shopping-cart' className='text-gray-700 ' />
</NavLink>
Now when we click on the cart icon, it also takes us to our cart component.
Let’s build the add-to-cart feature.
Adding and deleting products from cart
Redux configuration
Redux is a state management tool used for precisely managing states. It saves our state in a store that is accessed from any component.
We’ll use Redux to handle adding and deleting items from the cart and also increasing and decreasing the quantity of added items.
We will install redux
and react-redux
:
yarn add redux react-redux
Let’s create a folder called redux
in our src
folder.
Then we add an action
and reducers
folder in the redux
folder.
Actions are JavaScript objects with key-value fields. They have a type
field that describes the action to be performed—to add or delete products—and a payload
field that holds the data—product
—passed in as a parameter.
We create an index.js
file in the actions folder and add the code below:
export const addProduct = (product) => {
return {
type: 'ADDITEM',
payload: product,
};
};
export const delProduct = (product) => {
return {
type: 'DELITEM',
payload: product,
};
};
Let’s also create a reducer.
Reducers are pure functions used to update an application's state using actions. Pure functions mean they have no side effects and will have the same result if the same arguments are passed over again.
In our reducers folder, let’s create a file called handleCart.js
. Here we will write the function that will handle adding and removing products.
Add product reducer:
const cart = [];
const handleCart = (state = cart, action) => {
const product = action.payload;
switch (action.type) {
case 'ADDITEM':
// check if item exist
const checkItem = state.find((item) => item.meta.id === product.meta.id);
if (checkItem) {
return state.map((item) =>
item.meta.id === product.meta.id
? { ...item, qty: item.qty + 1, newPrice: item.price * (item.qty + 1)
}
: item
);
} else {
return [
...state,
{
...product,
qty: 1,
newPrice: product.price,
},
];
}
break;
}
};
export default handleCart;
The Reducer function accepts two parameters: state and action.
To break down the above code, we assign an empty array to cart
. This is where we are going to push every added product. This cart
is also assigned to our state
.
const cart = [];
const handleCart = (state = cart, action)
action.payload
is the product that gets added to the cart.
const product = action.payload;
When a product gets added to the cart, we check if the action.type is ADDITEM
:
switch (action.type) {
case 'ADDITEM':
If that's the type, we check through the cart to see if the product already exists.
const checkItem = state.find((item) => item.meta.id === product.meta.id);
If the product
exists, we map
through the state
and find the id
that corresponds to that of the product
being added, then we add a qt
y property to it and increment it by 1
. We also multiply the product price
by the qty
and assign it to a new property called newPrice
. This would increase our price
as qty
increases.
If id doesn’t match, we just return the product:
if (checkItem) {
return state.map((item) =>
item.meta.id === product.meta.id
? { ...item, qty: item.qty + 1, newPrice: item.price * (item.qty + 1)
}
: item
);
If product
doesn’t exist in cart, we return the state and add the new product
with a qty
property set to 1
. We also set the product price to newPrice
:
return [
...state,
{
...product,
qty: 1,
newPrice: product.price,
},
];
Delete product reducer
Removing a product from the cart is also similar to adding one.
case 'DELITEM':
const checkDelItem = state.find(
(item) => item.meta.id === product.meta.id
);
if (checkDelItem.qty === 1) {
return state.filter((item) => item.meta.id !== product.meta.id);
} else {
return state.map((item) =>
item.meta.id === product.meta.id
? { ...item, qty: item.qty - 1, newPrice: item.price * (item.qty - 1)
}
: item
);
}
break;
default:
return state;
break;
We check the cart
for the product to be deleted:
const checkDelItem = state.find(
(item) => item.meta.id === product.meta.id
);
If the qty
of the product
is equal to 1
, we delete the product
:
if (checkDelItem.qty === 1) {
return state.filter((item) => item.meta.id !== product.meta.id);
}
If the qty
is more than 1
then we can subtract the qty
by 1
until it gets to 1
and gets deleted. The price is also reduced.
state.map((item) =>
item.meta.id === product.meta.id
? { ...item, qty: item.qty - 1, newPrice: item.price * (item.qty - 1)
}
: item
);
We also add a default
case as is standard and return state
:
default:
return state;
Break;
Combining reducers
Since we have two reducers in one file, we can combine them using combineReducers
.
In the reducer folder, let’s create an index.js
file and pass handleCart
to combineReducers
, saving it in a variable called rootReducers
:
import handleCart from './handleCart';
import { combineReducers } from 'redux';
const rootReducers = combineReducers({ handleCart });
export default rootReducers;
Now we can import rootReducers
to our store
.
In the redux
root folder, let’s create store.js
, import creatStore
from redux, and pass our rootReducers
to createStore
.
import { createStore } from 'redux';
import rootReducers from './reducers';
const store = createStore(rootReducers);
export default store;
We can import our store to use in our root index.js
file.
import { Provider } from 'react-redux';
import store from './redux/store';
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
The provider
component makes the store available to any component that needs it.
Now, let’s dispatch our actions and add/delete items from the cart.
Dispatching actions
The Redux method dispatch
is the only way state can be updated in Redux. We use dispatch to carry out actions.
In product.js
, we import useDispatch
. We also import the addProduct
action from our action file.
import { useDispatch } from 'react-redux';
import { addProduct } from './../redux/action';
const dispatch = useDispatch();
const addCart = (product) => {
dispatch(addProduct(product));
};
Now, we can add our product
to this addCart function when the add to cart
button gets clicked.
<button onClick={()=>addCart(prod)}>Add to cart </button>
We can add a console.log(product)
in the handleCart
reducer just to see if the code works.
Another thing we can do is make our cart
icon display the number of products in the cart.
We use useSelector
to reference our state on any component.
Import useSelector
in Navbar
:
import { useSelector } from 'react-redux';
const state = useSelector((state) => state.handleCart);
Now we can access our state and also check the length. We can insert this in a p
tag and use absolute
positioning to place it above the cart
icon.
<NavLink to='/cart' className='cursor-pointer relative'>
<Icon icon='el:shopping-cart' className='text-gray-700 ' />
<p className='text-xs absolute -top-2 left-3'>{state.length}</p>
</NavLink>
Populate cart component
We already created a cart
component and routed it, now let's populate it with our products.
import React from 'react';
import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { delProduct } from '../redux/action/index.js';
import { NavLink } from 'react-router-dom';
import { addProduct } from '../redux/action/index.js';
import { Icon } from '@iconify/react';
const Cart = () => {
const state = useSelector((state) => state.handleCart);
const dispatch = useDispatch();
const delCart = (product) => {
dispatch(delProduct(product));
};
return (
<>
<div>
{state.map((cartItem) => (
<div className='px-20 py-10' key={cartItem.meta.id}>
<div className='flex justify-center relative '>
<div className='absolute right-56 cursor-pointer'>
<Icon icon='bi:x-lg' onClick={() => delCart(cartItem)} />
</div>
<div className='w-2/12 mr-10'>
<img src={cartItem.image} alt='cart' className='' />
</div>
<div className='ml-10 items-center'>
<h3 className='text-2xl font-semibold'>{cartItem.title}</h3>
<div className='flex'>
<p className='text-xl font-bold mr-4'>Qty: {cartItem.qty}</p>
{console.log(cartItem.qty)}
<p className='text-xl font-bold'>${cartItem.newPrice}</p>
</div>
<div>
<button className='text-3xl mr-5'>-</button>
<button className='text-3xl'>+</button>
</div>
</div>
</div>
</div>
))}
<div className='px-20 text-center mx-auto'>
<NavLink
to='/cart'>
Proceed to checkout
</NavLink>
</div>
</div>
</>
);
};
export default Cart;
In the above code, we reference the state
using useSelector
just like we did before and also import delProduct
from actions.
Then we map through our state
and return products
in the cart.
Increasing and decreasing product quantity
We also added plus
and minus
buttons we can use to increase or decrease product quantity.
Let’s write a function to implement that:
const increaseItem = (product) => {
dispatch(addProduct(product));
};
To decrease product qty
we can use the delCart
function we previously wrote and just pass our product to it.
<button
onClick={() => delCart(cartItem)}
className='text-3xl mr-5'
>
-
</button>
<button
onClick={() => increaseItem(cartItem)}
className='text-3xl'
>
+
</button>
Empty cart
We want the users to know when the cart is empty, so let’s create a component called EmptyCart
.
import React from 'react';
import { Link } from 'react-router-dom';
const EmptyCart = () => {
return (
<>
<div className='px-20 py-20 text-center'>
<h1 className='text-3xl font-bold'>
CART IS EMPTY..{' '}
<Link
to='/products'>
Go to Products
</Link>
</h1>
</div>
</>
);
};
export default EmptyCart;
You can customize it to your preference.
Back in cart.js
, we call EmptyCart
when the state
is empty.
{state.length === 0 && <EmptyCart/>}
Total price
It would be great if users could see the total cost of items in their cart. We’ll write a function to display this.
const totalPrice = state.reduce((acc, curr) => {
return acc + curr.newPrice;
}, 0);
This totalPrice
function can be added below the product list in the cart
component.
<div className=' flex px-20 py-10 justify-center items-center'>
<h3 className='text-xl font-semibold mr-3'>Total Price:</h3>
<h1 className='font-bold ml-3 text-3xl'>${totalPrice}</h1>
</div>
We can also add a button that leads us to checkout.
<NavLink
to='/checkout'
className=''
>
Proceed to checkout
</NavLink>
We won’t be creating a checkout feature in this article, but there are awesome 3rd party checkout services you can check out—Stripe is a great example.
Adding cart notifications
Before we conclude, let’s add a notification that pops up when a product gets added to the cart.
In product.js
, create a notifyMessage
state:
const [notifyMessage, setNotifyMessage] = useState(null)
In our
addCart
function, we set a notification message and save it in the notifyMessage
state:
const addCart = (product) => {
dispatch(addProduct(product));
setNotifyMessage(`${product.title} has been added to the cart`);
};
Now we can place this notification message at the top of our page:
{notifyMessage != null && (
<div className='px-20 py-10 text-center w-6/12 mx-auto'>
<p class=' p-4 mb-4 font-medium text-sm
text-green-700 bg-green-100 rounded-lg'>
{notifyMessage}
</p>
</div>
)}
We set a condition to only render it when notifyMessage
is not null.
And that’s it. We’ve built an e-commerce app from scratch showcasing our products.
Quick recap
This article discussed why dynamic web apps are important. We also discussed why an e-commerce app should be dynamic.
We created a React e-commerce site and hosted products and images in a headless content management system—ButterCMS.
We built our hero slider section using Swiper.js and retrieved all images used in the slider from ButterCMS. Without opening the codebase, we can add more images to our slider from the ButterCMS dashboard.
We also built out the products page and retrieved all our product details from ButterCMS. We grouped our products into categories and created a tab that filtered our products. Like with the hero component, we can add or remove new products from the ButterCMS dashboard.
We created a single product page that displayed individual product details with an action button to add products to the cart.We also created a cart component that showed all products on the cart with their total price and an option to increase or decrease the product quantity.
If we wanted to, we could add more features like a search feature, an advanced filter feature, a related products section, an about us page, and a contact page. We can also add a rating feature, completely build out the checkout feature, and improve the styling—the possibilities are endless.
Conclusion
Dynamic websites are essential. They ease the process for everyone, from developers to business owners to customers.
I hope this article has impacted you one way or the other, and I hope you find the answers you seek.
Live link https://ButterCMS-ecommerce.vercel.app/
Github repo link here.
ButterCMS is the #1 rated Headless CMS
Related articles
Don’t miss a single post
Get our latest articles, stay updated!
Chineta Adinnu is a frontend developer focused on building scalable web applications. She enjoys technical writing and is passionate about discovering and learning new technologies.