Build an Ecommerce Store with Next.js, Auth0 and Cloudinary

nefejames - Oct 18 '21 - - Dev Community

Multimedia content optimization is the process of delivering high-quality media files in the right dimension, size, and format without sacrificing quality.

When poorly optimized, the images and videos in your websites can lead to a poor user experience, loss of traffic, and low SEO rankings.

In this article, we will learn how Cloudinary enables us to serve fast-loading images and videos through their CDN. We will also learn to set up user authentication with Auth0 and create responsive components with Chakra UI. We will combine these technologies to develop an eCommerce application.

We completed the project demo in a codesandbox.

You can find the deployed version here and the codebase in this Github repo.

Prerequisites/Installation

Having some knowledge of and experience with, React.js and Chakra UI are required to follow the steps in this article. We will use the images and videos hosted on Cloudinary so you will need a Cloudinary account. Auth0 will handle the user authentication through their universal login service, so you must create an account.

As I mentioned earlier, we will be using Next.js in this project, so we need to scaffold a new Next.js application.

We will also use the following packages in the application:

We install the required packages in our application using the CLI command:

npm i chakra-ui cloudinary-react auth0/nextjs-auth0
Enter fullscreen mode Exit fullscreen mode

Creating the Homepage

The homepage consists of a header and a hero section; the header consists of the logo and a login/sign-in button, and the hero consists of the website copy on the left and an image on the right.

We can see the appearance of the homepage in the image below.

PetShop Homepage

Let’s begin by creating the layout for the homepage.

    import { Box, Container } from "@chakra-ui/react";
    import HomeHeader from "@components/Headers/Home";

    export default function HomeLayout({ children }) {
      return (
        <Box>
          <Container maxW={"7xl"}>
            <HomeHeader />

            {children}
          </Container>
        </Box>
      );
    }
Enter fullscreen mode Exit fullscreen mode

For the homepage layout, we create a component, HomeLayout. HomeLayout consists of a HomeHeader component and the children we will pass into it later. HomeLayout is where the hero section will sit.

