Create an Ecommerce Admin App with Xata and Cloudinary

nefejames - Nov 23 '22 - - Dev Community

Content is king, and behind every content is the CMS (Content Management System), which is the queen. CMSs revolutionalized how we store, produce, and share content. Today, there are a variety of content management solutions, whether traditional, headless or hybrid.

In this article, we‘ll learn how to create a dummy ecommerce store CMS admin with Next.js, Cloudinary's Upload widget, and Xata.

Source Code

Access the source code for the application on GitHub.

Prerequisites

To follow along with this article, we need to have:

What Is Xata?

Xata is a serverless database built on top of PostgreSQL and Elasticsearch. It comes with useful integrations with popular frameworks and technologies like Next.js, Netlify, Nuxt.js, Vercel, Remix, and SvelteKit.

What is Cloudinary?

Cloudinary is a cloud-based and end-to-end platform with tools and functionalities for storing, processing, editing, managing, delivering, and transforming media assets.

What We Will Create

The screenshots below shows the application we will create.
Users can add new products by filling in the name, price and image of the products. They can also delete products or update the products’ details.

Products page

Product’s detail page

Getting Started

Download, clone, or fork this starter template that contains the code for the UI of the ecommerce admin.

    npx create-next-app xata-cloudinary-chat-app -e https://github.com/nefejames/hackmamba-ecom-admin-app-ui
    or
    yarn create next-app xata-cloudinary-chat-app -e https://github.com/nefejames/hackmamba-ecom-admin-app-ui
Enter fullscreen mode Exit fullscreen mode

Next, navigate to the project directory, install the necessary dependencies, then run the application

The starter template consists of the following files:

  • components/AddProductForm.js: responsible for adding a new product to the shop
  • components/ProductCard.js: displays a product’s details
  • components/ProductModal.js: the modal that holds the form
  • components/UpdateProductForm.js: responsible for updating a product’s details
  • layout/index.js: the layout for the application
  • utils: will hold the Xata instance, and the logic for the product image upload and product deletion

Setting up Xata Database

We start by creating a new database called chat-app on our Xata account.

creating a new Xata database

Next, create a Products table that will contain the data of different products. Each record will have name, price, img, and id properties; the id is automatically generated by Xata.
We got the images from the media filesuploads in our Cloudinary account.

create the Products table

The database schema is ready, so let’s set up the Xata instance next, and integrate it into the application.

Setting up Xata Instance

Run the command below to install the CLI globally.

    npm install @xata.io/cli -g
Enter fullscreen mode Exit fullscreen mode

Next, run xata auth login, which will prompt you to Create a new API key in the browser or use an existing one; we’ll go with the first option.

Xata auth login

Once in the browser, give the API a name and click the Create API key button.

create a new Xata API key

Back on the dashboard, click on the Get Code Snippet button at the top right corner and copy the second command. We will use it to initialize the project locally with the aid of the CLI.

Run the command in the terminal and choose from the configuration options. The screenshot below shows the configurations for this application.

Xata CLI config

The CLI will automatically create a xata.js file that contains the XataClient instance we need; let’s move on to the coding aspects of the application.

Setting up the API Routse

We need to set up these 3 API (Application Programming Interface) routes for the app:

  • api/add-product.js: contains the logic for adding a product to the Products database
  • api/update-product.js: contains the logic for updating the information of a product on the Products database
  • api/delete-prduct.js: contains the logic for deleting a product on the Products database

The code for the add-product.js API route:

    import { getXataClient } from "@utils/xata";
    const xata = getXataClient();

    const handler = async (req, res) => {
      const { name, price, imgUrl } = req.body;
      await xata.db.Products.create({
        name: name,
        price: price,
        img: imgUrl,
      });
      res.end();
    };

    export default handler;
Enter fullscreen mode Exit fullscreen mode

It takes the name, price, and URL of the uploaded image and passes them as arguments to Xata’s create method to add a new product to the Products db.

