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:
- cloudinary-react’s Video component to display videos.
- Auth0’s Next.js SDK to handle the user authentication.
- Chakra UI to create the user interface.
We install the required packages in our application using the CLI command:
npm i chakra-ui cloudinary-react auth0/nextjs-auth0
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.
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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.
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>
);
}
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"
/>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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 theProductCard
component. We will use this to update themodalData
state with the data from the current product being clicked. - We pass an
isOpen
boolean, anonClose
method and and themodalData
state to theModalProduct
component. The value ofisOpen
depends on the value ofmodalData
. IfmodalData
is null, thenisOpen
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
]
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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