Modern eCommerce storefront with Xata and Cloudinary Gallery Widget
Tags: webdev, database, jamstack, nextjs
Published: 2022-11-23 12:08:08

E-commerce stores, as we all know, have a high demand in current times. From my observation on freelancing platforms, more than 50% of developers have had a gig to build an e-commerce storefront at least once. As such, developers need a fast and straightforward approach to creating e-commerce websites without compromising security and performance. 

In this article, we will look at a quick method of building a performant e-commerce storefront using the following tools:

- [Xata](https://xata.io/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a serverless database that saves the time and effort of handling the servers ourselves.
- [Cloudinary Product gallery](https://cloudinary.com/documentation/product_gallery/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a Cloudinary widget handles product image displays with already-styled components.
- [FingerprintJS](https://fingerprint.com/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a fingerprinting library that assigns a unique id to site visitors based on their browser attributes.
- [Next.js](https://nextjs.org/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a React framework that offers serverside rendering and a fast build time.

## Prerequisite

This article requires knowledge of Next.js and its API routes for a smooth flow. Also, we will need to create a [Xata account](https://app.xata.io/signin/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog), a [Cloudinary account](https://cloudinary.com/users/register_free/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) and [FingerprintJS](https://dashboard.fingerprint.com/signup/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) keys. They are all free-tier accounts.
We also assume to have a Next.js boilerplate with TailwindCSS configured and opened in our text editor.

## What is Xata

Xata is a serverless database built on Next.js. It offers a variety of data types that includes:

- String: a short text
- Link: a connection between data tables
- Email: an email address
- Date
- Integer: all whole numbers
- Long text: a lengthy text; as seen in a description
- decimals: all fractions of numbers

It also supports a variety of languages, such as Typescript and Python.

## What is Cloudinary

Cloudinary is a SaaS solution for managing web and application media assets
. It helps developers serve optimised web assets while modifying them on the fly. 
Cloudinary offers a variety of other services asides from media hosting and management. Some of this includes; the product gallery widget, which will be used in this article.

## Setup Development Environment

Install the following dependencies in your Next.js boilerplate:

   ` yarn add fingerprintjs/fingerprint-pro-react axios @xata.io/client `

## App Demo

The app demo can be found [here](https://jamzstore.netlify.app/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog), and the code [here](https://github.com/yhuakim/jamstore/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

{% embed https://www.dropbox.com/s/gt9fvu9riithi2o/Screen%20Recording%202022-11-08%20at%2005.55.25.mov?dl=0 %}

## Getting Started with Xata

Here we will talk about how to integrate Xata into our applications seamlessly.

## Setting up a database

To use Xata, we will create a workspace in our free tier account. The image below shows steps for creating a database in Xata;


![create xata database](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667838131830_image.png)
![create xata database](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667838477986_Screenshot+2022-11-07+at+09.35.31.png)

![created database](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667838540206_Screenshot+2022-11-07+at+09.35.50.png)


Next, navigate to the *e-commerceDB* and click on the *Schema* navigation on the menu bar. Now, the screen below comes into view;


![schema page](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667837959623_image.png)

At this point, let's create our database schema for the demo store. Every store has products it sells; thus, let's create a **products table** for our store. The **products table** will consist of the following columns:

- *name:* a type of **string.** Stores the name of each product
- *desc:* a type of **long-text.** Holds the description of each product
- *imageUrl:* a type of **string.** Stores the image URL served from Cloudinary for each product
- *price:* a **decimal** type. Stores each product's price
- *tags:* a type of **string**. Stores the tags for each product
- *discount_id:* a **link** that is a reference to the **product_discount table**.
![products table](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667842062244_Screenshot+2022-11-07+at+18.02.44+5.png)
![products table linked to product_discount table](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667842096193_Screenshot+2022-11-07+at+18.16.09.png)


The **product_discount table** consists of 

- *name:* a **string** type that stores the discount's name, such as "*ECOMMERCE20*."
- *isActive:* a **boolean** type that holds the validity of the discount
- *discount_percent:* a **decimal** type that stores the percentage decrease in the product price.


![discount_table](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667860163302_Screenshot+2022-11-07+at+23.27.20.png)


Next, let's create a **user table** that will store the **id** generated by FingerprintJS. After this, let's create a **cart table** that stores each user's selected products. This table will consist of the following column:

- *product_id:* a reference to the properties of the desired product
- *user_id:* a reference to the user who picked the product(s)
- *quantity:* the total quantity per product selected by the user
- *amount:* the total price per product picked by the user

Once we have successfully created these tables, it's time to populate them with some dummy data.
First, let's download and host our product images on Cloudinary. To do this, we will upload our desired product images to Cloudinary while adding a unique **tag** to each. Then, Cloudinary generates a URL for each image we will use in the *imageUrl* column of each product. The image below shows a sample record created.

![xata record sample](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667883222252_Screenshot+2022-11-08+at+05.52.49.png)



## Fetching Products from Xata Database

Open up the Next.js boilerplate and navigate to the `pages/index.js` directory. In the file add the following snippet   

```javascript
// pages/index.js
import { BaseClient } from '@xata.io/client';
    import Products from '../components/Products';
    
    export default function Home({ products }) {
      return (
        <div className="">
          <main>
            <div id='products' className="">
              <h1 className="text-3xl font-semibold text-center py-10">Products</h1>
              <Products products={products} />
            </div>
          </main>
        </div>
      )
    }
    export async function getServerSideProps() {
      const xata = new BaseClient({
        branch: 'main',
        apiKey: process.env.API_KEY,
        databaseUrl: process.env.XATA_DATABASE_URL
        //fetch: fetchImplementation // Required if your runtime doesn't provide a global `fetch` function.
      });
      const page = await xata.db.products
        .select(["*", "discount_id.*"])
        .getAll()
      return {
        props: {
          products: page,
        }
      }
    }
```

The snippet above handles fetching our products from the database. To achieve this,

- First, import `BaseClient` from `@xata.io/client`, and `axios`  from `axios`
- Next, export a `Home` page function with a prop of `products`
- Then, create an asynchronous `getServerSideProps``()` function, that initialises the Xata client. To initialise our Xata client we need to have gotten our API keys from Xata and added them to a `.env` file


> Our API keys and database URL, can be found as shown in the image below
![Xata Settings](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667878271816_Screenshot+2022-11-08+at+04.29.56.png)

- Next, our products will be fetched using the Xata method; `xata.db.products` and selecting all products using `"``*``"`. While appending each linked(referenced) table to the results. This will return a response as shown below;
![log data](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667879309140_Screenshot+2022-11-08+at+04.48.09.png)

Since the products are now available as props on our `Homepage`, let’s display them. To achieve this, let’s create a `components` directory and a `Products.jsx` file. Inside this file add the following snippet:

```javascript
    //components/Products.jsx
    import Link from "next/link"
    const Products = ({ products }) => {
        return (
            <div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 justify-items-center">
                {
                    products && products.map((prod, index) => (
                        <div className="relative w-36 md:w-52 bg-white rounded-md border border-gray-200 shadow-md m-3" key={index}>
                            <small>
                                <b className={`${!prod.discount_id ? 'none' : 'absolute top-2 right-2 text-xs text-red-300 bg-red-100 p-1'}`}>{prod.discount_id && prod.discount_id.isActive ? `${prod.discount_id.discount_percent * 100}%` : ''}</b>
                            </small>
                            <div className="">
                                <Link href={`/product/${prod.id}`} className="">
                                    <img className='object-cover w-36 md:w-52 h-48' src={prod.imageUrl} alt="" />
                                </Link>
                            </div>
                            <div className="card-info py-2 px-2">
                                <Link href={`/product/${prod.id}`}>
                                    <h3>{prod.name}</h3>
                                    <p>
                                        <b>₦{prod.discount_id && prod.discount_id.isActive ? (prod.price - (prod.price * prod.discount_id.discount_percent)) : prod.price}</b> <br />
                                        <small><s className={`${!prod.discount_id ? 'hidden' : 'text-gray-500 decoration-gray-500'}`}>₦{prod.discount_id && prod.discount_id.isActive ? prod.price : ''}
</small>
                                    </p>
                                </Link>
                            </div>
                        </div>
                    ))
                }
            </div>
        )
    }
    export default Products

```
The above component is then imported into our `pages/index.js` file. Where the fetched `products` are passed as `props` to it. Then, it maps through these products and displays them as shown below:

![displalyed products](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667880389330_Screenshot+2022-11-08+at+05.05.52.png)

## Creating User Identification using FingerprintJS

An e-commerce store must be aware of its visitors to track their activities. One of the essential activities to follow is the "what" product the user is adding to their cart.
However, we wouldn't want to bug your user with a login form constantly. To eliminate this, the app will silently create an ID unique to every user using a fingerprinting library. Now, let's walk through the code. Navigate to `/pages/_app.jsx` and add the following snippet.

```javascript
// pages/_app.jsx
import { FpjsProvider } from '@fingerprintjs/fingerprintjs-pro-react'
    import NavBar from '../components/NavBar'
    import '../styles/globals.css'
    function MyApp({ Component, pageProps }) {
      return (
        <FpjsProvider
          loadOptions={{
            apiKey: process.env.FPJS_KEY
          }}
        >
          <NavBar />
          <Component {...pageProps} />
        </FpjsProvider>
      )
    }
    export default MyApp
```

In the snippet above, FingerprintJS is tied to our application using a provider from `@fingerprinjs/fingerprintjs-pro-react` and initialised with the API Keys.

Next, let’s create a server logic in our API route. This logic will receive our user’s **id** and search for instances in the database. Then create a new user record if there is no instance of the user. To achieve this, we will add a file to `/pages/api` called `assignuser.js`. Then we add the following snippet:

```javascript
// pages/api/assignuser.js
import { BaseClient } from '@xata.io/client';
    export default async function handler(req, res) {
      const { visitorID } = req.body
      const xata = new BaseClient({
        branch: 'main',
        apiKey: process.env.API_KEY,
        databaseURL: process.env.XATA_DATABASE_URL
        //fetch: fetchImplementation // Required if your runtime doesn't provide a global `fetch` function.
      });
      try {
        //Fetch all users and find a match for the visitorID
        const records = await xata.db.anon_user.select(["*"]).filter("id", visitorID).getAll()
        //if a match is not found, create a user with the visitorID
        if (records.length === 0) {
          await xata.db.user.create(visitorID, {
            _id: visitorID
          })
        }
        res.status(200)
      } catch (err) {
        console.error(err)
      }
    }
```

In the above snippet, we will:

- import `BaseClient` from our `@xata.io/client` and initialise a client with our valid credentials
- create a handler function where we fetch users from Xata and filter them by the `visitorId` received from our clientside
- check if a User with the `visitorID` exists. If it doesn’t exist, we create a new record but if it does we will do nothing

Next, let’s create a client side logic to post the user’s **id**  to our API route `/api/assignuser`. Navigate to `/pages/index.jsx` and add the following snippet:


```javascript
    // pages/index.jsx
    import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react'
    import { useEffect } from 'react'
    import axios from 'axios'
    export default function Home({ products }) {
      const { isLoading, data } = useVisitorData({ immediate: true })
      useEffect(() => {
        const registerAnon = async () => {
          if (!isLoading) {
            const visitorID = data?.visitorId
            await axios.post('/api/assignuser', { visitorID })
          }
        }
        registerAnon()
      }, [isLoading, data])
      return (
        <div className="">
          ...
        </div>
      )
    }
    export async function getServerSideProps() {
      ...
    }
```
    

In the above snippet, we will: 

- import `useVisitorData` from `@fingerprintjs/fingerprintjs-pro-react`
- destructure the values of `isLoading` and `data` from `useVisitorData`
- initiate a `useEffect` hook and create a `registerAnon` function. Here we will, pull the value of `visitorId` from `data`. Next, we will make a `post` request to the API route `/api/assignuser` with the generated `visitorID` as the body

With this, we have successfully assigned an ID to the visitor and stored it in our database.

## Creating a Single Product Page

Here we will leverage Next.js dynamic routing features to create a page for a single product gallery display.

**Creating Dynamic Routing**
First, create a `product` directory in the `pages` folder. Then we will create a dynamic file named `[id].jsx`. In this file add the following snippet:

```javascript
   // pages/product/[id].jsx
    import { BaseClient } from '@xata.io/client';
    
    const Product = ({ product }) => {
        return (
        ...
        )
    export default Product;
    export async function getStaticPaths() {
        const xata = new BaseClient({
            branch: 'main',
            apiKey: process.env.API_KEY,
            databaseURL: process.env.XATA_DATABASE_URL
            //fetch: fetchImplementation // Required if your runtime doesn't provide a global `fetch` function.
        });
    
        const page = await xata.db.products
            .select(["*", "category_id.*", "inventory_id.*", "discount_id.*", "tag_id.*"])
            .getAll()
        const products = page
        const paths =
            products &&
            products.map((prod) => ({
                params: { id: prod.id }
            }));
        return {
            paths: paths,
            fallback: false
        };
    }
```

In the above snippet, we will:

- create a `Product` page component and import `BaseClient` from `@xata.io/client`
- create a `getStaticPaths` asynchronous function. Here we will fetch all our products `id` from Xata and assign them as `paths`

Next, we will filter these `paths` and return only the user-requested product. To do this add the following snippet:


```javascript
    // pages/product/[id].jsx
    
    // this line is below getStaticPaths()
    export async function getStaticProps({ params }) {
        const xata = new BaseClient({
            branch: 'main',
            apiKey: process.env.API_KEY,
            databaseURL: process.env.XATA_DATABASE_URL
            //fetch: fetchImplementation // Required if your runtime doesn't provide a global `fetch` function.
        });
    
        const page = await xata.db.products
            .select(["*", "category_id.*", "inventory_id.*", "discount_id.*", "tag_id.*"])
            .getAll()
        const products = page
        const product =
            products && products.filter((prod) => params.id === prod.id);
        return {
            props: {
                product
            }
        };
    }
```

In the above snippet, we have also returned our filtered product as a page props.

Now, we need to display this requested product to the user by adding the following snippet to `pages/product/[id].jsx`:

```javascript
    // pages/product/[id].jsx

    import { useState } from "react";
    const Product = ({ product }) => {
          return (
            <div className='relative mx-auto'>
                <div className="flex flex-col md:w-6/6 p-8 md:flex-row">
                    <div className="price md:w-2/6">
                        <h3 className='text-3xl font-semibold'>{product[0].name}</h3>
                        <p className='text-lg'>
                            {product[0].desc} <br />
                            Lorem ipsum dolor sit amet consectetur adipisicing elit. Laudantium eaque totam aperiam temporibus asperiores, soluta eligendi architecto magni tempora. Perspiciatis a sequi id ut qui eligendi illo non unde vero!
                            Numquam quia maxime laudantium, provident libero fuga, voluptatem animi eveniet aliquid beatae mollitia odit laborum amet dolores. Id adipisci quidem nostrum alias dolorem ea, commodi rem repellendus repellat nihil molestias.
                        </p>
                        <p>
                            <b>₦{product[0].discount_id && product[0].discount_id.isActive ? (product[0].price - (product[0].price * product[0].discount_id.discount_percent)) : product[0].price}</b> <br />
                            <small><s className={`${!product[0].discount_id ? 'hidden' : 'text-gray-500 decoration-gray-500'}`}>₦{product[0].discount_id && product[0].discount_id.isActive ? product[0].price : ''}
</small>
                        </p>
                    </div>
                </div>
            </div>
        );
    }
```

The snippet above renders our product `title`, `price`  and `description`.
Next, we will display the images for each product. To achieve this, we will use Cloudinary’s product gallery widget feature.

**S****etup for Clouding Product Gallery Widget**
Cloudinary gallery widget retrieves images (or videos) with specified tags based on the [Client-side asset lists](https://cloudinary.com/documentation/advanced_url_delivery_options#client_side_asset_lists/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) feature. To learn more about cloudinary product gallery see [here](https://cloudinary.com/documentation/product_gallery/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

> Ensure that the Resource list option is enabled on your Cloudinary account to use this feature. By default, the list delivery type is restricted. In your console, navigate to Security settings and clear the Restricted media types item under the Resource list.
![Cloudinary widget settings](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667853177320_Screenshot+2022-11-07+at+21.25.08.png)
![Cloudinary widget settings](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667853186912_Screenshot+2022-11-07+at+21.29.10.png)


Next, add the following snippet to `/pages/_document.js`

```javascript
    // pages/_document.js
    import Document, { Html, Head, Main, NextScript } from "next/document";
    class MyDocument extends Document {
        render() {
            return (
                <Html>
                    <Head />
                    <body>
                        <Main />
                        <NextScript />
    //script for cloudinary product gallery
                        <script
                            src="https://product-gallery.cloudinary.com/all.js"
                            type="text/javascript"
                            strategy="beforeInteractive"
                        ></script>
                    </body>
                </Html>
            );
        }
    }
    export default MyDocument;
```

The `script` marked above fetches the product widget before the page becomes interactive.

Next, to render this in our product page, we will add the following snippet:

```javascript
    // pages/product/[id].jsx'
    
    import { useState, useEffect } from "react";
    import axios from 'axios';
    const Product = ({ product }) => {
        useEffect(() => {
            const productGallery = cloudinary.galleryWidget(
                {
                    container: "#gallery",
                    cloudName: "jilis",
                    mediaAssets: [{ tag: `${product[0].tag_id.tagname}`, mediaType: "image" }]
                },
                []
            );
            productGallery.render();
        });
          return (
            <div className='relative mx-auto'>
                <div className="flex flex-col md:w-6/6 p-8 md:flex-row">
                    //the line below renders the product gallery
                    <div className="md:w-4/6 md:h-96 px-5">
                        <div className='' id="gallery"></div>
                    </div>
                  ...
                </div>
            </div>
        );
    }
```

In the above snippet, we are using the `useEffect` hook to fetch and render the Gallery widget into an element with an `id` of `gallery`. The images to be rendered are hosted on cloudinary and have same `tagname` with the user-requested product.  Below is how the UI will appear:


![product page](https://paper-attachments.dropboxusercontent.com/s_7C99576C21A100D1A16AB03233B1B24975C8E9318A5F622E0F0F49D9F30CD524_1667999243989_Screenshot+2022-11-09+at+14.07.13.png)



## Conclusion

Building with the Xata database was a swift experience. I enjoyed working out my code on their Playground interface before going into my editor. Also, integrating the Cloudinary gallery widget was seamless, not to mention how fast my images were delivered since it was hosted on Cloudinary. These tools are amazing and I believe every developer will be excited to jump on them too to create awesomeness.

## Resources

[Cloudinary Gallery Widget](https://cloudinary.com/documentation/product_gallery/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
[Xata Documentation](https://xata.io/docs/overview/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
[FingerprintJs Docs](https://dev.fingerprint.com/docs/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)