The code for the update-product.js API route:

    import { getXataClient } from "@utils/xata";
    const xata = getXataClient();

    const handler = async (req, res) => {
      const { id, name, price } = req.body;
      await xata.db.Products.update(id, {
        name: name,
        price: price,
      });
      res.end();
    };

    export default handler;
Enter fullscreen mode Exit fullscreen mode

It takes the name, price, and id of a specific product and passes them as arguments to Xata’s update method to update the product’s information.

The code for the delete-product.js API route:

    import { getXataClient } from "@utils/xata";
    const xata = getXataClient();

    const handler = async (req, res) => {
      const { id } = req.body;
      await xata.db.Products.delete(id);
      res.end();
    };

    export default handler;
Enter fullscreen mode Exit fullscreen mode

It takes the id of a specific product and passes it as a parameter to Xata’s delete method to delete the product from the Products db.

Having set up the API routes, let’s proceed with other parts of the application.

Fetching and Displaying the Products in the Database

We’ll start by fetching and displaying the products that are currently in the database. Update the pages/shop.js file with the following code:

    import { Box, Flex, Heading, VStack } from "@chakra-ui/react";
    import ProductCard from "@components/ProductCard";
    import { getXataClient } from "utils/xata";
    import ProductModal from "@components/ProductModal";
    import AddProductForm from "@components/AddProductForm";

    export default function Shop({ data }) {
      return (
        <Box>
          <Flex justify="space-between" align="">
            <Heading as="h1" mb={5}>
              Products
            </Heading>
            <ProductModal modalTitle="Add product" modalBtnTitle="Add Product">
              <AddProductForm />
            </ProductModal>
          </Flex>
          <VStack spacing="2em">
            {data.map(({ id, img, name, price }) => (
              <ProductCard id={id} img={img} name={name} price={price} key={id} />
            ))}
          </VStack>
        </Box>
      );
    }

    export async function getServerSideProps() {
      const xata = getXataClient();
      const data = await xata.db.Products.getAll();
      return { props: { data } };
    }
Enter fullscreen mode Exit fullscreen mode

Here, we did the following:

  • Imported the getXataClient utility and initialized a new instance
  • Queried the Products table and fetched all records with getServerSideProps
  • Passed the fetched data to the ProductCard component to render

Adding a Product

