Create Product Reviews with Images in Next.js

Banner for a MediaJam post

Emadamerho-Atori Nefe

Product reviews are an integral aspect of an e-commerce store's branding and marketing. They help build trust and loyalty among customers and help drive sales.

In this article, we‘ll learn how to create a product review application with Next.js and Cloudinary's Upload widget.

Sandbox

The completed project is on CodeSandbox. Fork it and run the code.

The source code is also available on GitHub.

Prerequisites

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

  • Knowledge of React and Next.js.
  • A Cloudinary account.
  • Understanding of Chakra UI, as the demo's user interface is built with it.

Getting started

Create a Next.js project by running the command below in our terminal.

1npx create-next-app product-review-app

Next, navigate into the project directory.

1cd product-review-app

Then, run the command below to start the application.

1npm run dev

We will use react-hook-form later in the article, so install that also.

1npm i react-hook-form

What we will create

We will start by creating the user interface below:

Clicking the 'Review Product' button will cause the modal containing the Form component to pop up:

To effectively manage changes in the application, we need to set up state management using the Context API. The application will have two contexts: ImageUploadContext and ReviewsContext.

The ImageUploadContext will hold the image the user uploads, whereas the ReviewsContext will contain the user's comment and the uploaded image.

Creating the image upload context

As stated earlier, this context will hold the url of the uploaded image.

ImageUploadContext.js

1import { createContext, useContext, useState } from "react";
2
3 const ImageUploadContext = createContext("");
4 export const useImageUploadContext = () => useContext(ImageUploadContext);
5
6 export default function ImageUploadContextProvider({ children }) {
7 const [uploadedImgUrl, setUploadedImgUrl] = useState(null);
8
9 return (
10 <ImageUploadContext.Provider value={{ uploadedImgUrl, setUploadedImgUrl }}>
11 {children}
12 </ImageUploadContext.Provider>
13 );
14 }

Here, we do the following:

  • Import the required dependencies from React.
  • Create the ImageUploadContext that will hold the image URL.
  • Set up the ImageUploadContextProvider that will wrap the root of the application.
  • Set up the uploadedImgUrl state and pass it as the provider's value along with setUploadedImgUrl.
  • Create and export a custom hook, useImageUploadContext from which we will access the data in the ImageUploadContext.

Creating the review context

Though similar in set up to the ImageUploadContext, ReviewsContext will hold the user's comment and the uploaded image.

ReviewsContext.js

1import { createContext, useContext, useState } from "react";
2
3const ReviewsContext = createContext(null);
4export const useReviewsContext = () => useContext(ReviewsContext);
5
6export default function ReviewsContextProvider({ children }) {
7 const [reviews, setReviews] = useState([
8 { reviewText: "first review", reviewImage: "/product.webp" },
9 ]);
10
11 return (
12 <ReviewsContext.Provider value={{ reviews, setReviews }}>
13 {children}
14 </ReviewsContext.Provider>
15 );
16}

Here, we do the following:

  • Import createContext, useContext, and useState from React.
  • Create the ReviewsContext that will hold the comment and image url.
  • Set up the ReviewsContextProvider that will wrap the root of the application.
  • Set up the reviews state and pass it as the provider's value along with setReviews. The reviews state will hold an array of objects, and the objects will have two properties: reviewText and reviewImage. reviewText is the user's comment, and reviewImage is the uploaded image.
  • Create a custom hook, useReviewsContext, from which we will access the data in the ReviewsContext.

Creating the ProductCard component

The ProductCard component will depict how an e-commerce product card looks in real-world applications.

ProductCard.js

1import { Box, Heading } from "@chakra-ui/react";
2 import Image from "next/image";
3
4 export default function ProductCard() {
5 return (
6 <Box>
7 <Box position="relative" w="full" h="70%">
8 <Image src="/product.webp" alt="an img" />
9 </Box>
10 <Box display="flex" alignItems="center" h="90px" pl={6}>
11 <Heading>My Product</Heading>
12 </Box>
13 </Box>
14 );
15 }

Here, we import Box and Heading from Chakra UI and Image from Next.js and use them to create the card.

Creating the Review component

The Review component will hold the user's review comment and image.

Review.js

