Skip to main content
Post image
· 12 min read

Next.js E-commerce App with Strapi and Chakra UI

basket

In this article, we will create the e-commerce client of our Strapi-Multitenancy admin panel that we have done before.

It is now used headless with the refine 3 version. You can use any UI library you want with the headless feature.

We will use Strapi and Chakra-UI together with Next.js in our E-commerce client example application.

Refine Project Setup

Let's start by creating our refine project. You can use the superplate to create a refine project.

npx superplate-cli -p refine-nextjs refine-ecommerce-example
✔ What will be the name of your app · refine-ecommerce-example
✔ Package manager: · npm
✔ Do you want to using UI Framework? > No(headless)
✔ Data Provider: Strapi
✔ i18n - Internationalization: · no

superplate will quickly create our refine project according to the features we choose. Let's continue by install the refine Strapi-v4 Data Provider and Chakra-UI packages that we will use later.

Installation

cd refine-ecommerce-example

npm i @pankod/refine-strapi-v4 @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6

Our refine project and installations are now ready! Let's start using it.

Usage

Configure Refine for Strapi-v4

pages/_app.tsx
import React from "react";
import { AppProps } from "next/app";
import Head from "next/head";
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-nextjs-router";
import { DataProvider } from "@pankod/refine-strapi-v4";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
const dataProvider = DataProvider(API_URL);

return (
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider}
>
<Component {...pageProps} />
</Refine>
);
}

Chakra-UI Provider Setup

pages/_app.tsx
import React from "react";
import { AppProps } from "next/app";
import Head from "next/head";
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-nextjs-router";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { ChakraProvider } from "@chakra-ui/react";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
const dataProvider = DataProvider(API_URL);

return (
<Refine routerProvider={routerProvider} dataProvider={dataProvider}>
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
</Refine>
);
}

Create Strapi Collections

We created three collections on Strapi as store, product and order and added a relation between them. For detailed information on how to create a collection, you can check here.

We created our collections in the previous Strapi Multitenancy guide. Now we will use the same collections.

Refer to the Project Collections for detailed information. →

Create Refine Layout

refine headless is not affiliated with any UI. It is entirely up to you to customize your UI. Let's create a simple layout for this example.

The Layout we've created now will only show the refine logo. In the following steps, we will edit our Layout.

components/Layout.tsx
import { LayoutProps } from "@pankod/refine-core";
import { Box, Container, Flex, Image } from "@chakra-ui/react";

export const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<Box
display={"flex"}
flexDirection={"column"}
backgroundColor={"#eeeeee"}
minH={"100vh"}
>
<Container maxW={"container.lg"}>
<Flex justify={"space-between"} mt={4} alignSelf={"center"}>
<a href="https://refine.dev">
<Image alt="Refine Logo" src={"./refine_logo.png"} />
</a>
</Flex>
{children}
</Container>
</Box>
);
};
pages/_app.tsx
import React from "react";
import { AppProps } from "next/app";
import Head from "next/head";
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-nextjs-router";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { ChakraProvider } from "@chakra-ui/react";
import { Layout } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
const dataProvider = DataProvider(API_URL);

return (
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider}
Layout={Layout}
>
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
</Refine>
);
}
layout

Product Card Design with Chakra-UI

Let's design our product cards with Chakra-UI.

src/components/ProductCard.tsx
import React from "react";
import { Box, Image, Badge, Button } from "@chakra-ui/react";

export type ProductProps = {
id: string;
title: string;
description: string;
cardImage: string;
};

export const ProductCard: React.FC<ProductProps> = ({
id,
title,
description,
cardImage,
}) => {
return (
<Box maxH={"sm"} borderWidth="1px" borderRadius="lg" overflow="hidden">
<Image w={"100%"} h={200} src={cardImage} />
<Box p="6" bgColor={"gray.600"}>
<Box display="flex" alignItems="baseline" mb={2} ml={-2}>
<Badge borderRadius="full" px="2" colorScheme="teal">
New Product
</Badge>
</Box>

<Box
mt="1"
fontWeight="semibold"
as="h4"
lineHeight="tight"
isTruncated
color={"white"}
>
{title}
</Box>

<Box color={"white"}>{}</Box>
<Box
color="white"
fontSize="sm"
display={"flex"}
mt={4}
justifyContent={"flex-end"}
></Box>
</Box>
</Box>
);
};
sample_product

We created our Product Card component. Now let's move on to the process of fetch and showing our products from the Strapi.

Fetch Products with SSR

First, let's fetch our products with the nextjs getServerSideProps function.

GetServerSideProps

pages/index.tsx
import { GetServerSideProps } from "next";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct } from "interfaces";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

export const getServerSideProps: GetServerSideProps = async (context) => {
const data = await DataProvider(API_URL).getList<IProduct>({
resource: "products",
metaData: { populate: ["image"] },
});

return {
props: { products: data },
};
};

Create Product List with Refine

Let's process the data we fetch above using refine's useTable hook. Then let's put our data in our ProductCard component.

pages/index.tsx
import { GetServerSideProps } from "next";
import { LayoutWrapper, GetListResponse, useTable } from "@pankod/refine-core";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct } from "interfaces";
import { SimpleGrid } from "@chakra-ui/react";
import { ProductCard } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

type ItemProps = {
products: GetListResponse<IProduct>;
};

export const ProductList: React.FC<ItemProps> = ({ products }) => {
const { tableQueryResult } = useTable<IProduct>({
resource: "products",
queryOptions: {
initialData: products,
},
metaData: { populate: ["image"] },
});

return (
<LayoutWrapper>
<SimpleGrid columns={[1, 2, 3]} mt={6} spacing={3}>
{tableQueryResult.data?.data.map((item) => (
<ProductCard
id={item.id}
title={item.title}
description={item.description}
cardImage={
item.image
? API_URL + item.image.url
: "./error.png"
}
/>
))}
</SimpleGrid>
</LayoutWrapper>
);
};

export default ProductList;

export const getServerSideProps: GetServerSideProps = async (context) => {
const data = await DataProvider(API_URL).getList<IProduct>({
resource: "products",
metaData: { populate: ["image"] },
});

return {
props: { products: data },
};
};
products

Adding Store-based Filtering

We have fetch all the products above. Now, let's fetch the stores and list the store-specific products separately.

First, let's fetch our stores by using the refine useMany hook within the getServerSideProps function. Next we will create buttons for the stores. When these buttons are clicked, a store is selected, we will do a filtering with useTable setFilters and list the products specific to that store.

pages/index.tsx
export const getServerSideProps: GetServerSideProps = async (context) => {
const data = await DataProvider(API_URL).getList<IProduct>({
resource: "products",
metaData: { populate: ["image"] },
pagination: { current: 1, pageSize: 9 },
});

const { data: storesData } = await DataProvider(API_URL).getMany({
resource: "stores",
ids: ["1", "2", "3"],
});

return {
props: {
products: data,
stores: storesData,
},
};
};
pages/index.tsx
import { GetServerSideProps } from "next";
import { LayoutWrapper, GetListResponse, useTable } from "@pankod/refine-core";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct, IStore } from "interfaces";
import { Button, SimpleGrid, Flex, Text } from "@chakra-ui/react";
import { ProductCard, FilterButton } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

type ItemProps = {
products: GetListResponse<IProduct>;
stores: IStore[];
};

export const ProductList: React.FC<ItemProps> = ({ products, stores }) => {
const { tableQueryResult, setFilters } = useTable<IProduct>({
resource: "products",
queryOptions: {
initialData: products,
},
metaData: { populate: ["image"] },
});

return (
<LayoutWrapper>
<Flex mt={6} gap={2}>
<FilterButton
setFilters={() =>
setFilters([
{
field: "stores][id]",
operator: "eq",
value: undefined,
},
])
}
>
<Text fontSize={{ base: "12px", md: "14px", lg: "14px" }}>
All Products
</Text>
</FilterButton>
{stores?.map((item) => {
return (
<FilterButton
setFilters={() =>
setFilters([
{
field: "stores][id]",
operator: "eq",
value: item.id,
},
])
}
>
<Text
fontSize={{
base: "12px",
md: "14px",
lg: "14px",
}}
>
{item.title}
</Text>
</FilterButton>
);
})}
</Flex>
<SimpleGrid columns={[1, 2, 3]} mt={6} spacing={3}>
{tableQueryResult.data?.data.map((item) => (
<ProductCard
id={item.id}
title={item.title}
description={item.description}
cardImage={
item.image
? API_URL + item.image.url
: "./error.png"
}
/>
))}
</SimpleGrid>
</LayoutWrapper>
);
};

export default ProductList;

export const getServerSideProps: GetServerSideProps = async (context) => {
const data = await DataProvider(API_URL).getList<IProduct>({
resource: "products",
metaData: { populate: ["image"] },
pagination: { current: 1, pageSize: 9 },
});

const { data: storesData } = await DataProvider(API_URL).getMany({
resource: "stores",
ids: ["1", "2", "3"],
});

return {
props: {
products: data,
stores: storesData,
},
};
};
pagination

Adding Pagination

We list all products on our All Products page. Let's add pagination to this page and divide the products into pages. We will perform pagination using the pageSize, current and setCurrent properties from the useTable hook.

Refer to the useTable documentation for detailed information. →

pages/index.tsx
import { GetServerSideProps } from "next";
import { LayoutWrapper, GetListResponse, useTable } from "@pankod/refine-core";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct, IStore } from "interfaces";
import { Button, SimpleGrid, Flex, Text } from "@chakra-ui/react";
import { ProductCard, FilterButton } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

type ItemProps = {
products: GetListResponse<IProduct>;
stores: IStore[];
};