Update the components/AddProductForm.js file with the code below to set up the product addition functionality.

    import { useState } from "react";
    import { Button, Box, Image, Input, VStack, useToast } from "@chakra-ui/react";
    import ShowImageUploadWidget from "@utils/upload";

    export default function AddProductForm() {
      const [imgUrl, setImgUrl] = useState(null);
      const toast = useToast();

      const handleSubmit = async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        const data = Object.fromEntries(formData);
        const name = data.name;
        const price = data.price;
        fetch("/api/add-product", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ name, price, imgUrl }),
        });
        e.target.reset();
        toast();
      };
      return (
        <form onSubmit={handleSubmit}>
          //insert form fields here
          <Button
            onClick={() => ShowImageUploadWidget(setImgUrl)}>
            Upload Image
          </Button>
        </form>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Here, we did the following:

  • Created a handleSubmit function that takes the form data and passes it to the add-product route, which will handle pushing the form data to the Products db
  • Set up an imgUrl state that will contain the URL of the product’s image users upload to Cloudinary
  • Imported a ShowImageUploadWidget function from the utils directory
  • Passed ShowImageUploadWidget to the Upload Image button’s onClick event handler
  • Used the toast method to display a message upon successful form submission
  • Passed handleSubmit to the form’s onSubmit event handler

Here’s the code for the ShowImageUploadWidget function. Create a utils/upload.js file and update it with the code. The product image upload is handled by Cloudinary’s Upload Widget.

    function ShowImageUploadWidget(setImgUrl) {
      window.cloudinary
        .createUploadWidget(
          {
            cloudName: "your-cloud-name",
            uploadPreset: "ml_default",
          },
          (error, result) => {
            if (!error && result && result.event === "success") {
              setImgUrl(result.info.thumbnail_url);
            }
            if (error) {
              console.log(error);
            }
          }
        )
        .open();
    }

    export default ShowImageUploadWidget;
Enter fullscreen mode Exit fullscreen mode

Let’s breakdown the snippet above:

  • ShowImageUploadWidget calls the createUploadWidget that exists in the cloudinary object
  • createUploadWidget takes in our cloudname as part of its config
  • ShowImageUploadWidget accepts the setImgUrl function as an argument. If an avatar is uploaded successfully, the imgUrl state will be updated with the URL of the avatar a user uploads

We have to load the widget’s script in order for it to work. Next.js provides a [Script](https://nextjs.org/docs/basic-features/script) component that we can use to load third-party scripts in our application.

Update the _app.js file with the following code:

    import { ChakraProvider } from "@chakra-ui/react";
    import Wrapper from "@layout/index";
    import Script from "next/script";

    function MyApp({ Component, pageProps }) {
      return (
        <ChakraProvider>
          <Script
            src="https://upload-widget.cloudinary.com/global/all.js"
            type="text/javascript"
            strategy="beforeInteractive"
          />
          <Wrapper>
            <Component {...pageProps} />
          </Wrapper>
        </ChakraProvider>
      );
    }
    export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Here, we imported the Script component from Next.js and used it to load the widget’s script.

Updating a Product

Update the components/UpdateProductForm.js file with the code below to set up the product update functionality.

    import { useRouter } from "next/router";
    import { Button, Input, VStack, useToast } from "@chakra-ui/react";

    export default function UpdateProductForm({ product }) {
      const toast = useToast();
      const router = useRouter();

      const handleSubmit = async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        const data = Object.fromEntries(formData);
        const name = data.name;
        const price = data.price;
        fetch("/api/update-product", {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ id: product.id, name, price }),
        });
        toast();
        router.push("/shop");
      };

      return (
        <form onSubmit={handleSubmit}>
          //insert form fields here
        </form>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Here, we did the following:

  • Created a handleSubmit function that takes the form data and passes it to the update-productroute, which will update the product’s data in the Products db
  • Used the toast method to display a message upon successful form submission
  • Redirected the user to the /shop route after form submission
  • Passed handleSubmit to the form’s onSubmit event handler

Deleting a Product

Update the pages/product/[id].js file with the code below to set up the product deletion functionality.

    import Head from "next/head";
    import Image from "next/image";
    import { useRouter } from "next/router";
    import { Box, Button, Flex, Heading, HStack, Text } from "@chakra-ui/react";
    import { getXataClient } from "@utils/xata";
    import ProductModal from "@components/ProductModal";
    import UpdateProductForm from "@components/UpdateProductForm";

    const xata = getXataClient();

    export default function Product({ product }) {
      const router = useRouter();

      function deleteProduct(id) {
        fetch("/api/delete-product", {
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ id }),
        });
        router.push("/shop");
      }
      return (
        <Box>
          //insert ui code here 
        </Box>
      );
    }

    export async function getStaticProps({ params }) {
      try {
        const data = await xata.db.Products.filter({
          id: params.id,
        }).getMany();
        return {
          props: { product: data[0] },
        };
      } catch (error) {
        return {
          props: {},
        };
      }
    }

    export async function getStaticPaths() {
      const products = await xata.db.Products.getAll();
      return {
        paths: products.map((product) => ({
          params: { id: product.id },
        })),
        fallback: true,
      };
    }
Enter fullscreen mode Exit fullscreen mode

Here, we did the following:

  • Used getStaticPaths and getStaticProps to dynamically render the paths for each product
  • Created a deleteProduct function that takes in a product’s id and passes it to the delete-product route, which will delete the product from the db
  • Redirected the user to the /shop route after form submission

With that, we have successfully set up the functionality for the application, and it is good to go.

Conclusion

In this article, we learned how to create an ecommerce admin application with Xata and Cloudinary.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .