YouTube, Twitch, and TikTok are some of the most used platforms to promote and advertise products. According to Blog Hubspot, consumers prefer videos to images because videos give a better overview of the product.
Shoppable Video is an e-commerce feature by Cloudinary that allows users to add “shoppable” items in their product videos. This feature will enable customers to shop while watching your videos. The customers can shop the items that are then added to the cart. They will be redirected to a page to make a purchase when done shopping.
In this article, we will learn how we can render Shoppable videos in a NextJS app.
You can learn more on Shoppable video from the Cloudinary documentation.
This Shoppable Video feature is included in Cloudinary’s video package. Therefore, we can use the Cloudinary package to add the Shoppable Video feature to our videos. We will also learn how to render data of any selected item in the shoppable video’s product bar.
To demonstrate this, we will build a Next.js web app. This app will render the product videos on an index page. We will have a product details page, and this page will open to display a product when it’s clicked on the product bar.
On the product details page, the product’s name, price, image are shown, and an Add to Cart
button to add the product to our shopping basket.
Our final app will look like this:
The entire project was completed on CodeSandbox. Fork it to run the code.
GitHub URL: https://github.com/nextjs-prj/cloudinary-shoppable
## Prerequisites & Installation
The knowledge of JavaScript and React.js is required for this project. We require a Node.js binary and NPM, its package manager installed.
To create a new Next.js app, we open the terminal in our desired directory and run the below command:
```bash
yarn create next-app nextjs-shop
# OR
npx create-next-app nextjs-shop
Alternatively, you can create a Next.js project using yarn, as seen above.
This command will create a Next
app in the nextjs-shop
directory. We move into the directory with the command cd nextjs-shop
.
The Shoppable Video feature was built by Cloudinary; therefore, to add the feature to our app, we have to install Cloudinary’s SDK and its dependencies, which include the following:
- lodash : Contains major utility functions for Nodejs.
- cloudinary-core: Cloudinary JavaScript library that provides Cloudinary’ APIs and administration capabilities to web development.
- cloudinary-video-player: The Cloudinary Video Player is a JavaScript-based HTML5 video player bundled with many valuable customization and integration capabilities and is monetization and analytics-ready.
We will reference the lodash library, Cloudinary core and the Cloudinary video player directly from the Unpkg CDN.
In the root directory of the project, open _app.js
and update the returned JSX elements in the <Head>..</Head>
section to include:
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
<link
href="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.css"
rel="stylesheet"
/>
<script
src="https://unpkg.com/cloudinary-core@latest/cloudinary-core-shrinkwrap.min.js"
type="text/javascript"
></script>
<script
src="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.js"
type="text/javascript"
></script>
</Head>
//...
Code language: JavaScript (javascript)
This will load all the libraries once the app starts, and cloudinary
is available to all components through the window
object.
We will create our page route files. Then, the already existing index.js
in the pages
folder will render the index page.
So we create the pages using the terminal command in the project’s pages
directory:
mkdir product
touch product/[id].js
mkdir cart
touch cart/index.js
The product/[id].js
is a dynamic page. It takes the id of each component in its [id]
. The cart/index.js
is the page that lists the cart.
In _app.js
, we will set up a cart provider using React’s Context API. Then, we will persist products added to our cart using localStorage.
We will have methods getCart
, removeFromCart
and addToCart
. The Context Provider will pass around these methods, and components will consume them via the useContext
hook. To do this, first, we import all required dependencies using:
import "../styles/globals.css";
import Header from "../components/Header";
import Head from "next/head";
import React, { useCallback } from "react";
export const CartContext = React.createContext(null);
Code language: JavaScript (javascript)
Then, we create a MyApp
component with functions to get, add, and remove an item from cart:
function MyApp({ Component, pageProps }) {
const getCart = useCallback(() => {
const state = window.localStorage.getItem("state");
if (state) {
return JSON.parse(state);
}
return [];
});
const addToCart = useCallback((product) => {
const state = window.localStorage.getItem("state");
if (state) {
const stateObj = JSON.parse(state);
stateObj.push(product);
window.localStorage.setItem("state", JSON.stringify(stateObj));
} else {
const stateObj = [product];
window.localStorage.setItem("state", JSON.stringify(stateObj));
}
window.location.reload();
});
const removeFromCart = useCallback((product) => {
const state = window.localStorage.getItem("state");
const stateObj = JSON.parse(state);
const remProducts = stateObj.filter((_prduct) => _prduct.id != product?.id);
window.localStorage.setItem("state", JSON.stringify(remProducts));
window.location.reload();
});
return (
<>
</>
);
}
export default MyApp;
Code language: JavaScript (javascript)
We created a CartContext
by calling the React.createContext
API.
Finally, we return the JSX component:
function MyApp({ Component, pageProps }) {
// [component methods go in here]
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
<link
href="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.css"
rel="stylesheet"
/>
<script
src="https://unpkg.com/cloudinary-core@latest/cloudinary-core-shrinkwrap.min.js"
type="text/javascript"
></script>
<script
src="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.js"
type="text/javascript"
></script>
</Head>
<Header />
<CartContext.Provider value={{ getCart, addToCart, removeFromCart }}>
<Component {...pageProps} />
</CartContext.Provider>
</>
);
}
export default MyApp;
Code language: JavaScript (javascript)
Here the Header
component is rendered before the Component
. This was done so that the Header
will appear on all pages.
Component
is the component being rendered in a route. We enclosed the Component
with the CartContext.Provider
passing in an object to its value
attribute. The object contains the methods getCart
, removeFromCart
and addToCart
.
We create a file to contain a Header component using the terminal command:
mkdir components components/Header
touch components/Header/index.js touch components/Header/Header.module.css
We updated the content in components/Header/index.js
to:
import { header, headerName } from "./Header.module.css";
export default function Header() {
return (
<section className={header}>
<div className={headerName}>Shopify</div>
</section>
);
}
Code language: JavaScript (javascript)
It renders “Shopify”, the name of our app.
We will proceed to create a Cloudinary instance and render our product videos.
In pages/index.js
, we first import all dependencies with:
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Header from "../components/Header";
import { ProductCard } from "../components/ProductCard";
import { useContext, useEffect, useState } from "react";
import { CartContext } from "./_app";
import Link from "next/link";
Next, we created a Home component with:
```jsx
export default function Home() {
const [cld, setCld] = useState();
const [cart, setCart] = useState([]);
const { getCart } = useContext(CartContext);
useEffect(() => {
if (cloudinary) {
const _cld = cloudinary.Cloudinary.new({ cloud_name: "demo" });
setCld(_cld);
setCart(getCart());
}
}, []);
return (
<div className={styles.container}>
<Head>
<title>Shopify</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.breadcrumb}>
<h2>Shopify Products</h2>
<span></span>
</div>
<div className={styles.productscontainer}>
<div className={styles.yourproducts}>
<h3>Product Videos</h3>
<span>
<Link href="/cart">
<span
style={{
cursor: "pointer",
textDecoration: "underline",
color: "blue",
}}
>
Cart: <span>{cart?.length}</span>
</span>
</Link>
</span>
</div>
<div>
{cld &&
["rafting", "sea_turtle", "forest_bike"].map((product, i) => (
<ProductCard cld={cld} product={product} key={i} />
))}
</div>
</div>
</main>
</div>
);
}
Code language: HTML, XML (xml)
In the useEffect
hook, we got the cloudinary
object and created a Cloudinary
instance.
The cloud_name
passed to the Cloudinary.new(..)
call is a unique indentifier for our Cloudinary account.
To obtain a cloud name, create a Cloudinary account for free. The cloud name is visible on your Cloudinary dashboard.
_cld
is an instance of the Cloudinary
class. With this, we can call different methods and properties on the Cloudinary
object.
We added the product videos name in an array and rendered the ProductCard
component for each. The ProductCard
is passed the cld
instance and the product video name via similarly named props.
Before we create the ProductCard
component, we have to set up the shoppable configuration.
We have product videos forest_bike
, sea_turtle
and rafting
. We will set a shoppable configuration in the data
directory of the project that will be passed to the Cloudinary player when initializing the videos.
Here is the configuration for the rafting product video in this GitHub Gist:
https://gist.github.com/Chuloo/5db54cf58a83f2dd4c65bd7a134817a1
The products
array in the shoppable
object holds the shoppable config for all the products in the rafting
product video.
This snippet sets the raft in the video as a product in the products bar. Here’s a description of each key:
- The
productId
is the ID of the product. - The
productName
is the name of the product. It is set toRaft
. - The
startTime
is the time the raft appears in the video. The unit is in milliseconds. - The
endTime
is the time the raft product no longer appears in the video. - The
publicId
is the ID of the product image. The ID can be the id of the image in our Cloudinary account, or the id can be a public image URL. - The
hotspots
hold places in the video where we can add hotspots. The viewers can click on them for more info on the product.
Let’s see the object in the hotspots
array:
-
time
: the exact time to display the hotspot. -
x
: The position of the hotspot on the x-axis. -
y
: The position of the hotspot on the y-axis. -
tooltipPosition
: This is the alignment of the hotspot. The text can be aligned from either the left or from the right. - Finally,
clickUrl
: This is the URL to visit when the URL is clicked. - The
onClick
sets the click-behaviour of the product in the products bar. - The
action
is the action to perform when clicked. It is set togoto
, which will load a URL in a separate tab. - The
args
object sets the argument passed to thegoto
action. - The
url
sets the URL to go to when the product is clicked. Here, we put it to the dynamic routeproduct/[id]
with theid
beingraft
.
Similarly, we have Shoppable configuration objects for both the sea_turtle and forest_bike product videos.
This component renders each product video. We create new files for the component and its CSS modules using the terminal command:
mkdir components/ProductCard
touch components/ProductCard/index.js
touch components/ProductCard/ProductCard.module.css
Next, we edit ProductCard/index.js
to:
import { useEffect, useRef } from "react";
import styles from "./ProductCard.module.css";
import data from "./../../data";
export function ProductCard({ cld, product }) {
const videoRef = useRef(null);
useEffect(() => {
const source = data[product];
const demoplayer = cld.videoPlayer(videoRef.current).width(850);
demoplayer.source(product, source);
});
return (
<div className={styles.productcard}>
<video width="500" ref={videoRef} controls muted preload="false">
{" "}
</video>
</div>
);
}
Code language: JavaScript (javascript)
Here we destructured cld
and product
from the props passed which are the Cloudinary instance, and the name of the Cloudinary’s video to render, respectively.
In the useEffect
hook we set the shoppable config from the product
passed. Then we called the videoPlayer()
method from the cld
Cloudinary instance. This videoPlayer()
method is passed the reference to a video
element. This action makes Cloudinary sets its features on the video
HTMLElement. We passed videoRef.current
to the method. This holds a reference to the video
element in our UI.
The width
method sets the width of the video
element. The third line of code calls the source
method in the demoPlayer
. This demoPlayer
holds a reference to the video
element. We passed the product
to the source
method as the first argument and the source
as the second argument. The source
has the shoppable configuration of each video.
With this, we have configured and rendered shoppable videos in a Nextjs app.
Next, we’ll scaffold other components.
This component will render each product card in the cart. We create the component file and CSS module using the terminal command:
mkdir components/CartCard
touch components/CartCard/index.js touch components/CartCard/CartCard.module.css
We update CartCard/index.js
to:
import styles from "./CartCard.module.css";
export default function CartCard({ cart }) {
const { name, imageUrl, price } = cart;
return (
<div className={styles.cartcard}>
<div
style={{ backgroundImage: `url(${imageUrl})` }}
className={styles.cartcardimg}
></div>
<div className={styles.cartcarddetails}>
<div className={styles.cartcardname}>
<h3>{name}</h3>
</div>
<div className={styles.cartcardprice}>
<span>{price}</span>
</div>
</div>
</div>
);
}
Code language: JavaScript (javascript)
Here, we destructured the cart
object from the component props. Then, we render the name, the price and the image URL of the product.
We already configured each product in the product bars of each video to load the route product/[id]
. So the id
is the name of the product. For example, product/raft
, id becomes raft
.
In data/product_data.js
we create product data that will hold the price, name, and the image URL of each shoppable product.
You can find it in this GitHub Gist below.
https://gist.github.com/Chuloo/3809aae48de401ae193ea1c5f47904d4
https://gist.github.com/Chuloo/3809aae48de401ae193ea1c5f47904d
Each object in the data
array has an id
to hold the unique ID of the product, name
to hold the name of the product, imageUrl
to hold the image URL of the product, and the price
to hold the price of the product.
In the product/[id].js
page component, we will extract the product’s id from the id
param, use it to get the product details from the data
above, then render it.
First, we imported all required dependencies, defined the component, and its methods:
import styles from "../../styles/ProductView.module.css";
import { useRouter } from "next/router";
import products_data from "./../../data/products_data";
import { useEffect, useState, useContext } from "react";
import { CartContext } from "./../_app";
export default function Product(params) {
const { getCart, removeFromCart, addToCart } = useContext(CartContext);
const [inCart, setInCart] = useState(false);
const router = useRouter();
const {
query: { id },
} = router;
const product = products_data.find((_product) => _product.id == id);
useEffect(() => {
const cart = getCart();
const _cartFound = cart.find((_prdct) => _prdct.id == id);
const inCart = _cartFound ? true : false;
setInCart(inCart);
});
function _addToCart() {
addToCart(product);
}
function _removeFromCart() {
removeFromCart(product);
}
return (
}
Code language: JavaScript (javascript)
We got the value of the CartContext
using the useContext
hook. The state inCart
is a boolean to hold a true
value if the product is already in the shopping cart, and false
if otherwise.
Next, we extracted the id
of the product from the params using the useRouter
hook. Finally, we get the details of the product from the data
object and then render it on the UI.
The useEffect
callback checks the cart to know if the product has been in the cart and then sets the inCart
state appropriately.
The _addToCart
and _removeFromCart
functions all call the addToCart
and removeFromCart
functions in the CartContext
.
Next we render the JSX elements with:
export default function Product(params) {
// component definitions go in here
return (
<div className={styles.productviewcontainer}>
<div className={styles.productviewmain}>
<div
style={{ backgroundImage: `url(${product?.imageUrl})` }}
className={styles.productviewimg}
></div>
<div style={{ width: "100%", marginLeft: "15px" }}>
<div className={styles.productviewname}>
<h1>{product?.name}</h1>
</div>
<div className={styles.productviewminidet}>
<div
style={{
borderTop: "1px solid black",
borderBottom: "1px solid black",
paddingTop: "18px",
paddingBottom: "18px",
}}
>
<span
style={{
marginRight: "4px",
color: "rgb(142 142 142)",
display: "flex",
alignItems: "center",
}}
>
Price:{" "}
<span style={{ color: "black", fontSize: "2em" }}>
{product?.price}
</span>
</span>
</div>
<div style={{ padding: "14px 0" }}>
<span>
{!inCart ? (
<button className="btn" onClick={_addToCart}>
Add to Cart
</button>
) : (
<button className="btn-danger" onClick={_removeFromCart}>
Remove from Cart
</button>
)}
</span>
</div>
</div>
</div>
</div>
</div>
);
}
Code language: JavaScript (javascript)
We used the inCart
value to conditionally render either an Add to Cart
or Remove from Cart
button.
This page will render all the products in our cart. In a new index.js
file in the pages/cart
directory, we add the cart page component with:
import styles from "./../../styles/Home.module.css";
import { CartContext } from "./../_app";
import { useContext, useEffect, useState } from "react";
import CartCard from "./../../components/CartCard";
export default function Cart() {
const { getCart } = useContext(CartContext);
const [cart, setCart] = useState([]);
useEffect(() => {
const cart = getCart();
setCart(cart);
});
return (
<div className={styles.container}>
<main className={styles.main}>
<div className={styles.breadcrumb}>
<h2>Cart</h2>
<span>
<button
className="btn"
style={{ backgroundColor: "green", borderColor: "green" }}
>
Checkout
</button>
</span>
</div>
<div className={styles.productscontainer}>
<div>
{cart.map((_cartItem, i) => (
<CartCard cart={_cartItem} key={i} />
))}
</div>
</div>
</main>
</div>
);
}
Code language: JavaScript (javascript)
Here, we got the getCart
function from the CartContext
using the useContext
hook.
The cart
state is an array that holds the products in our cart. In the useEffect
hook, the getCart
function gets called, and the result is added to the cart
state. The cart
is then rendered using the map
function.
Finally, each cart item is rendered with the CartCard
component.
Shoppable Videos add interactivity to your product videos. This increases purchases and store conversion. In this tutorial, we started by introducing and learning about Shoppable videos. We demonstrated how to render them in a Nextjs app. Finally, we built a live Next.js store to demo the usage.
Head over to the Shoppable Video docs to learn more. You can find the source code of the project here.
You may find the following resources useful.