Creating the Homepage Header
Next, we create the homepage header.

    import NextLink from "next/link";
    import { Box, Flex, Button } from "@chakra-ui/react";
    import Logo from "@svgs/Logo";

    export default function HomeHeader() {
      return (
        <Box>
          <Flex>
            <NextLink href="/" rel="noopener noreferrer" target="_blank">
              <a>
                <Logo />
              </a>
            </NextLink>
          </Flex>      
        </Box>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The HomeHeader component is made up of the Logo. We will add a login button later in the article.

Creating the Hero Section
Now, we move on to the hero section.

    import { Stack, Flex, Box, Heading, Text, Image } from "@chakra-ui/react";
    import HomeLayout from "@layout/Home";

    export default function Home() {
      return (
        <HomeLayout>
          <Stack>
            <Stack flex={1} spacing={{ base: 5, md: 10 }}>
              <Heading lineHeight={1.1} fontWeight={600} fontSize={["3xl", "4xl", "6xl"]}
              >
                Get your perfect companion{" "}
                <Text as="span" color="blue.500">
                  today!
                </Text>
              </Heading>
              <Text color="gray.200" fontSize={["lg"]}>
                Buyng a pet is a big decision, and we are here to help you through
                the process. It's time to add a new love to your family. Discover
                the right pet for you. It's time to find your new best friend.
              </Text>
            </Stack>
            <Flex>
              <Box>
                <Image alt="Hero Image" fit="cover" align="center" w="100%" h="100%"
                  src=" https://res.cloudinary.com/nefejames/image/upload/q_auto,f_auto,w_auto/v1632501665/Hackmamba/Images/hero.jpg"
                />
              </Box>
            </Flex>
          </Stack>
        </HomeLayout>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The hero image is gotten from our Cloudinary media repository. Let’s breakdown the URL of the image:

  • q_auto: produces a high quality image with a minimal the file size.
  • f_auto: delivers the image in the best format supported by the browser.
  • w_auto: serves images proportional to users’ device resolution and screen sizes.

Creating the Dashboard

The dashboard consists of a header, sidebar, and dashboard view that contains the products.
The header consists of the logo and a button to toggle the sidebar.
The sidebar consists of a link to illustrate how a dashboard looks in a full-scale application.

Let’s start by creating the layout of the dashboard.

    import { useMediaQuery, Box, Stack } from "@chakra-ui/react";
    import Header from "@components/Headers/Dashboard/";
    import Page from "./Page";
    import { DesktopSidebar, MobileSidebar } from "@components/DashboardSidebar";

    export default function DashboardLayout({ children }) {
      const [isSmallScreen] = useMediaQuery("(max-width: 768px)");

      return (
        <Box w="full">
          <Header />
          <Box pos="relative" h="max-content">
            <Stack direction="row" spacing={{ md: 5 }}>
              <DesktopSidebar />
              {isSmallScreen && <MobileSidebar />}
              <Page>{children}</Page>
            </Stack>
          </Box>
        </Box>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The dashboard layout consists of two dashboards, a mobile dashboard, and another for desktops.
We create a DashboardLayout component and use Chakra’s [useMediaQuery](https://chakra-ui.com/docs/hooks/use-media-query) hook to set up a isSmallScreen boolean. We show the mobile sidebar when isSmallScreen is true - when the max-width is 768px.

The Dashboard Context and Interactivity Flow
Before we continue building the dashboard, we need to understand its flow. We want to add some interactivity to the dashboard to boost the user experience.

We want to be able to toggle the width of the desktop sidebar. We also want the mobile sidebar to slide in and out when toggled.

A core aspect of this functionality is a sidebar context, which we will create. The value of the sidebar context is gotten from Chakra UI’s [useDisclosure](https://chakra-ui.com/docs/hooks/use-disclosure) hook. useDisclosure is a custom hook used to help handle common open, close, or toggle scenarios.

When setting up the sidebar, we will create and export a useSidebarAuth hook that consumes the sidebar context and makes its values accessible throughout the application.

We will pass the useSidebarAuth hook to a SidebarToggleButton component which we will create later. SidebarToggleButton will enable us to toggle the sidebar context’s isOpen value when clicked. We will pass SidebarToggleButton to the dashboard header later.
Whenever a user clicks the SidebarToggleButton, we alter the behavior of the desktop or mobile sidebars based on the current value of isOpen.

The flow chart image below shows us how the sidebar context works with the dashboard components and how we will set up the interactivity.

dashboard context and interactivity flow

Creating the Sidebar Context

Now that we understand the sidebar context and how it is used in the dashboard, let us set up the context.

    import { createContext, useContext } from "react";
    import { useDisclosure } from "@chakra-ui/react";

    const SidebarContext = createContext(null);
    export const useSidebarAuth = () => useContext(SidebarContext);

    export default function SidebarContextProvider({ children }) {
      const sidebarState = useDisclosure();

      return (
        <SidebarContext.Provider value={sidebarState}>
          {children}
        </SidebarContext.Provider>
      );
    }
Enter fullscreen mode Exit fullscreen mode

As stated earlier, we use the useDisclosure hook to set up the context and export a useSidebarAuth hook that consumes the context.

Creating the Sidebar Toggle Button
The next thing we need to do is create the SidebarToggleButton component.

    import { Icon, IconButton } from "@chakra-ui/react";
    import { CgClose, CgMenu } from "react-icons/cg";
    import { useSidebarAuth } from "@context/sidebarContext";

    export default function SidebarToggleButton() {
      const { onToggle, isOpen } = useSidebarAuth();
      const icon = isOpen ? CgClose : CgMenu;
      return (
        <IconButton
          size="sm"
          fontSize="lg"
          variant="ghost"
          onClick={onToggle}
          icon={<Icon as={icon} />}
          aria-label="Toggle Actions"
          transition="all .4s ease-in-out"
        />
      );
    }
Enter fullscreen mode Exit fullscreen mode

As explained earlier, when SidebarToggleButton is clicked, the isOpen value of the context is toggled.

Creating the Dashboard Header
Next we set up the dashboard header.

    import NextLink from "next/link";
    import { Flex, Spacer, Stack, Button, Link } from "@chakra-ui/react";
    import SidebarToggleButton from "./components/SidebarToggleButton";
    import Logo from "@svgs/Logo";

    export default function DashboardHeader() {
      return (
        <Flex
          h="10vh"
          minH="70px"
          pos="sticky"
          top="0"
          zIndex="2"
        >
          <Stack direction="row" w="full" alignItems="center" spacing="8">
            <NextLink href="/" rel="noopener noreferrer" target="_blank">
              <a>
                <Logo />
              </a>
            </NextLink>
            <SidebarToggleButton />

          </Stack>
        </Flex>
      );
    }
Enter fullscreen mode Exit fullscreen mode

As I explained with the flow chart, we add the SidebarToggleButton component to the dashboard header.

Creating the Desktop Sidebar
The dashboard has two sidebars, one for mobile devices, and another for desktop devices. Let’s start with the desktop sidebar.

    import { useRouter } from "next/router";
    import { useSidebarAuth } from "@context/sidebarContext";

    export default function Sidebar() {
      const router = useRouter();

      return (
        <Stack
          minH="full"
          h="90vh"
          pos="sticky"
          top="10vh"
          display={["none", , "initial"]}
          transition="width .4s ease-in-out"
        >
          <NavLink
            active={router.pathname === "/"}
            name="Home"
            href="/"
            icon={IoHomeSharp}
          />
        </Stack>
      );
    }
Enter fullscreen mode Exit fullscreen mode

We want the desktop sidebar to be hidden on mobile, so we set the display to none.

Creating the Mobile Sidebar
We make use of Chakra’s [Drawer](https://chakra-ui.com/docs/overlay/drawe) component to set up the mobile sidebar.

    import { useEffect } from "react";
    import { useRouter } from "next/router";
    import {
      Drawer,
      DrawerCloseButton,
      DrawerContent,
      DrawerOverlay,
    } from "@chakra-ui/react";
    import { useSidebarAuth } from "@context/sidebarContext";
    import FullNavLink from "./FullNavLink";

    export default function MobileSidebar() {
      const router = useRouter();
      const { isOpen, onClose } = useSidebarAuth();

      useEffect(() => {
        router.events.on("routeChangeComplete", onClose);
        return () => {
          router.events.off("routeChangeComplete", onClose);
        };
      }, []);
      return (
        <Drawer isOpen={isOpen} onClose={onClose} placement="left">
          <DrawerOverlay display={["initial", , "none"]}>
            <DrawerContent layerStyle="neutral" py={12} bg="gray.900">
              <Stack spacing={2} fontSize="sm">
                <DrawerCloseButton />
                <FullNavLink
                  active={router.pathname === "/"}
                  name="Home"
                  href="/"
                  icon={IoHomeSharp}
                />
              </Stack>
            </DrawerContent>
          </DrawerOverlay>
        </Drawer>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Next.js provides a router event, routeChangeComplete, which we use to toggle the mobile sidebar when a link is clicked.

The Dashboard View
Before we proceed, we need to break down the dashboard view. The dashboard view consists of the DashboardLayout component we set up earlier, a grid of products in a ProductCard component, and a ModalProduct component.

Let’s set up the dashboard view.

    import { useState } from "react";
    import { Box, SimpleGrid } from "@chakra-ui/react";
    import { motion } from "framer-motion";
    import ProductCard from "@components/ProductCard";
    import DashboardLayout from "@layout/Dashboard";
    import ModalProduct from "@components/ModalProduct";
    import data from "@root/data";
    const MotionSimpleGrid = motion(SimpleGrid);
    const MotionBox = motion(Box);

    export default function Dashboard() {
      const [modalData, setModalData] = useState(null);
      return (
        <DashboardLayout>
          <Box>
            <MotionSimpleGrid>
              {data.map((product) => (
                <MotionBox variants={cardVariant} key={product.id}>
                  <ProductCard product={product} setModalData={setModalData} />
                </MotionBox>
              ))}
            </MotionSimpleGrid>
            <ModalProduct
              isOpen={modalData ? true : false}
              onClose={() => setModalData(null)}
              modalData={modalData}
            />
          </Box>
        </DashboardLayout>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Let’s breakdown the snippet above:

  • We define a modalData state where the data of the clicked product will be stored.
  • We pass setModalData and the product data to the ProductCard component. We will use this to update the modalData state with the data from the current product being clicked.
  • We pass an isOpen boolean, an onClose method and and the modalData state to the ModalProduct component. The value of isOpen depends on the value of modalData. If modalData is null, then isOpen is false.

When ProductCard is clicked, it triggers ModalProduct and causes ModalProduct to pop up.

Here’s a sample of the data we map through and pass to ProductCard.

    const data = [
      {
        title: "First Pet",
        price: 250,
        img: "https://res.cloudinary.com/nefejames/image/upload/q_auto,f_auto,w_auto/Hackmamba/Images/pet1.jpg",
        video: "/Hackmamba/Videos/pet1",
      },
      {
        title: "Second Pet",
        price: 250,
        img: "https://res.cloudinary.com/nefejames/image/upload/q_auto,f_auto,w_auto/Hackmamba/Images/pet2.jpg",
        video: "/Hackmamba/Videos/pet2",
      },
      // other data objects below
    ]

Enter fullscreen mode Exit fullscreen mode

Creating the **ProductCard** and **ModalProduct** Component
Now that we understand how ProductCard and ModalProduct work, let’s create them.

Let’s start with setting up ProductCard.

    import Image from "next/image";
    import { Box, Flex } from "@chakra-ui/react";
    import { StarIcon } from "@chakra-ui/icons";

    export default function ProductCard({ product, setModalData }) {
      const { img, beds, baths, title, price } = product;
      const score = Math.floor(Math.random(5) * 5);
      const reviewCount = Math.floor(Math.random(50) * 50);

      return (
        <Flex>
          <Box onClick={() => setModalData(product)}>
            <Box>
              <Image
                src={img}
                objectFit="cover"
                alt="picture of an animal"
                layout="fill"
                objectFit="cover"
              />
            </Box>

            <Box p="6">
              <Box> {title} </Box>
              <Box>${price}</Box>

              <Box d="flex" mt="3" alignItems="center">
                {Array(5)
                  .fill("")
                  .map((_, i) => (
                    <StarIcon key={i} color={i < score ? "teal.500" : "gray.300"} />
                  ))}
                <Box as="span" ml="2" color="gray.600" fontSize="sm">
                  {reviewCount} reviews
                </Box>
              </Box>
            </Box>
          </Box>
        </Flex>
      );
    }
Enter fullscreen mode Exit fullscreen mode

As stated earlier, we pass setModalData to ProductCard. When ProductCard is clicked, the modalData state is updated with the data of the product card that was clicked.

Next, let’s set up ModalProduct. To do that we use Chakra UI’s Modal component.

    import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody,
      ModalCloseButton, Button, useToast } from "@chakra-ui/react";
    import { Video } from "cloudinary-react";

    export default function ModalProduct({ isOpen, onClose, modalData }) {
      const { title, price, video } = modalData || {};
      const toast = useToast();

      const handleModalClose = () => {
        toast({
          title: "Purchase successsful.",
          description: "One more happy pet.",
          status: "success",
          duration: 3000,
          isClosable: true,
        });
        setTimeout(() => {
          onClose();
        }, 1000);
      };

      return (
        <Modal isOpen={isOpen} onClose={onClose} size="xl">
          <ModalOverlay />
          <ModalContent>
            <ModalCloseButton />
            <ModalHeader>Pet Details</ModalHeader>
            <ModalBody>
              <Box w="full" h="full">
                <Video controls publicId={video} width={300} crop="scale" />

                <Box pt="3">
                  <Box> {title} </Box>
                  ${price}
                </Box>
              </Box>
            </ModalBody>
            <ModalFooter>
              <Button onClick={handleModalClose}>
                Purchase
              </Button>
            </ModalFooter>
          </ModalContent>
        </Modal>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Earlier, we passed isOpen, onClose, and modalData to ModalProduct from the dashboard view. In the snippet above, we access them through props. We use isOpen and onClose to set up the modal and pass the data from modalData to the appropriate parts of ModalProduct.

We also define a function, handleModalClose, and pass it to the modal’s purchase button. The handleModalClose function displays a success toast and closes the modal when it is called.

User Authentication

As stated earlier, we will use Auth0 to handle the user authentication in this application. Auth0 has provided a guide on how to work with their Next.js SDK.
The useUser hook exports two objects, user and error, and an isLoading boolean.

  • user contains information about the authenticated user.
  • We use isLoading to check if the SDK has completed loading.
  • We use the error object to check if there were any authentication errors.

We will use the logged-in user profile in the following parts of our application:

  • the index.js page
  • the homepage header
  • the dashboard header

The Index.js Page
When the user logs in from the homepage, we want to redirect them to the dashboard route. We set up a useEffect and check if user exists. If it does, we redirect the user to the app route.

    import { useEffect } from "react";
    import HomePage from "@views/Home";
    import { useUser } from "@auth0/nextjs-auth0";
    import { useRouter } from "next/router";

    export default function Home() {
      const { user } = useUser();
      const router = useRouter();

      useEffect(() => {
        if (user) {
          router.push("/app");
        }
      }, [user, router]);

      return (
        <div>
          <Head>
            <title>Hackmamba PetShop</title>
          </Head>

          <HomePage />
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The Homepage Header
Here, we conditionally show the logout and login buttons. We show the login button if there is no user and the logout button if there is a logged-in user.

    import NextLink from "next/link";
    import { Box, Flex, Button } from "@chakra-ui/react";
    import Logo from "@svgs/Logo";
    import { useUser } from "@auth0/nextjs-auth0";

    export default function HomeHeader() {
      const { user, isLoading } = useUser();

      return (
        <Box>
          <Flex>
            <NextLink href="/" rel="noopener noreferrer" target="_blank">
              <a>
                <Logo />
              </a>
            </NextLink>

            {!isLoading && !user && (
              <NextLink href="/api/auth/login" passHref>
                <Button as="a" bg="blue.700" _hover={{ bg: "blue.900" }}>
                  Sign In/Log in
                </Button>
              </NextLink>
            )}

            {user && (
              <NextLink href="/api/auth/logout" passHref>
                <Button as="a" bg="blue.700" _hover={{ bg: "blue.900" }}>
                  Logout
                </Button>
              </NextLink>
            )}
          </Flex>
        </Box>
      );
    }

Enter fullscreen mode Exit fullscreen mode

The Dashboard Header
Here, we show the logout button when a user is logged in.

    import NextLink from "next/link";
    import { Flex, Spacer, Stack, Button, Link } from "@chakra-ui/react";
    import SidebarToggleButton from "./components/SidebarToggleButton";
    import { useUser } from "@auth0/nextjs-auth0";
    import Logo from "@svgs/Logo";

    export default function DashboardHeader() {
      const { user } = useUser();

      return (
        <Flex>
          <Stack direction="row" w="full" alignItems="center" spacing="8">
            <NextLink href="/" rel="noopener noreferrer" target="_blank">
              <a>
                <Logo />
              </a>
            </NextLink>
            <SidebarToggleButton />

            <Spacer />

            {user && (
              <Link href="/api/auth/logout">
                <Button bg="blue.700" _hover={{ bg: "blue.900" }}>Logout</Button>
              </Link>
            )}
          </Stack>
        </Flex>
      );
    }

Enter fullscreen mode Exit fullscreen mode

Conclusion

We learned how to use Auth0, Next.js, Chakra UI, and Cloudinary to create an eCommerce application. We also learned how to add interactivity to our applications and make them responsive with Chakra UI’s useDisclosure and useMediaQuery hooks.

Further Reading

You can learn more about the technologies used from the links below.

Cloudinary
React video transformations
Image optimization
cloudinary-react

Auth0
How to authenticate Next.js apps with Auth0
The ultimate guide to Next.js authentication with Auth0

Next.js
Getting started with Next.js

Content created for the Hackmamba Jamstack Content Hackathon

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