export const ProductList: React.FC<ItemProps> = ({ products, stores }) => {
const { tableQueryResult, setFilters, current, setCurrent, pageSize } =
useTable<IProduct>({
resource: "products",
queryOptions: {
initialData: products,
},
initialPageSize: 9,
metaData: { populate: ["image"] },
});

const totalPageCount = Math.ceil(tableQueryResult.data?.total!! / pageSize);

return (
<LayoutWrapper>
<Flex mt={6} gap={2}>
<FilterButton
setFilters={() =>
setFilters([
{
field: "stores][id]",
operator: "eq",
value: undefined,
},
])
}
>
<Text fontSize={{ base: "12px", md: "14px", lg: "14px" }}>
All Products
</Text>
</FilterButton>
{stores?.map((item) => {
return (
<FilterButton
setFilters={() =>
setFilters([
{
field: "stores][id]",
operator: "eq",
value: item.id,
},
])
}
>
<Text
fontSize={{
base: "12px",
md: "14px",
lg: "14px",
}}
>
{item.title}
</Text>
</FilterButton>
);
})}
</Flex>
<SimpleGrid columns={[1, 2, 3]} mt={6} spacing={3}>
{tableQueryResult.data?.data.map((item) => (
<ProductCard
id={item.id}
title={item.title}
description={item.description}
cardImage={
item.image
? API_URL + item.image.url
: "./error.png"
}
/>
))}
</SimpleGrid>
<Flex justify={"flex-end"} mt={4} mb={4} gap={2}>
{Array.from(Array(totalPageCount), (e, i) => {
if (current > totalPageCount) {
setCurrent(i);
}
return (
<Button
colorScheme={"teal"}
onClick={() => setCurrent(i + 1)}
>
{"Page: " + (i + 1)}
</Button>
);
})}
</Flex>
</LayoutWrapper>
);
};

export default ProductList;

export const getServerSideProps: GetServerSideProps = async (context) => {
const data = await DataProvider(API_URL).getList<IProduct>({
resource: "products",
metaData: { populate: ["image"] },
pagination: { current: 1, pageSize: 9 },
});

const { data: storesData } = await DataProvider(API_URL).getMany({
resource: "stores",
ids: ["1", "2", "3"],
});

return {
props: { products: data, stores: storesData },
};
};
page_size

Adding Basket and Payment features with Snipcart

One of the steps that should be in an E-commerce application is the cart and payment transactions. In our example, we will use Snipcart for this process.

Refer to the Snipcart documentation for detailed information. →

Installation Snipcart Widget

pages/_app.tsx
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
const dataProvider = DataProvider(API_URL);

return (
<>
<Head>
<link rel="preconnect" href="https://app.snipcart.com" />
<link
rel="stylesheet"
href="https://cdn.snipcart.com/themes/v3.0.16/default/snipcart.css"
/>
<script
async
src="https://cdn.snipcart.com/themes/v3.0.16/default/snipcart.js"
/>
</Head>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider}
resources={[{ name: "products" }]}
Layout={Layout}
>
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
</Refine>
<div hidden id="snipcart" data-api-key="YOUR_SNIPCART_TEST_KEY" />
</>
);
}

Adding "Add to Basket" Button on ProductCard component

src/components/ProductCard.tsx
import React from "react";
import { Box, Image, Badge, Button } from "@chakra-ui/react";

export type ProductProps = {
id: string;
title: string;
description: string;
cardImage: string;
};

export const ProductCard: React.FC<ProductProps> = ({
id,
title,
description,
cardImage,
}) => {
return (
<Box
maxH={"sm"}
maxW="sm"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
>
<Image w={"100%"} h={200} src={cardImage} />
<Box p="6" bgColor={"gray.600"}>
<Box display="flex" alignItems="baseline" mb={2} ml={-2}>
<Badge borderRadius="full" px="2" colorScheme="teal">
New Product
</Badge>
</Box>

<Box
mt="1"
fontWeight="semibold"
as="h4"
lineHeight="tight"
isTruncated
color={"white"}
>
{title}
</Box>
<Box
color="white"
fontSize="sm"
display={"flex"}
mt={4}
justifyContent={"flex-end"}
>
<Button
className="buy-button snipcart-add-item"
bgColor={"green.400"}
data-item-id={id}
data-item-price="5"
data-item-url="/"
data-item-name={title}
data-item-description={description}
data-item-image={cardImage}
>
Add to Basket
</Button>
</Box>
</Box>
</Box>
);
};
basket

Conclusion

One of the biggest features that distinguishes refine from other frameworks is that it is customizable. Combined with refine headless, it now provides more customization options. This provides a great deal of convenience in the project you will develop.

As you can see in this article, we have developed the Client part of our Admin Panel, which we have done before, with refine. refine offers the opportunity to develop B2B and B2C applications without any restrictions and in a fully customizable manner.

Refer to the Admin side of the project →

Live Codesandbox Example



Related Articles

Frontend developer
Build a Progressive Web App (PWA) with Next.js
· 19 min read
Software Developer
Top React Component Libraries and Frameworks Every Developer Should Know
· 5 min read
Software Developer
Build low-code, customizable and authorization ready (accesscontrol) admin panel with NestJS.
· 4 min read

From Same Author

Frontend Developer
How to Create Full Stack React/Next.JS Web App in Few Hour
· 9 min read
Frontend Developer
Create Full Featured Admin Panel with React and Ant Design
· 13 min read
Frontend Developer
How to Multipart File Upload Using FormData with React Hook Form
· 7 min read