Interactive Product Gallery with Cloudinary

Ifeoma Imoh

They say a picture speaks a thousand words. What if you needed the picture to speak more words than that? More words are certainly required to convince consumers to part with their hard-earned money, right? Adding interactivity to the images provides a more profound experience for the customer, making the product image almost tangible. In this tutorial, I will show you how to implement an in-house interactive media gallery with minimal fuss using Cloudinary.

In this tutorial, we will build the gallery management feature for an e-commerce web application. Using the application, you can upload images and videos of a product. Upon successful upload, you can view the items rendered using the Cloudinary product gallery widget.

Here is a link to the demo CodeSandbox.

Project Setup

Create a new React app using the following command:

1npx create-react-app cloudinary_product_gallery

Next, add the project dependencies using the following command:

1npm install antd @ant-design/icons axios react-router-dom

For UI components in our application, we will use antd while Axios will be used for uploading media files to our Cloudinary store. React router will be used to transition between pages.

Next, we need to import the antd CSS. To do this, open the src/App.css file and edit its content to match the following:

1@import "~antd/dist/antd.css";

Set up Cloudinary

To use Cloudinary's provisioned services, you need to first sign up for a free Cloudinary account if you don't have one already. Important details are displayed on your account's Management Console (aka Dashboard): your cloud name, API key, etc.

For this tutorial, we will be sending images to Cloudinary via unsigned POST requests. To do this, we need our account cloud name and an unsigned upload preset. To create one, select Settings > Upload in the Management Console and then scroll to the Upload presets section. Create a new upload preset by clicking Add upload preset at the bottom of the upload preset list. In the displayed form, ensure the Signing Mode is set to Unsigned, as shown below.

Click Save to complete the upload preset definition, then copy the preset name displayed on the Upload Settings page.

Next, let’s create environment variables to hold the details of our Cloudinary account. Create a new file called .env at the root of your project and add the following to it:

1REACT_APP_CLOUD_NAME = "INSERT YOUR CLOUD NAME HERE";
2REACT_APP_UPLOAD_PRESET = "INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE";

This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command:

1cp .env .env.local

By default, this local file resides in the .gitignore folder, mitigating the security risk of inadvertently exposing secret credentials to the public. You can update the .env.local file with your Cloudinary credentials.

In the src directory of the project, create a new folder named cloudinary. This folder will hold the Cloudinary-related helper classes we will need in our components. In the cloudinary folder, create a new file called cloudinaryConfig.js. This file will give access to the environment variables and prevent repeated process.env. calls throughout the project. Add the following to the cloudinaryConfig.js file:

1export const cloudName = process.env.REACT_APP_CLOUD_NAME;
2export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
3export const defaultUploadTag = "cloudinary_interactive_gallery";

The Cloudinary gallery widget relies on the Client-side asset lists feature to retrieve the list of images (or videos) with the specified tag. To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted. To enable it, open the Security settings in your Management console and clear the Resource list item under Restricted media types.

With these in place, we can write the helper function to upload media files to Cloudinary. In the cloudinary folder, create a new file named cloudinaryHelper.js and add the following to it:

1import axios from "axios";
2import { cloudName, defaultUploadTag, uploadPreset } from "./cloudinaryConfig";
3
4export const upload = ({ file, fileType, successCallback }) => {
5 const url = `https://api.cloudinary.com/v1_1/${cloudName}/${fileType}/upload`;
6 const data = new FormData();
7 data.append("file", file);
8 data.append("upload_preset", uploadPreset);
9 data.append("tags", defaultUploadTag);
10 axios
11 .post(url, data, {
12 headers: {
13 "Content-Type": "multipart/form-data",
14 },
15 })
16 .then((response) => successCallback(response.data));
17};

The upload function is used to upload files to Cloudinary using the previously defined upload preset and upload tag. The function has one argument, which is an object containing the file to be uploaded, the file type (for this tutorial, it could be either image or video), and a function to be called upon receipt of a successful response from the Cloudinary API.

Add Gallery Context

The functionality for the gallery widget is delivered via CDN and instantiated in the index.js file. However, we need access to the instantiated object elsewhere in our application to update the widget on successful upload. To avoid prop drilling, we’ll use React Context to pass this object to any component where it may be required. In the src folder, create a new folder named context. In the src/context folder, create a new file named GalleryContext.js and add the following to it:

