Creating Interactive Product Pages With React and Cloudinary
Because today’s consumers expect a level of product customization they purchase online, e-commerce sites must support more personalization, a key to which is adding features to product pages, also called order pages. That’s where shoppers can customize the products they want to buy by changing product properties, such as size, color, delivery means, and quantity.
User-friendly UX demands that when shoppers make purchases, they receive visual feedback. For example, if someone is buying a shirt in red, the product page should account for that by updating the shirt’s image to a red variant. Given how difficult it is to change the color of non-SVG images, implementing such a feature can be daunting. Fortunately, Cloudinary, a cloud-based solution for managing and delivering rich media, including images and videos, makes it a breeze.
This post steps you through the process of leveraging Cloudinary to build a demo app with these three capabilities for product pages:
- Varying image sizes: Cloudinary can seamlessly deliver product images in multiple sizes (main image; thumbnails; hi-resolution, zoomed-in images). All you need to do is add the sizes to the URL, after which Cloudinary dynamically generates the various images.
- Varying colors: Some products come in multiple colors, the images for which are typically individual ones that all need to be uploaded and processed. With Cloudinary, you can change the color of a product by calculating how much to adjust the RGB channels of the original color to arrive at the desired color. This approach allows infinite scaling, enabling you to switch to any color in the spectrum.
- Custom text: Many retailers offer features for personalization, such as embroidering, adding logos, and designing your own products. Cloudinary can overlay text and images on top of a shirt, for example, and can even make it look photorealistic with displacement mapping.
This is what the demo app looks like:
Here are the topics:
- Setting Up the Environment
- Solving Problem 1: Varying Image Sizes
- Solving Problem 2: Varying Colors
- Solving Problem 3: Custom Text
- Trying It Out
Setting Up the Environment
Cloudinary integrates well with all front-end frameworks so feel feel to use the JavaScript core library or a specific framework-wrapper library. For the demo app in this article, set up the environment with React, as follows:
# 1. Install create-react-app globally.
npm install -g create-react-app
# 2. Create a new app.
create-react-app cloudinary-retail-page
# 3. Install the Cloudinary React library.
npm install --save cloudinary-react
For simplicity, all the sample code resides in the src/App.js
folder.
Solving Problem 1: Varying Image Sizes
Above are two sets of images: main and thumbs (short for thumbnails). The main image is for shoppers; the thumbs, for interactions. Obviously, the selected thumb is the same as the main image, and you can deliver them in either of these two most common ways:
- Manually create multiple images for a given product. However, this approach is not scalable given the large amount of products on most e-commerce sites.
- Resize high-resolution images to fit the main or thumb section by means of CSS width and height properties. That’s the wrong thing to do because if an image has three variations and if each of them takes up 800 KB, you end up with the following: 1 main (800 KB) + (1 thumb (800 KB) x 3 models) = 3,200 KB (3.2 MB)
By specifying the size you prefer during delivery through image transformation with Cloudinary, you would have only one image on your Cloudinary server and can request a particular size for delivery. See this example code:
import React, { Component } from 'react';
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';
const ImageTransformations = ({width, selectedShirt}) => {
return (
<Image publicId={selectedShirt.main+'.jpg'}>
<Transformation width={width} crop="scale" />
</Image>
);
};
class App extends Component {
constructor(props) {
super(props);
const defaultShirt = {id: 1, main: 'shirt_only'};
this.state = {
shirts: [
defaultShirt,
{id: 2, main: 'laying-shirt'},
{id: 3, main: 'hanging_t-shirt'}
],
selectedShirt: defaultShirt,
};
}
selectShirt(thumb) {
this.setState({selectedShirt: thumb}, _ => this.forceUpdate())
}
render() {
return (
<div className="App">
<CloudinaryContext cloudName="<YOUR_CLOUD_NAME>">
<div id="imageDemoContainer">
<div id="mainImage">
<ImageTransformations
width="600"
rgb={rgb}
selectedShirt={this.state.selectedShirt}
text={this.state.text} />
</div>
<div id="imageThumbs">
<ul id="thumbs">
{this.state.shirts.map(thumb => {
return (
<li className={thumb.main === this.state.selectedShirt.main ? 'active': ''} onClick={this.selectShirt.bind(this, thumb)} key={thumb.id}>
{/*<Image publicId={thumb.main}>*/}
{/*<Transformation width="75" crop="scale" />*/}
{/*</Image>*/}
<ImageTransformations
width="75"
rgb={rgb}
selectedShirt={thumb}
text={' '} />
</li>
)
})}
</ul>
</div>
</div>
</CloudinaryContext>
</div>
);
}
}
export default App;
Cloudinary’s React library exposes four components:
- Image: Delivery of images, each with a public ID (
publicId
). - Video: Delivery of videos, each with a public ID (
publicId
). - Transformation: Transformations of images and videos.
- CloudinaryContext: Wrapping of multiple instances of
Image
andVideo
under your cloud name, which Cloudinary assigns to you once you’ve signed up for free.
Here’s what transpires:
- The React state holds an array of image public IDs, `shirts`, that are on the Cloudinary server.
- You iterate over that array of shirts and request images of width 75 px. with the custom `ImageTransformations` component. Those images are then displayed as thumbs.
- On a click of a thumb, `ImageTransformations` renders the main image of width 600 px., which makes for an optimized solution, as shown here:
1 main (800 KB) + 1 thumb (100 KB ) x 3 models = 1,100 KB (1.1 MB)
3,200 KB – 1,100 KB = 2,100 KB (2.1 MB)
That’s a saving of more than 60% of extra kilobytes, thanks to Cloudinary.
Solving Problem 2: Varying Colors
When shoppers choose a color for a product, what retailers usually do is create a clickable color palette and replace the images with the product image that sports the selected color. Such a practice does not scale, however, because a shopper might prefer a color that does not match any of the product colors on your page. Ideally, a given product would have countless colors, or shoppers would be able to customize the product’s color after purchase.
With Cloudinary, you can adjust the RGB channels of the original color with simple transformation—and with only one image as the source. See this code:
<Transformation effect="red:255" />
<Transformation effect="blue:255" />
<Transformation effect="green:255" />
You can then apply that code to the previous example, like this:
...
import { SketchPicker } from 'react-color';
const ImageTransformations = ({width, rgb, selectedShirt, text}) => {
return (
<Image publicId={selectedShirt.main+'.jpg'}>
<Transformation width={width} crop="scale" />
<Transformation effect={'red:'+((-1+rgb.r/255)*100).toFixed(0)} />
<Transformation effect={'blue:'+((-1+rgb.b/255)*100).toFixed(0)} />
<Transformation effect={'green:'+((-1+rgb.g/255)*100).toFixed(0)} />
<Transformation underlay={selectedShirt.underlay} flags="relative" width="1.0" />
<Transformation overlay={selectedShirt.overlay} flags="relative" width="1.0" />
</Image>
);
};
class App extends Component {
constructor(props) {
super(props);
const defaultShirt = {id: 1, main: 'shirt_only', underlay: 'model2', overlay: ''};
this.state = {
shirts: [
defaultShirt,
{id: 2, main: 'laying-shirt', underlay: '', overlay: ''},
{id: 3, main: 'hanging_t-shirt', underlay: '', overlay: 'hanger'}
],
text: ' ',
selectedShirt: defaultShirt,
background: {rgb:{r:255,g:255,b:255}}
};
}
handleColorChange(color) {
// Updates color
this.setState({ background: color }, _ => this.forceUpdate());
};
selectShirt(thumb) {
// Updates main image
this.setState({selectedShirt: thumb}, _ => this.forceUpdate())
}
render() {
const rgb = this.state.background.rgb;
return (
<div className="App">
<CloudinaryContext cloudName="christekh">
<div id="demoContainer">
<div id="header">
<a href="http://cloudinary.com/">
<img width="172" height="38" src="http://res-1.cloudinary.com/cloudinary/image/asset/dpr_2.0/logo-e0df892053afd966cc0bfe047ba93ca4.png" alt="Cloudinary Logo" />
</a>
<h1>Product Personalization Demo</h1>
</div>
</div>
<div id="imageDemoContainer">
<div id="mainImage">
<ImageTransformations
width="600"
rgb={rgb}
selectedShirt={this.state.selectedShirt}
text={this.state.text} />
</div>
<div id="imageThumbs">
<ul id="thumbs">
{this.state.shirts.map(thumb => {
return (
<li className={thumb.main === this.state.selectedShirt.main ? 'active': ''} onClick={this.selectShirt.bind(this, thumb)} key={thumb.id}>
{/*<Image publicId={thumb.main}>*/}
{/*<Transformation width="75" crop="scale" />*/}
{/*</Image>*/}
<ImageTransformations
width="75"
rgb={rgb}
selectedShirt={thumb}
text={' '} />
</li>
)
})}
</ul>
</div>
</div>
<div id="demoInputContainer">
<div className="inputSelections">
<h2>Shirt Color:</h2>
<SketchPicker
color={ this.state.background.hex }
onChangeComplete={ this.handleColorChange.bind(this) }
/>
</div>
</div>
</CloudinaryContext>
</div>
);
}
}
export default App;
You’ve now extended the app, as follows:
react-color
library is a color library with many options. With the optionSketchPicker
, you select a color and set thebackground
state with that color by means ofhandleColorChange
.- You set the
r
,g
, andb
values of the image with theTransformation
component. - The
RGB
values are negative because Cloudinary’sred
,blue
, andgreen
parameters adjust the respective color channel by percentage, and the color white in RGB is the maximum value of 256,256,256. The only way to go is down from white, hence the negative adjustments. - To display the two images, one of the model wearing a shirt and the other of a hanger, you apply the
underlay
andoverlay
transformation, respectively.
Note: The code manually updates the view with component.forceUpdate()
because component.setState()
is asynchronous, which causes delays in reflecting the changes.
Solving Problem 3: Custom Text
You can add text to images with the Cloudinary overlay feature, which is a standard service offered by companies that print custom text on fabrics. The code can’t be simpler:
<Transformation overlay="text:Roboto_30Scotch.io" />
Next, add the text Scotch.io in 30-px. Roboto font. Alternatively, add a text field to collect the text and update the image with the text on receipt of a keystroke, as follows:
import React, { Component } from 'react';
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';
import { SketchPicker } from 'react-color';
import './App.css';
const ImageTransformations = ({width, rgb, selectedShirt, text}) => {
return (
<Image publicId={selectedShirt.main+'.jpg'}>
<Transformation overlay={'text:Roboto_30:'+text} flags="relative" gravity="center" />
</Image>
);
};
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: ' ',
...
};
}
...
handleTextChange(event) {
this.setState({text: event.target.value}, _ => this.forceUpdate())
}
render() {
const rgb = this.state.background.rgb;
return (
<div className="App">
<CloudinaryContext cloudName="christekh">
<div id="imageDemoContainer">
..
</div>
<div id="demoInputContainer">
...
<div className="inputSelections">
<h2>Text:</h2>
<input className="form-control" type="email" placeholder="Enter text" value={this.state.text} onChange={this.handleTextChange.bind(this)} />
</div>
</div>
</CloudinaryContext>
</div>
);
}
}
export default App;
We’ve truncated the above code sample so you can see the entire process.
Trying It Out
Using Cloudinary on your e-commerce site greatly eases the life of everyone on the creative team, opening up new opportunities that wouldn’t be available otherwise. Get started by signing up for free on the Cloudinary website.