Interactive Product Page with React and Cloudinary

0
136

Consumers today expect to have the ability to customize products they purchase online. Ecommerce sites must evolve to support greater personalization, and a key to that is enabling more functionality on their product retail page, also known as an order page. On this page is where buyers can customize the products they want to buy, changing product properties, such as size, color, delivery means, quantity and other criteria.

Amazon Retail Page

Amazon retail page

Proper UX demands that when these decisions are being made, the user deserves a visual feedback. For example, if the user is buying a shirt and prefers the red one, the developer should account for that by updating the shirt image to a red variant.

The idea of implementing such features can be scary. We know how difficult it is to manipulate the color of a non SVG image. Fortunately, Cloudinary, a cloud-based image management and delivery solution, makes it very easy to implement this feature.

With Cloudinary, we will account for the following challenges in this article:

  • Varying Image Sizes: Product images in multiple sizes (thumbnail, main image, hi-res zoom image). Cloudinary can deliver these easily, you only need to add the size into the URL and they various images are dynamically generated.
  • Varying Colors – Some products come in multiple colors, and typically these are all shot as individual images, then they are all uploaded and processed. With Cloudinary, we can simply change the color of the product by calculating how much we have to adjust the RGB channels from the original color to arrive at the desired color. This allows infinite scaling, as we can switch to any color in the spectrum.
  • Custom Texts – Many retailers offer some personalized offerings, such as embroidering, adding logos and designing your own products. Cloudinary can overlay text and images on top of the shirt and can even make it look photorealistic with Displacement Mapping.

This is what the final demo looks like:

#Setup Environment

Cloudinary integrates well with any front-end framework. You have the option to use the JavaScript core library or a specific framework wrapper library. For the examples in this article, we will be using React. We can set it up using create-react-app:

# 1. Install create-react-app globally
npm install -g create-react-app
# 2. Create new app
create-react-app cloudinary-retail-page
# 3. Install Cloudinary React library
npm install --save cloudinary-react

For simplicity, all our example code will live in the src/App.js folder.

#Problem 1: Varying Image Sizes

Let’s address the challenges we listed above, one after the other. Varying Image Sizes is the first on the list.

The image above shows two sets of images: Thumbnails and main. The main image is for the buyers consumption, while the thumbs are for interaction.

The selected thumb and the main image are obviously the same, and the most common ways of delivering them are:

  1. Creating multiple images for a given product manually. This is not scalable considering the 100s of thousands of products available on the website.
  2. Using a high resolution image and resize them to fit the main or thumb section using CSS width and height properties. This is a terrible idea because if each image is 800kb with 3 variations, you end up with the following:

1 main (800) + (1 thumb (800) x 3 models) = 3200kb = 3.2mb

Cloudinary enables you to specify the size you prefer during delivery using transformation. In that case, you would have one image on your Cloudinary sever and request a particular size. Let’s see an example:

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 4 components:

  • Image: Delivering images using a publicId
  • Video: Delivering videos using a publicId
  • Transformation: Applying transformations to images and vidoes
  • CloudinaryContext: Wraps multiple Images and Videos to provide them your cloud name. The cloud name is received once you sign up for free

Our React state holds an array of image public Ids, shirts, that are on the Cloudinary server.

We iterate over this array of shirts and use the custom ImageTransformations component to request images of width 75px. These images are displayed as thumbs.

When the thumb is clicked, ImageTransformations is also used to render the main image of width 600px. This leaves us with an optimized solution:

1 main (800) + 1 thumb (100) x 3 models = 1100kb = 1.1mb

3200 – 1100 = 2100kb = 2.1mb

We stripped off more than 60% of xtra kilobytes with Cloudinary.

#Problem 2: Varying Colors

When buyers are allowed to choose a color, what we do most times is to create a clickable color palette and replace the images with the product image that has the selected color.

This common pattern doesn’t scale because a buyer might prefer a color that does not match the colors of the products you provided. This is limiting because a given product might have varying colors, or the product’s colors can be customized to have the buyers color choice after purchase.

Cloudinary makes it possible to use just one image and adjust the RGB channels from the original color with simple transformation.

<Transformation effect="red:255" />
<Transformation effect="blue:255" />
<Transformation effect="green:255" />

We can apply it in the previous example as shown below:

...
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;

We have extended our app with the following:

  • react-color library is a color library with varieties of options. We use one of these options, SketchPicker, to select colors and set the background state with the selected color. The state is set using handleColorChange.
  • The r, g and b values are set to the image using Transformation component.
  • The RGB values are negative because Cloudinary’s red/blue/green adjusts the respective color channel by percentage and the color white in RGB is the max value of 256,256,256. The only way to go is down from white, hence the negative adjustments.
  • The trick for the model wearing a shirt and the hanger is using underlay and overlay transformation respectively. We just used this opportunity to include those.

We have been using component.forceUpdate() to manually update the view because component.setState() is async which makes changes happen very late.

#Problem 3: Custom Text

With the Cloudinary overlay feature, you can add text to images. This feature is common among companies that are known for printing custom text on fabrics. It is quite simple to accomplish using Cloudinary:

<Transformation overlay="text:Roboto_30Scotch.io" />

Then we will add a text, “Scotch.io”, with Roboto font and 30px large. We can set that in our example, as well, by adding a text field to collect the text and updating the image with the text when a keystroke is received:

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;

The code sample has been truncated so you can see what is going on.

#Conclusion

Using Cloudinary on your retail site makes the lives of everyone involved in the creative cycle easier and opens up new opportunities that wouldn’t be possible otherwise. Get started if you haven’t by signing up for free on the Cloudinary website.

You can also access the repository HERE

Suggest

The Full JavaScript & ES6 Tutorial – (including ES7 & React)

React.js: Building Production Ready Apps, Start to Finish

JavaScript: Understanding the Weird Parts 

LEAVE A REPLY

Please enter your comment!
Please enter your name here