1import { Box, Text, Image } from "@chakra-ui/react";
2
3 export default function Review({ review }) {
4 return (
5 <Box p={2}>
6 <Text fontSize="2xl" mr={6}>
7 {review.reviewText}
8 </Text>
9 <Image src={review.reviewImage} alt={review.reviewImage} />
10 </Box>
11 );
12 }

Let's break down the code above:

  • We import Box, Heading, and Image from Chakra UI, and use them to create the comment's interface.
  • The component receives a review prop, which is an object containing the reviewText and reviewImage we set up earlier in the ReviewsContext.
  • We import Box, Heading, and Image from Chakra UI and use them to create the comment's interface.
  • The component receives a review prop, an object containing the reviewText and reviewImage we set up earlier in the ReviewsContext.

Creating the ReviewsContainer component

The ReviewsContainer will contain the different reviews for a product.

ReviewsContainer.js

1import { Box, VStack } from "@chakra-ui/react";
2 import Review from "@components/Review";
3
4 import { useReviewsContext } from "context/ReviewsContext";
5
6 export default function ReviewsContainer() {
7 const { reviews } = useReviewsContext();
8
9 return (
10 <Box mt={10} rounded="md" border="1px" borderColor="gray.200" p={3}>
11 <VStack spacing={5} align="flex-start">
12 {reviews.map((review) => (
13 <Review review={review} key={review.reviewText} />
14 ))}
15 </VStack>
16 </Box>
17 );
18 }

Let's break down the code above:

  • We import the useReviewsContext hook from ReviewsContext and access the reviews.
  • reviews is an array of objects, so we map through it and pass each review to the Review component we set up earlier.

Creating the FormModal component

The FormModal contains the Form component.

FormModal.js

1import {
2 Modal,
3 ModalOverlay,
4 ModalContent,
5 ModalHeader,
6 ModalBody,
7 ModalCloseButton,
8 useDisclosure,
9 Button,
10 Box,
11 } from "@chakra-ui/react";
12 import Form from "./Form";
13
14 export default function FormModal() {
15 const { isOpen, onOpen, onClose } = useDisclosure();
16
17 return (
18 <Box mt={4}>
19 <Button onClick={onOpen}>Review Product</Button>
20 <Modal isOpen={isOpen} onClose={onClose}>
21 <ModalOverlay />
22 <ModalContent>
23 <ModalHeader>Review Product</ModalHeader>
24 <ModalCloseButton />
25 <ModalBody>
26 <Form closeModal={onClose} />
27 </ModalBody>
28 </ModalContent>
29 </Modal>
30 </Box>
31 );
32 }

Here, we set up the modal and set the Form component as the modal's content. We also pass the onClose method to Form. We will need onClose to close the modal after submitting the form.

Creating the Form component

The Form component will hold the input field where the user can enter their review, a button that will trigger Cloudinary's Upload Widget, and another button to submit the form.

Form.js

1import { useForm } from "react-hook-form";
2 import { FormControl, Input, Stack, Text, Button } from "@chakra-ui/react";
3
4 export default function Form({ closeModal }) {
5 const { handleSubmit, register } = useForm();
6
7 function onSubmit(value) {
8 console.log(value);
9 //do something with form data then close the modal
10 closeModal();
11 }
12
13 return (
14 <Stack spacing={4}>
15 <Text>Please leave a review</Text>
16 <form onSubmit={handleSubmit(onSubmit)}>
17 <FormControl>
18 <Input type="text" {...register("reviewText")} />
19 <Button>upload image</Button>
20 </FormControl>
21 <Button colorScheme="blue" mt={3} type="submit">
22 Submit
23 </Button>
24 </form>
25 </Stack>
26 );
27 }

Here, we do the following:

  • Initialize react-hook-form and import its useForm hook. We will use react-hook-form to track the input field's value and handle the form submission.
  • Import handleSubmit and register from useForm.
  • Register the input field with react-hook-form.
  • Create an onSubmit function where we define how to process the form data; after submitting the form, we call the closeModal method.

Bringing it all together

Having created the required components, let's bring them into the index.js file.