1import { createContext } from "react";
2
3const GalleryContext = createContext(null);
4export default GalleryContext;

With our context in place, we can instantiate the widget and update the context value.

Instantiate Gallery Widget

Open the public/index.html file and add the following <script> tag in the head of the HTML file:

1<script
2 src="https://product-gallery.cloudinary.com/all.js"
3 type="text/javascript"
4>
5 {" "}
6</script>

Next, update the src/index.js file to match the following:

1import React from "react";
2import ReactDOM from "react-dom/client";
3import "./index.css";
4import App from "./App";
5import { defaultUploadTag, cloudName } from "./cloudinary/cloudinaryConfig";
6import GalleryContext from "./context/GalleryContext";
7import { BrowserRouter } from "react-router-dom";
8
9/* eslint-disable */
10const myGallery = cloudinary.galleryWidget({
11 container: "#gallery",
12 cloudName: cloudName,
13 mediaAssets: [
14 { tag: defaultUploadTag }, // by default mediaType: "image"
15 { tag: defaultUploadTag, mediaType: "video" },
16 ],
17});
18
19const root = ReactDOM.createRoot(document.getElementById("root"));
20root.render(
21 <React.StrictMode>
22 <BrowserRouter>
23 <GalleryContext.Provider value={myGallery}>
24 <App />
25 </GalleryContext.Provider>
26 </BrowserRouter>
27 </React.StrictMode>
28);

Here we initialize the product gallery widget with the cloudinary.galleryWidget method call. We pass the initialization options, including the cloud name and the tags of the media assets we wish to retrieve. We also specify a key named container. This is the id of the div element in which the galley will be rendered. The instantiated object is then passed to the GalleryContext provider via the value prop.

We used the eslint-disable annotation to avoid eslint throwing an undefined variable exception.

Add Components

With the gallery widget instantiated and the object made available via Context, let’s create two components - one for uploading files and the other for rendering the gallery.

In the src folder, create a new folder named components. Next, in the src/components folder, create a new file called MediaUpload.js and add the following to it:

1import { useState } from "react";
2import { Button, Card, message, Upload } from "antd";
3import { UploadOutlined } from "@ant-design/icons";
4import { useNavigate } from "react-router-dom";
5import { upload } from "../cloudinary/cloudinaryHelper";
6import { defaultUploadTag } from "../cloudinary/cloudinaryConfig";
7import { useContext } from "react";
8import GalleryContext from "../context/GalleryContext";
9
10const MediaUpload = () => {
11 const [isUploading, setIsUploading] = useState(false);
12 const [selectedFiles, setSelectedFiles] = useState([]);
13 const navigate = useNavigate();
14 const myGallery = useContext(GalleryContext);
15
16 const uploadSelection = () => {
17 if (selectedFiles.length === 0) {
18 message.error("You need to upload a media file first");
19 } else {
20 setIsUploading(true);
21 selectedFiles.forEach((file, index) => {
22 const fileType = file["type"].split("/")[0];
23 upload({
24 file,
25 fileType,
26 successCallback: () => {
27 if (index === selectedFiles.length - 1) {
28 message.success("Images uploaded successfully");
29 myGallery.update({
30 mediaAssets: [
31 { tag: defaultUploadTag },
32 { tag: defaultUploadTag, mediaType: "video" },
33 ],
34 });
35 setIsUploading(false);
36 navigate("/gallery");
37 }
38 },
39 });
40 });
41 }
42 };
43
44 const props = {
45 multiple: true,
46 onRemove: (file) => {
47 setSelectedFiles((currentSelection) => {
48 const newSelection = currentSelection.slice();
49 const fileIndex = currentSelection.indexOf(file);
50 newSelection.splice(fileIndex, 1);
51 return newSelection;
52 });
53 },
54 beforeUpload: (file) => {
55 setSelectedFiles((currentSelection) => [...currentSelection, file]);
56 return false;
57 },
58 showUploadList: true,
59 };
60
61 return (
62 <Card
63 style={{ margin: "auto", width: "50%" }}
64 actions={[
65 <Button type="primary" loading={isUploading} onClick={uploadSelection}>
66 Submit
67 </Button>,
68 ]}
69 >
70 <Upload.Dragger {...props}>
71 <p className="ant-upload-drag-icon">
72 <UploadOutlined />
73 </p>
74 <p className="ant-upload-text">Click to Upload Files</p>
75 </Upload.Dragger>
76 </Card>
77 );
78};
79
80export default MediaUpload;

