How to Create a Product Review Application in Next.js
Tags: nextjs, webdev, javascript, tutorial
Published: 2022-11-24 19:27:56

Making important decisions sometimes requires the opinions of people more knowledgeable than us, especially for new products. Getting people to share their thoughts on items they've used in the past can save us from disappointment and waste in general. A product review application is an example of a platform that delivers these opinions to us.


## What we will be building

This article discusses creating a product review application using the Cloudinary upload widget and storing the data from this application on our Xata database.

**GitHub URL**
[https://github.com/Iheanacho-ai/product-review](https://github.com/Iheanacho-ai/product-review)


## Prerequisites
To get the most out of this article, we require the following:
- A basic understanding of CSS, JavaScript, and React.js
- A Cloudinary account we can create [here](https://cloudinary.com/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- A Xata account. Create a free one [here](https://xata.io/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)

## Setting up our Next.js app

[Next.js](https://nextjs.org/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is an open-source React framework that enables us to build server-side rendered static web applications.

To create our Next.js app, we navigate to our preferred directory and run the terminal command below: 
```bash
    npx create-next-app@latest
    # or
    yarn create next-app
```
After creating our app, we change the directory to the project and start a development server with:

```bash
    cd <name of our project>
    npm run dev
```
To see our app, we go to [http://localhost:3000](http://localhost:3000).


## Installing Tailwind CSS
[Tailwind CSS](https://tailwindcss.com/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is a "utility-first" CSS framework that allows us to create user interfaces for web applications rapidly.

To install Tailwind CSS in our project, we run these terminal commands.

```bash
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
```
These commands create two files in the root directory of our project, `tailwind.config.js` and `postcss.config.js`.

In our `tailwind.config.js`, we add the paths to all our template files with the code below.

```js
    module.exports = {
      content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
```
Next, we add the tailwind directives in our `styles/global.css` file.

```css
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
```

## Installing the Cloudinary Dependencies
[Cloudinary](https://cloudinary.com/documentation?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is a cloud-based service that provides an end-to-end image and video management solution, including uploads, storage, manipulations, optimizations, and delivery.

We run this terminal command to install the Cloudinary dependencies in our project.

```bash
    npm i @cloudinary/url-gen @cloudinary/react
```

## Creating a Xata Workspace
[Xata](https://xata.io/docs/overview?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is a [serverless data platform](https://xata.io/docs/intro/serverless-data-platform?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) that offers developers a serverless relational database, search engine, and analytics engine, all behind a consistent API. 

Xata allows developers to build applications easier and faster.

To start using Xata, we need to create a workspace. A Xata workspace represents an organization and helps to secure our data.

Next, we click on 'Add a database' and create a Xata database.

![Xata Database](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669192922847_Screenshot+763.png)


After creating our database, we click on 'Start from Scratch' to create tables.

![Xata database](https://paper-attachments.dropboxusercontent.com/s_28E749F44F325008AA9D2C9B2E71454BC2A0F048786C941DF7C38C59EFF33975_1669196512631_Screenshot+738.png)




![Xata Database](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669192989028_Screenshot+782.png)


We add tables to our database by clicking the '+' icon on the table header. 

![Xata Database](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193020060_Screenshot+783.png)


These tables can represent data attributes, and we will create four more tables in addition to the default **id** table, which are:
- **productName,** which holds strings
- **productPrice,** which contains integers ****
- **productImage**, is an image URL, which is a type of Link
- **productReview**, which holds long texts


![Xata Database Tables](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193047188_Screenshot+789.png)

## Installing Xata
After creating our database, we install the Xata CLI globally with this terminal command:

```bash
    npm i -g @xata.io/cli
```
Next, we authenticate our identity by logging in with this terminal command:

```bash
    xata auth login
```
When we execute this command, we are given two options to either create a new API key or use an existing key.  We will create a new API key because this is a new project. To know more about Xata API keys, check out the [Xata documentation](https://xata.io/docs/concepts/api-keys).

Next, we initialize our project by running this terminal command:
```bash
    xata init
```
Running this command will prompt a questionnaire. We should answer these questions as follow:

![Xata initialization questions](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669235099316_Screenshot+803.png)


After answering the questions, Xata will add our API key to our `.env` file, and our project will be set up to use Xata. 

We restart our development server for our project to read the contents in our `.env` file.

## Creating our Product Review Form
We create our product review page in our `index.js` file. This page will be divided into two sections, one to collect product information and reviews and the other to display the product with the data.

In this section, we will work on the form for collecting product data. To create the Tailwind CSS-styled form, we paste this code into our `index.js` file.

 [https://gist.github.com/Iheanacho-ai/998c7ba832c21a36ff7226e03ee4a4a0]
 
Next, we add these styles in our `global.css` file to center and re-size our form.
```css
    .product-container{
      margin-left: 37%;
      width: 30%;
    }
```

Here is how our product review form looks:

![Product Review Form](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193178825_Screenshot+790.png)

## Embedding the Cloudinary Upload Widget
To upload images to our Xata database, we will use the Cloudinary upload widget. The Cloudinary upload widget is an interactive user interface that allows users to upload media from various sources.

We need to include the Cloudinary widget JavaScript file in the `Head` section of our `index.js` file to use the widget.

```jsx
    <div className= 'product-catalog'> 
      <Head>
        <script src="https://upload-widget.cloudinary.com/global/all.js" type="text/javascript"/>
      </Head>
    ...
```
**Creating Cloudinary upload presets**
Cloudinary's upload presets allow us to define an action or a set of actions that will occur when we upload media.

When performing uploads with the Cloudinary upload widget, we need to specify an upload preset.

To create an upload preset, we go to our [Cloudinary Console](https://cloudinary.com/console) and click on the **Settings** tab.

![Cloudinary Console Settings Tab](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193274626_Screenshot+787.png)


Next, we click on the **Upload** tab and scroll down to the **Upload presets** section of the page.

![Cloudinary Console Upload tab](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193446921_Screenshot+791.png)
![Cloudinary Console Upload page](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193446952_Screenshot+792.png)


Next, we click on **Add upload preset**. We can use the **upload preset name** given to us by Cloudinary, or rename the upload preset.

Then we change the **Signing Mode** to 'Unsigned' and click the **Save** button to save our upload preset.


![Cloudinary Console Upload preset page](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669193493435_Screenshot+794.png)


We then copy the upload preset name as we need it when creating our widget.

**Creating our Cloudinary Upload Widget**
In our `index.js` file, we create an `openupWidget()` function to embed and open up our widget.

```jsx
  const openupWidget = () => {
    window.cloudinary.openUploadWidget(
      { cloud_name: ***, //we add our cloud name hwe
        upload_preset: 'xoskczw2'
      },
      (error, result) => {
        if (!error && result && result.event === "success") {
          console.log(result.info.url)       
        }else{
          console.log(error)
        }
      }
    ).open();
  } 
```    

In the code block above, we do the following:

- Use the `openUploadWidget()` method that receives two parameters, the `cloud_name` and our upload preset. To get our `cloud_name`, we go to our [Cloudinary Dashboard](https://cloudinary.com/console)
- Logs the image URL or the error encountered to the console depending on if the upload was successful or not

Next, we pass the `openupWidget()` function to an `onClick` event listener on our "Upload files" button. The `onClick` event listener calls our `openupWidget()` function when the button is clicked.

```jsx
    <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" type="button" onClick= {openupWidget}>
        Upload files
    </button>
```

## Adding Interaction with our Xata Database

To efficiently interact with the Xata database, we create four variables. These variables will collect the data from our forms and store it in our database.

To create the variables, we need to import the `useState` hook in our `index.js` file.

```jsx
    import { useState } from 'react';
```
Next, we create the variables with this piece of code:
```jsx
    const [productName, setProductName] = useState()
    const [productPrice, setProductPrice] = useState()
    const [productReview, setProductReview] = useState()
    const [productImage, setproductImage] = useState()
```

The variables hold the following information:
- The `productName` variable has the name of our product
- The `productPrice` variable holds the price of our product
- The `productReview` variable contains the reviews on our product
- The `productImage` variable holds the image URL of the product

Our `openupwidget()` function updates the `productImage` variable.

```js
  const openupWidget = () => {
    window.cloudinary.openUploadWidget(
      { cloud_name: 'amarachi-2812',
        upload_preset: 'xoskczw2'
      },
      (error, result) => {
        if (!error && result && result.event === "success") {
          //we save the image URL
          setproductImage(result.info.url)
        }
      }
    ).open();
  } 
```
Next, we put the variables and their respective functions in the input fields to store the input values.

```jsx
    {/* productName variable goes here */}
    <div>
       <label htmlFor="about" className="block text-sm font-medium text-gray-700">
          Name
        </label>
        <div className="mt-1">
            <textarea
              id="about"
              name="about"
              rows={1}
              value= {productName}
              onChange = {(e)=> setProductName(e.target.value)}
              className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
              />
         </div>     
    </div>
    
          {/* productPrice variable goes here */}  
    <div>
       <label htmlFor="about" className="block text-sm font-medium text-gray-700">
          Price
        </label>
        <div className="mt-1">
           <textarea
              id="productPrice"
              name="productPrice"
              rows={1}
              value= {productPrice}
              onChange = {(e)=> setProductPrice(e.target.value)}
              className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
            />
        </div>
    </div>
    
          {/* productReview variable goes here */}  
    <div>
      <label htmlFor="about" className="block text-sm font-medium text-gray-700">
        Review
        </label>
        <div className="mt-1">
           <textarea
             id="productReview"
              name="productReview"
              rows={3}
              value= {productReview}
              onChange = {(e)=> setProductReview(e.target.value)}
              className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
             />
        </div>  
    </div>           
```
**Adding data to our database** 
In our `api/pages` folder, we create a `add-product.js` file to allow us safely interact with our database without exposing our Xata API key.

Next, we add this code in our `/api/add-product.js`.
```js
    //pages/api/add-product.js
    import { getXataClient } from '../../src/xata';
    
    const xata = getXataClient();
    
    const handler = async (req, res) => {
      const {productName, productPrice, productReview, productImage} = req.body;
      const result = await xata.db["Product-Review"].create({productName, productPrice, productReview, productImage});
      res.send({result});
    };
    export default handler;
```
 In the code block above, we do the following:

- Import  the `getXataClient` from the `xata` file that was automatically created for us during project initialization. We then create a new instance with the `getXataClient` object
- We then pull out the `productName`, `productPrice`, `productReview`, and `productImage` variable from the body of our request to add to the database 
- Pass the variables in the Xata `create` method to create data on our database. It is important to note that we get the `create()` method from the `Product-Review` object because that is the name of our database, and the variables are the same as the tables on our database
- Send the resulting data to the client side after saving the data in our database

In our `index.js` file, we create a `submitProduct` that will query our `api/add-product` endpoint.

```js
    const submitProduct = () => {
      fetch('/api/add-product', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          productName,
          productPrice,
          productReview,
          productImage
        })
      }).then(() => {
        window.location.reload();
      }).catch((error)=> {
          console.log(error)
      });
    }
```
In the code block above, we query the `api/add-product` endpoint that will safely connect to the Xata database. We then pass the `productName`, `productPrice`, `productReview`, and `productImage` variables in the request's body so that our endpoint can access them.

We then pass the `submitProduct` function to the `onClick` event listener on our 'Save' button. 
```jsx
    <button
        type="submit"
        onClick={submitProduct}
        className="cursor inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
        >
          Save
    </button>
```
**Deleting data from our database**
In our `pages/api` folder, we create a `delete-product.js` file. The `delete-product.js` file will contain this code.

```js
    import { NextApiHandler } from "next";
    import { getXataClient } from '../../src/xata';
    
    const xata = getXataClient();
    const handler = async (req, res) => {
      const { id } = req.body;
      await xata.db["Product-Review"].delete(id);
      res.end();
    };
    export default handler;
```
In the code block above, we delete a product from our database using an `id` that we get from the request body. The `id` allows our handler to find and successfully delete a product.

Next, we create a `deleteProduct` function in our `index.js` file.
```js
    const deleteProduct = (id) => {
      fetch("/api/delete-product", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ id }),
      }).then(() => {
        window.location.reload();
      }).catch((error)=> {
          console.log(error)
      });
    }
```
In the code block above, we query the `/api/delete-product` endpoint and pass in the `id` the `/api/delete-product` handler needs. After deleting the image, we reload the window to see the changes in our application.

**Collecting Data from our database**
After writing the logic for creating and deleting data, we want to collect it in our database and render it in our application.

In our `ìndex.js` file, we write this code:
```js
    export const getServerSideProps = async () => {
      const xata = getXataClient();
      const products = await xata.db["Product-Review"].getAll()
      return { props: { products } }
    }
```
Querying our data in `getServerSideProps` ensures that the function runs on the backend and collects our data before we render the page.

We pass our products as props to our `Home` page in the code block above.

```jsx
    const Home = ({products}) => {
        return(
          ...
        ) 
    }
    
    export const getServerSideProps = async () => {
      const xata = getXataClient();
      const products = await xata.db.product.getAll()
      return { props: { products } }
    }
```
After this section, here is how our `index.js` file looks:

https://gist.github.com/Iheanacho-ai/afb5a008c69d01861bd9d6bbd3ecd3d0

## Creating our Product Listing Page

After creating the product and its reviews, we want to render it on the home page.

In our `index.js` file, we write this piece of code.
```jsx
    <div className="bg-white">
        <div className="max-w-2xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:max-w-7xl lg:px-8">
        <h2 className="sr-only">Images</h2>
        <div className="grid grid-cols-1 gap-y-10 sm:grid-cols-2 gap-x-6 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
           {
              products.map(({productImage, productReview, productPrice, id}) => (
                  <a href="#" className="group" id= {id}>
                    <div className="w-full aspect-w-1 aspect-h-1 bg-gray-200 rounded-lg overflow-hidden xl:aspect-w-7 xl:aspect-h-8">
                        <img src={productImage} alt="Tall slender porcelain bottle with natural clay textured body and cork stopper." className="w-full h-full object-center object-cover group-hover:opacity-75" />
                      </div>
                       <h3 className="mt-4 text-sm text-gray-700">${productPrice}</h3>
                        <div>{productReview}</div>
                        <button
                          type="button"
                          className="cursor inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                        onClick={()=> deleteProduct(id)}
                       >
                      Delete
                    </button>
                  </a>
                ))
              }
            </div>   
        </div>
    </div>
```
In the code block above, we loop through the `products` prop to render each product with its name, price, and review. Next, we add a `deleteProduct()` function on the `onClick` event listener on our 'Delete' button.

After this section, here is how our `index.js` file looks:

https://gist.github.com/Iheanacho-ai/2b5e21d46f7b7bd75640062592643837

With this, we have created our product review application. Here is how our application looks.

![Product Review Application](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669194723112_giphy.gif)

Check out our Xata database to see our data getting saved.

![Xata Database](https://paper-attachments.dropboxusercontent.com/s_23D7FEC3B8591185C0C830AF6D5962D84B17D1A15CB2079215DB3A8AF2DB9D92_1669194233124_Screenshot+796.png)

## Conclusion

This article discusses using the Cloudinary upload widget to collect product images to create a product review application and store them on our Xata database.

## Resources
These resources can be useful:
- [Querying Data from a Next.js App](https://xata.io/docs/quickstart/building-a-web-app-with-xata-and-nextjs?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- [What are upload presets and how to use them](https://support.cloudinary.com/hc/en-us/articles/360016481620-What-are-Upload-presets-and-how-to-use-them?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- [Upload Widget - Cloudinary](https://cloudinary.com/documentation/upload_widget?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)