Next.js E-commerce App with Strapi and Chakra UI
Tags: refine, nextjs, ecommerce, webdev
Published: 2022-02-16 06:30:07

---
canonical_url: https://refine.dev/blog/handcrafted-nextjs-e-commerce-app-tutorial-strapi-chakra-ui/
---

![refine e-commerce examples](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uikauh9kmb8xd4bg163i.gif)
In this article, we will create the **e-commerce app** of our [Strapi-Multitenancy](/docs/guides-and-concepts/multi-tenancy/strapi-v4) 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](https://strapi.io/) and [Chakra-UI](https://chakra-ui.com/) together with [**Next.js**](/docs/guides-and-concepts/ssr-nextjs/) in our E-commerce client example application.

## Refine Project Setup

Let's start by creating our **refine** project. You can use the [superplate](https://github.com/pankod/superplate) to create a refine project.

```bash
npx superplate-cli -p refine-nextjs refine-ecommerce-example
```

```bash
✔ 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](/docs/guides-and-concepts/data-provider/strapi-v4/) and Chakra-UI packages that we will use later.

## Installation

```bash
cd refine-ecommerce-example

npm i @pankod/refine-strapi-v4
npm i @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/index.tsx:`

```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/index.tsx:`

```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](https://docs.strapi.io/developer-docs/latest/getting-started/quick-start.html).

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. →](https://refine.dev/docs/guides-and-concepts/multi-tenancy/strapi-v4/#create-collections)

## 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:`

```tsx 
import { Box, Container, Flex, Image } from "@chakra-ui/react";

export const Layout: React.FC = ({ 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:`

```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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0evksbgxpv327zjl90eg.png)

## Product Card Design with Chakra-UI

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

`src/components/ProductCard.tsx`

```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>
    );
};
```

![Product Card](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/076j5z5sn9hdhvuolh35.png)

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:`

```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:`

```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 List](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbjuukjlx23r7npx5d0e.png)
 
## 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:`

```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:`

```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,
        },
    };
};
```

![Store Pagination](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uvq8yr058pjzet5k0b03.gif)
 

## 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. →](https://refine.dev/docs/core/hooks/useTable/#pagination)

`pages/index.tsx:`

```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 },
    };
};
```
 
![Pagination](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hr9w3uf5qo0jithc0uv.gif)

## 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](https://snipcart.com/) for this process.

[Refer to the Snipcart documentation for detailed information. →](https://docs.snipcart.com/v3/)

### Installation Snipcart Widget

`pages/_app.tsx:`

```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:`

```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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mbat8dale702amu4sqf.gif)
 
## 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](https://refine.dev/docs/guides-and-concepts/multi-tenancy/strapi-v4/), 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 →](https://refine.dev/docs/guides-and-concepts/multi-tenancy/strapi-v4/)

[Source Code](https://github.com/pankod/refine/tree/master/examples/blog/ecommerce)

[Live CodeSandbox Example](https://codesandbox.io/s/refine-ecommerce-example-9rvzv)

[Check out for detailed information about refine. →](https://refine.dev/docs/getting-started/overview/)