1import { Heading } from "@chakra-ui/react";
2 import ReviewsContainer from "@components/ReviewsContainer";
3 import ProductCard from "@components/ProductCard";
4 import FormModal from "@components/FormModal";
5
6 export default function Home() {
7 return (
8 <div>
9 <Heading as="h1" mb={12}>
10 A Cool Ecommerce Product Review App
11 </Heading>
12 <ProductCard />
13 <FormModal />
14 <ReviewsContainer />
15 </div>
16 );
17 }

Integrating Cloudinary's Upload widget

We've created the app's interface, so now let's integrate Cloudinary's Upload widget.

Next.js provides a Script component that we can use to load third-party scripts in our application. We need the Script component to load the upload widget's script.

_app.js

1import { ChakraProvider } from "@chakra-ui/react";
2 import LayoutWrapper from "@layout/index";
3 import Script from "next/script";
4 import ReviewsContextProvider from "@context/ReviewsContext";
5 import ImageUploadContextProvider from "@context/ImageUploadContext";
6
7 function MyApp({ Component, pageProps }) {
8 return (
9 <ReviewsContextProvider>
10 <ImageUploadContextProvider>
11 <ChakraProvider>
12 <Script
13 src="https://upload-widget.cloudinary.com/global/all.js"
14 type="text/javascript"
15 strategy="beforeInteractive"
16 />
17 <LayoutWrapper>
18 <Component {...pageProps} />
19 </LayoutWrapper>
20 </ChakraProvider>
21 </ImageUploadContextProvider>
22 </ReviewsContextProvider>
23 );
24 }
25 export default MyApp;

Here, we:

  • Import Script into the _app.js file and load the widget's script.
  • Wrap our application with the ReviewsContextProvider and ImageUploadContextProvider.

Initializing the widget

Having integrated the widget, let's initialize it back in the Form component.

Form.js

1import { FormControl, Input, Stack, Text, Button } from "@chakra-ui/react";
2 import { useForm } from "react-hook-form";
3 import { useReviewsContext } from "context/ReviewsContext";
4 import { useImageUploadContext } from "@context/ImageUploadContext";
5
6 export default function Form({ closeModal }) {
7 const { reviews, setReviews } = useReviewsContext();
8 const { uploadedImgUrl, setUploadedImgUrl } = useImageUploadContext();
9
10 //widget initializer
11 function showWidget() {
12 window.cloudinary
13 .createUploadWidget(
14 {
15 cloudName: "OUR-ACCOUNT-CLOUD-NAME",
16 uploadPreset: "ml_default",
17 },
18 (error, result) => {
19 if (!error && result && result.event === "success") {
20 setUploadedImgUrl(result.info.thumbnail_url);
21 }
22 if (error) {
23 console.log(error);
24 }
25 }
26 )
27 .open();
28 }
29 const { handleSubmit, register } = useForm();
30
31 //form submission handler
32 function onSubmit(value) {
33 setReviews([
34 ...reviews,
35 { reviewText: value.reviewText, reviewImage: uploadedImgUrl },
36 ]);
37 closeModal();
38 }
39
40 return (
41 <Stack spacing={4}>
42 <Text>Please leave a review</Text>
43 <form onSubmit={handleSubmit(onSubmit)}>
44 <FormControl>
45 <Input type="text" {...register("reviewText")} />
46 <Button onClick={showWidget}>upload image</Button>
47 </FormControl>
48 <Button colorScheme="blue" mt={3} type="submit">
49 Submit
50 </Button>
51 </form>
52 </Stack>
53 );
54 }

Let's break down the code above:

  • We import ReviewsContext and ImageUploadContext. We access reviews and setReviews from ReviewsContext, and uploadedImgUrl and setUploadedImgUrl from ImageUploadContext.
  • We create a showWidget function that initializes the widget. We pass showWidget to the image button's onClick handler.
  • We update the onSubmit function. Instead of logging the form data to the console, we pass that data to the reviews state. Upon form submission, we add a new object to the state. We get the reviewText from the input field and the reviewImage from the uploadedImgUrl state. After that, we close the modal.

With this, we have successfully created a product review application.

Conclusion

This article taught us to create a product review application with Next.js and Cloudinary's Upload widget.

Resources

Emadamerho-Atori Nefe

Frontend Developer and Technical Writer