This component renders a simple drag and drop form using the antd Upload component. A submit button is also rendered, which calls the uploadSelection function when clicked.

If at least one file is selected, the function iterates through the selectedFiles state variable (updated when files are selected or removed via the Upload component). For each file, it calls the upload function we declared earlier.

Once the files have been uploaded, a success message is displayed, and the gallery widget (retrieved via the useContext call) is used to update the widget. Finally, the application redirects to the Gallery component, which we will build next.

In the src/components folder, create a new file named Gallery.js and add the following to it:

1import { useEffect, useContext } from "react";
2import GalleryContext from "../context/GalleryContext";
3
4const Gallery = () => {
5 const gallery = useContext(GalleryContext);
6 useEffect(() => {
7 gallery.render();
8 }, []);
9 return (
10 <>
11 <h1>Gallery</h1>
12 <div id="gallery"></div>
13 </>
14 );
15};
16
17export default Gallery;

This component retrieves the gallery widget from GalleryContext and declares the target container - an empty div with id gallery. An useEffect call is used to render the gallery on page load.

Add Navigation

We have our components, but we need to implement the routing between them. We also need a component to help the user trigger the navigation process where necessary.

We’ll start by adding a menu. In the src/components folder, create a new file named Menu.js and add the following to it:

1import { Menu as AntDMenu } from "antd";
2import { useState } from "react";
3import { Link } from "react-router-dom";
4import { PictureOutlined, UploadOutlined } from "@ant-design/icons";
5
6const Menu = () => {
7 const [currentlySelected, setCurrentlySelected] = useState("upload");
8
9 const handleMenuSelection = (e) => {
10 setCurrentlySelected(e.key);
11 };
12
13 const items = [
14 {
15 label: <Link to="/gallery">Gallery</Link>,
16 key: "gallery",
17 icon: <PictureOutlined />,
18 },
19 {
20 label: <Link to="/">Upload Media</Link>,
21 key: "upload",
22 icon: <UploadOutlined />,
23 },
24 ];
25
26 return (
27 <AntDMenu
28 mode="horizontal"
29 onClick={handleMenuSelection}
30 selectedKeys={[currentlySelected]}
31 items={items}
32 />
33 );
34};
35
36export default Menu;

This menu provides two items - one for the gallery and the other for the upload form. Using the Link component provided by react-router-dom, we specify the path to which the application should redirect when an item is clicked.

Next, in the src folder, create a new file named routes.js and add the following to it:

1import Gallery from "./components/Gallery";
2import MediaUpload from "./components/MediaUpload";
3
4const routes = [
5 {
6 path: "/",
7 element: <MediaUpload />,
8 },
9 {
10 path: "/gallery",
11 element: <Gallery />,
12 },
13];
14
15export default routes;

Here we specify the component to be rendered for a given path.

Update App.js

Finally, update src/App.js to match the following:

1import "./App.css";
2import { useRoutes } from "react-router-dom";
3import routes from "./routes";
4import Menu from "./components/Menu";
5import { Col, Row } from "antd";
6
7const App = () => {
8 const router = useRoutes(routes);
9 return (
10 <div style={{ margin: "1%" }}>
11 <Menu />
12 <div style={{ textAlign: "center" }}>
13 <Row justify="center" align="middle" style={{ textAlign: "center" }}>
14 <Col style={{ width: "100%", margin: "2%" }}>{router}</Col>
15 </Row>
16 </div>
17 </div>
18 );
19};
20
21export default App;

Here we render the menu along with the content rendered by the router (as determined by the current path).

With everything in place, run your application using the following command:

1npm start

By default, the application will be available at http://localhost:3000/. The final result will look like the GIF shown below.

Find the complete project here on GitHub.

Conclusion

In this article, we looked at how to build an interactive product gallery using Cloudinary. Using Cloudinary, not only can we deliver a seamless storage and retrieval process for images, we are also able to render an interactive, aesthetically pleasing gallery without worrying about styling, functionality, and the likes (the Gallery component is less than 30 lines long).

For this tutorial, we rendered only images and videos. However, the widget can also render 360 spin sets and 3D models, which you can read more about in the Cloudinary guide. With all these, your application guarantees a seamless experience that speaks much more than a static image.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.