Building an Offline PWA Camera App with React and Cloudinary
Tags: offline, pwa, react, cloudinary
Published: 2018-09-11 22:18:13

---
title: Building an Offline PWA Camera App with React and Cloudinary
published: true
description: An offline Progressive Web Application (PWA) Camera app with React and Cloudinary
tags: offline, pwa, react, cloudinary
---


> **Note:** This project was built as a thought starter for all those participating in the upcoming [React Riot](https://www.reactriot.com/) 48-hr hackathon. For details of the code, see the [GitHub](https://github.com/oreHGA/cl-react-pwa) repository. You can view and edit live code on [CodeSandbox](https://codesandbox.io/s/5xnvnn80xk) or checkout out the [demo](https://5xnvnn80xk.codesandbox.io/). Hope you enjoy!

This tutorial steps you through the process of building an offline Progressive Web App (PWA) camera app with React and Cloudinary—one that takes pictures with your camera and then uploads it to your Cloudinary media library. A marvelous feature in the app is that when you are offline, the app stores your images in the browser’s local storage (`localStorage`). Once an Internet connection becomes available, the app automatically uploads all the saved pictures to your media library on Cloudinary.


![CloudyCam in Online Mode](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536456626058_ezgif.com-video-to-gif.gif)
![CloudyCam in Offline Mode](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536456664229_ezgif.com-video-to-gif+1.gif)



## Installing the Prerequisites

First, install the following software on your machine:


- [Node](https://nodejs.org/)[.js](https://nodejs.org/) (version 6 or higher)
- [Yarn](https://yarnpkg.com/) and [NPM](https://www.npmjs.com/)


## Getting Started

Next, create a React app.

**Note:** If you manage dependencies on your machine with Yarn, download the package runner `npx`. However, if you use NPM for dependency management, you can skip that step because `npx` is already bundled with NPM (version 5.2 or higher).

To add `npx` to Yarn, run this command on your terminal:

```bash
    yarn global add npx
```

Afterwards, create a starter React project, which you will tweak as you proceed with this tutorial:

```bash
    npx create-react-app cloudy-cam-pwa
```

To ensure that the project is in place, go to the application directory and start the development server:

```bash
    cd cloudy-cam-pwa
    yarn start # or npm start
```

The above command starts a development server on `http://localhost:3000`. Navigating to that URL on your browser displays the React app:


![Simple React Application](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536456849174_Screen+Shot+2018-09-09+at+1.33.44+AM.png)



## Creating a `Webcam` Class

To grant the app access to your camera, build a `Webcam` class for the camera’s main capabilities by creating a `webcam.js` file in the `src` directory:

```js
    // src/webcam.js
    export class Webcam {
      constructor(webcamElement, canvasElement) {
        this.webcamElement = webcamElement;
        this.canvasElement = canvasElement;
      }

      adjustVideoSize(width, height) {
        const aspectRatio = width / height;
        if (width >= height) {
            this.webcamElement.width = aspectRatio * this.webcamElement.height;
        } else  {
            this.webcamElement.height = this.webcamElement.width / aspectRatio;
        }
      }
    [...]
```

The `Webcam` constructor accepts two elements: `Web``camElement` (`videoElement`) and  `CanvasElement`. The `adjustVideoSize()` method adjusts the video element to be proportionate to the size you specified when creating `videoElement` .

Now add the other methods to the `Webcam` class, as follows:

```js
    // src/webcam.js
    [...]
      async setup() {
        return new Promise((resolve, reject) => {
          if (navigator.mediaDevices.getUserMedia !== undefined) {
            navigator.mediaDevices.getUserMedia({
                audio: false, video: { facingMode: 'user' }
                })
                .then((mediaStream) => {
                    if ("srcObject" in this.webcamElement) {
                        this.webcamElement.srcObject = mediaStream;
                    } else {
                        // For older browsers without the srcObject.
                        this.webcamElement.src = window.URL.createObjectURL(mediaStream);
                    }
                    this.webcamElement.addEventListener(
                        'loadeddata',
                        async () => {
                            this.adjustVideoSize(
                                this.webcamElement.videoWidth,
                                this.webcamElement.videoHeight
                            );
                            resolve();
                        },
                        false
                    );
                });
          } else {
              reject();
          }
      });
      }

    [...]
```

The `setup()` function initializes the camera from the browser and assigns the video stream to your `VideoElement` in the component. That means granting access to the camera and returning the `videoStream` function to you.

Here are the methods for capturing images:

```js
    // src/webcam.js
    [...]
      _drawImage() {
        const imageWidth = this.webcamElement.videoWidth;
        const imageHeight = this.webcamElement.videoHeight;

        const context = this.canvasElement.getContext('2d');
        this.canvasElement.width = imageWidth;
        this.canvasElement.height = imageHeight;

        context.drawImage(this.webcamElement, 0, 0, imageWidth, imageHeight);
        return { imageHeight, imageWidth };
      }

      takeBlobPhoto() {
        const { imageWidth, imageHeight } = this._drawImage();
        return new Promise((resolve, reject) => {
            this.canvasElement.toBlob((blob) => {
                resolve({ blob, imageHeight, imageWidth });
            });
        });
      }

      takeBase64Photo({ type, quality } = { type: 'png', quality: 1 }) {
        const { imageHeight, imageWidth } = this._drawImage();
        const base64 = this.canvasElement.toDataURL('image/' + type, quality);
        return { base64, imageHeight, imageWidth };
      }
    }
```


The `_drawImage()` method takes the existing frame in `videoElement` when that function is called and displays the image on `canvasElement`. The `_drawImage()` method is then called in the `takeBlobPhoto()` and `takeBase64Photo()` methods to handle binary large object (blob) images or Base64 images, respectively.


## Creating a `Notifier` Component

Create a `components` folder in the `src` directory to hold the components for the app:

```bash
    mkdir components
```

To support offline use and access, you need a `Notifier` component that identifies the mode that is interacting with the app.

First, create a `Notifier` folder in your `src/components` directory:

```bash
    mkdir Notifier
    cd Notifier
    touch index.js Notifier.css # on Windows, run the following instead
    # copy NUL index.js
    # copy NUL Notifier.css
```

Next, install a package called `classnames` for displaying different colors for the various modes, that is, dynamically rendering different classes:

```bash
    yarn add classnames # or npm install classnames
```

Afterwards, edit your `Notifier/index.js` file to read like this:

```jsx
    // src/components/Notifier/index.js
    import React, { Component } from "react";
    import "./Notifier.css";
    import classnames from 'classnames';

    class Notifier extends Component {
      render() {
        const notifyclass = classnames('notify', {
          danger: this.props.offline
        });
        const message = this.props.offline ?
      `CloudyCam is offline! Your images will be saved now and then uploaded to your Cloudinary Media Library once your Internet connection is back up.`
      :
      `Take a picture and it will be uploaded to your Cloudinary Media Library.`;
        return (
            <div className={notifyclass}>
                <p>
                    <em>{message}</em>
                </p>
            </div>
        );
      }
    }

    export default Notifier;
```

Here, check the value of the  `offline` property that is passed when `Notifier` is called. If `offline` is `true`, the app is in offline mode and the class and message are displayed accordingly.

Edit your `Notifier/Notifier.css` file to read like this:

```css
    /* src/components/Notifier/Notifier.css */

    .notify{
        background-color: #0066B2;
        padding: 20px;
        text-align: center;
        color: white;
        margin-bottom: 20px;
    }

    .danger{
        background-color: #D77623;
    }
```

To use the `Notifier` component, edit the `src/App.js` file to read like this:

```jsx
    // src/App.js

    import React, { Component } from 'react';
    import logo from './logo.png';
    import './App.css';
    import Notifier from './components/Notifier';

    class App extends Component {
      constructor() {
        super();
        this.state = {
          offline: false
        }
      }

      componentDidMount() {
        window.addEventListener('online', () => {
          this.setState({ offline: false });
        });

        window.addEventListener('offline', () => {
          this.setState({ offline: true });
        });
      }

      componentDidUpdate() {
        let offlineStatus = !navigator.onLine;
        if (this.state.offline !== offlineStatus) {
          this.setState({ offline: offlineStatus });
        }
      }

      render() {
        return (
          <div className="App">
            <Notifier offline={this.state.offline} />
            <header className="App-header">
              <img src={logo} className="App-logo" alt="Cloudinary Logo" />
              <h1 className="App-title">CloudyCam</h1>
            </header>
          </div>
        );
      }
    }

    export default App;
```

The `App.js` component has one state, `offline`, which specifies whether or not the app is in offline mode. By default, the state is  `false`.  When `App.js` is mounted, the `componentDidMount` function, which is executed when the app is loaded, listens for the online/offline event and updates the `App.js` state accordingly.

The `render` function defines the layout of the app and the `Notifier` component, passing the offline state as a property to `Notifier` for display.

Fetch the Cloudinary logo from [here](https://res.cloudinary.com/cloudinary/image/upload/c_scale,w_200/v1/logo/for_white_bg/cloudinary_vertical_logo_for_white_bg.png) and save it in your `src` directory as `logo.png`.

Now you might wonder how all that is displayed in the app. In the `src/index.js` file, the `App` component is rendered on a `<``div``>` tag with the ID `root`, as follows:

```jsx
    // src/index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import registerServiceWorker from './registerServiceWorker';

    ReactDOM.render(<App />, document.getElementById('root'));
    registerServiceWorker();
```

To view your app, first run this command on your development server:

```bash
    yarn start
```

Afterwards, go to `http://localhost:3000` on your browser to display the app. Toggle your Internet connection and you will see one of the two versions on display, depending on whether you are online or offline (see below).

![Application View in Online Mode](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536299240543_Screen+Shot+2018-09-07+at+5.47.13+AM.png)
![Application View in Offline Mode](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536299240565_Screen+Shot+2018-09-07+at+5.46.50+AM.png)

## Creating a `ClCamera` Component

To put `Webcam` to use, create a Cloudinary Camera component called `ClCamera`. First, create a new `ClCamera` folder in your `src/components` folder:

```bash
    mkdir ClCamera
    cd ClCamera
    touch index.js ClCamera.css # on Windows, run the command
    # copy NUL index.js
    # copy NUL ClCamera.css
```

Install `axios`, which enables you to make HTTP requests in the app:

```bash
    yarn add axios # or npm install axios
```

Afterwards, edit the `ClCamera/index.js` file to read like this:

```jsx
    // src/components/ClCamera.js

    import React, { Component } from 'react';
    import { Webcam } from '../../webcam';
    import './ClCamera.css';
    import axios from 'axios';

    class ClCamera extends Component {
      constructor() {
        super();
        this.webcam = null;
        this.state = {
          capturedImage: null,
          captured: false,
          uploading: false
        }
      }

      componentDidMount() {
        // initialize the camera
        this.canvasElement = document.createElement('canvas');
        this.webcam = new Webcam(
            document.getElementById('webcam'),
            this.canvasElement
        );
        this.webcam.setup().catch(() => {
            alert('Error getting access to your camera');
        });
      }

      componentDidUpdate(prevProps) {
        if (!this.props.offline && (prevProps.offline === true)) {
          // if its online
          this.batchUploads();
        }
      }

      render() {
            const imageDisplay = this.state.capturedImage ?
                <img src={this.state.capturedImage} alt="captured" width="350" />
                :
                <span />;

            const buttons = this.state.captured ?
                <div>
                    <button className="deleteButton" onClick={this.discardImage} > Delete Photo </button>
                    <button className="captureButton" onClick={this.uploadImage} > Upload Photo </button>
                </div> :
                <button className="captureButton" onClick={this.captureImage} > Take Picture </button>

            const uploading = this.state.uploading ?
                <div><p> Uploading Image, please wait ... </p></div>
                :
                <span />

            return (
                <div>
                    {uploading}
                    <video autoPlay playsInline muted id="webcam" width="100%" height="200" />
                    <br />
                    <div className="imageCanvas">
                        {imageDisplay}
                    </div>
                    {buttons}
                </div>
            )
        }

    [...]
```

The `ClCamera` component contains three states:


- The `capturedImage` state, which holds a Base64 version of an image.
- A boolean `captured` state, which specifies whether an image has been captured.
- An `uploading` state, which specifies if an image is being uploaded to Cloudinary.

When the `ClCamera` component is mounted, the `componentDidMount()`  function creates a `canvas` element and a `Webcam` object, passing the `videoElement` and `canvasElement` elements as parameters. Afterwards, you initialize the camera feed.

When the app goes from offline to online mode, the `componentDidUpdate` method calls the `batchUpload()` method for uploading the images that were saved in the browser’s cache while the app was offline.

Here are the other methods that perform tasks in your app:


- When the `captureImage()` function is clicked, the `takeBase64Photo()` method is called to capture the image.

- The Base64 image is stored in the `capturedImage` state of `ClCamera`. And the `captured` state of the component is set to `true`.


- Two buttons are displayed, which trigger the `discardImage` method and the `uploadImage` method, prompting you to either discard or upload the image, respectively. The `discardImage()` method discards the image from the state of `ClCamera` and then sets the `captured` state to `false`.

```jsx
    // src/components/ClCamera/index.js
    [...]
        captureImage = async () => {
            const capturedData = this.webcam.takeBase64Photo({ type: 'jpeg', quality: 0.8 });
            this.setState({
                captured: true,
                capturedImage: capturedData.base64
            });
        }

        discardImage = () => {
            this.setState({
                captured: false,
                capturedImage: null
            })
        }

    [...]
```

The `uploadImage` function first checks your connection status and does the following:


- If the connection is offline, `uploadImage` creates a new unique string with the prefix `cloudy_pwa_` and then stores your Base64 image in the component’s `this.state.capturedImage` state in the browser’s `localStorage`. Finally, `uploadImage` calls the `discardImage()` method.


- If the connection is online, `uploadImage` makes a `POST` request to upload your Base64 image along with a Cloudinary Preset as a parameter.


> **Note:** Cloudinary Upload Presets are described in depth later in this tutorial.

```jsx
    // src/components/ClCamera/index.js
    [...]

        uploadImage = () => {
            if (this.props.offline) {
                console.log("you're using in offline mode sha");
                // create a random string with a prefix
                const prefix = 'cloudy_pwa_';
                // create random string
                const rs = Math.random().toString(36).substr(2, 5);
                localStorage.setItem(`${prefix}${rs}`, this.state.capturedImage);
                alert('Image saved locally, it will be uploaded to your Cloudinary media library once internet connection is detected');
                this.discardImage();
                // save image to local storage
            } else {
                this.setState({ 'uploading': true });
                axios.post(
                    `https://api.cloudinary.com/v1_1/CLOUDINARY_CLOUD_NAME/image/upload`,
                    {
                        file: this.state.capturedImage,
                        upload_preset: 'CLOUDINARY_CLOUD_PRESET'
                    }
                ).then((data) => this.checkUploadStatus(data)).catch((error) => {
                    alert('Sorry, we encountered an error uploading your image');
                    this.setState({ 'uploading': false });
                });
            }
        }

    [...]
```

> **Note:** Be sure to replace `CLOUDINARY_CLOUD_NAME` and `CLOUDINARY_UPLOAD_PRESET` with the actual values in your setup. See the next section for how to obtain those values.

**When `ClCamera` detects that your Internet connection has been restored, the `batchUploads` method is called, which searches `localStorage` for any previously stored images with the `findLocalItems` method. If no images are found, the function exits. Otherwise, the images are uploaded to the Cloudinary media library through a `POST` request to the upload endpoint with the image and preset as parameters. The `checkUploadStatus` method accepts the data response from Cloudinary’s API and then checks if the upload succeeded. In case of an error, `checkUploadStatus` displays a message to the effect that the image remains in `localStorage` for the next batch upload.

```jsx
        findLocalItems = (query) => {
            let i;
            let results = [];
            for (i in localStorage) {
                if (localStorage.hasOwnProperty(i)) {
                    if (i.match(query) || (!query && typeof i === 'string')) {
                        const value = localStorage.getItem(i);
                        results.push({ key: i, val: value });
                    }
                }
            }
            return results;
        }

        checkUploadStatus = (data) => {
            this.setState({ 'uploading': false });
            if (data.status === 200) {
                alert('Image Uploaded to Cloudinary Media Library');
                this.discardImage();
            } else {
                alert('Sorry, we encountered an error uploading your image');
            }
        }

        batchUploads = () => {
            // this is where all the images saved can be uploaded as batch uploads
            const images = this.findLocalItems(/^cloudy_pwa_/);
            let error = false;
            if (images.length > 0) {
                this.setState({ 'uploading': true });
                for (let i = 0; i < images.length; i++) {
                    // upload
                    axios.post(
                        `https://api.cloudinary.com/v1_1/CLOUDINARY_CLOUD_NAME/image/upload`,
                        {
                            file: images[i].val,
                            upload_preset: 'CLOUDINARY_CLOUD_PRESET'
                        }

                    ).then(
                      (data) => this.checkUploadStatus(data)
                    ).catch((error) => {
                        error = true;
                    })
                }
                this.setState({ 'uploading': false });
                if (!error) {
                    alert("All saved images have been uploaded to your Cloudinary Media Library");
                }
            }
        }
    }

    export default ClCamera;
```

> **Note:** Again, replace `CLOUDINARY_CLOUD_NAME` and `CLOUDINARY_UPLOAD_PRESET` with the actual values in your setup. See the next section for how to obtain those values.

The `ClCamera` component contains these style properties:

```css
    /* src/components/ClCamera/ClCamera.css */

    .captureButton{
      margin-top: 20px;
      padding: 10px;
      padding-left: 20px;
      padding-right: 20px;
      background-color: #0066B2;
      color: white;
      border-radius: 5px;
    }

    .deleteButton{
      margin-top: 20px;
      padding: 10px;
      padding-left: 20px;
      padding-right: 20px;
      background-color: #D77623;
      color: white;
      border-radius: 5px;
    }

    .imageCanvas{
      margin-top: 20px;
      width: 100%;
      height: 200px;
      display: flex;
      justify-content: center;
    }
```

## Setting up a Cloudinary Account
![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536349995882_Screen+Shot+2018-09-06+at+7.09.31+AM.png)


To handle image uploads in this app, leverage [Cloudinary](https://cloudinary.com). First, [create an account](https://cloudinary.com/signup) there.


![The Cloudinary Signup Page](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536350242110_Screen+Shot+2018-09-07+at+7.56.54+PM.png)


**Finding Out Your Cloud Name**
Cloudinary then takes you to your **Dashboard** (media console), in which your cloud name is specified under **Account Details** (see below). Replace the `CLOUDINARY_CLOUD_NAME` variable in the `ClCamera` component in the previous code segments with that name.


![Finding Out Your Cloud Name](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536350595670_Screen+Shot+2018-09-06+at+7.10.15+AM.png)


**Creating a Cloudinary Upload Preset**
Cloudinary Upload Presets enable you to set up the default behavior of your image uploads. That means that, instead of having to add parameters to apply to your images every time you upload one, you can define tags, transformations, and other analysis presets from your Cloudinary console. Simply specify the the preset name in your code and you’re good to go!

To create a preset, go to the [Upload Settings](https://cloudinary.com/console/settings/upload) screen and click the **Add upload preset** link:


![Adding an Upload Preset](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536352009847_Screen+Shot+2018-09-06+at+7.11.06+AM.png)


The **Add upload preset** screen is then displayed.

![Adding an Upload Preset](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536352218873_Screen+Shot+2018-09-06+at+7.11.34+AM.png)


Enter a name under **Preset name**, set **Mode** to **Unsigned**, and then specify the other details, as appropriate.


![Adding an Upload Preset](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536352218890_Screen+Shot+2018-09-06+at+7.11.54+AM.png)


When the `ClCamera` component uploads an image from your app, Cloudinary returns a data element that contains the information relevant to the image. That way, if you set up an Upload Preset to perform such tasks as face detection, image-color analysis, and object detection, Cloudinary returns the results to you for use as you deem appropriate. By default, Cloudinary returns the URL of your uploaded image.


## Testing and Auditing CloudyCam

 `ClCamera` is now ready for use. Update your `App.js` file to render the component, as follows:

```jsx
    // src/App.js

    // other imports
    [...]
    import ClCamera from "./components/ClCamera";

    class App extends Component {

      // other component methods
      [...]
      render() {
        return (
          <div className="App">
            <Notifier offline={this.state.offline} />
            <header className="App-header">
              <img src={logo} className="App-logo" alt="Cloudinary Logo" />
              <h1 className="App-title">CloudyCam</h1>
            </header>
            <ClCamera offline={this.state.offline} />
          </div>
        );
      }
    }

    export default App;
```
Next, ensure that your development server is running on `http://localhost:3000`. Navigate to that URL on your browser and verify that the various versions of your app are displayed:

![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536451882895_Screen+Shot+2018-09-09+at+12.02.40+AM.png)
![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536451882901_Screen+Shot+2018-09-09+at+12.05.35+AM.png)
![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536451883113_Screen+Shot+2018-09-09+at+12.07.34+AM.png)

![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536451883117_Screen+Shot+2018-09-09+at+12.07.38+AM.png)
![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536451883121_Screen+Shot+2018-09-09+at+12.07.48+AM.png)


**Accessing Uploaded Images**
To access all the uploaded images, go to your [Cloudinary Media Library](https://cloudinary.com/console/media_library):

![](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536453623753_Screen+Shot+2018-09-09+at+12.39.08+AM.png)


Feel free to use the images for your app as you desire. The [Cloudinary Documentation](https://cloudinary.com/documentation) on the existing usages of Cloudinary is a handy reference.

**Creating a Production Build**
To serve your app to users, first edit the CloudyCam manifest to read like this:

```json
    # public/manifest.json
    {
        "short_name": "CloudyCam",
        "name": "Clodinary Offline PWA Camera",
        "icons": [
            {
                "src": "favicon.ico",
                "sizes": "512x512 192x192 64x64 32x32 24x24 16x16",
                "type": "image/x-icon"
            }
        ],
        "start_url": "./index.html",
        "display": "standalone",
        "theme_color": "#000000",
        "background_color": "#ffffff"
    }
```
Recall that the `index.js` file contains this line of code:

```js
    registerServiceWorker();
```

It creates a service worker that caches the various assets and sections of your app so that even when your users are offline or have a poor Internet connection, they can still interact with and use CloudyCam.

Create a production build by running this command:

```bash
    yarn build # or npm run build
```

Yarn then creates an optimized production build of your app and places it in the `build` directory, ready for your users.

Serve the production build with the `serve` JavaScript package by running these two commands:

```bash
    yarn global add serve # or npm install -g serve
    serve -s build
```

Afterwards, Yarn creates a simple static server on `http://localhost:5000`. Navigate to that URL for the production version of your app.

Note that a panel on Google Chrome’s Developer Console, powered by [Lighthouse](https://developers.google.com/web/tools/lighthouse/), enables you to validate the quality of your web pages. Click the **Audits** tab of the Developer Console and run an audit on the production build. The results are then displayed:


![Auditing CloudyCam (1)](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536453175757_Screen+Shot+2018-09-09+at+12.30.31+AM.png)

![Auditing CloudyCam (2)](https://d2mxuefqeaa7sj.cloudfront.net/s_CB1529C18383F5548EDF13217A823CEEA09EC3BDFA57AD577E213213DCE5B3F4_1536453175770_Screen+Shot+2018-09-09+at+12.30.41+AM.png)


Here, CloudyCam is shown as a 100-percent PWA app even though the score reads 92. The remaining 8 percent will be achieved once your production server is running with HTTPS for all the app traffic.


## Moving On

You have now learned how to build a simple PWA Camera app with React and Cloudinary. For details of the code, see the [GitHub](https://github.com/oreHGA/cl-react-pwa) repository.

Feel free to use the concepts explained here to build other apps. Cloudinary offers a wide array of excellent features to make image and video management in web and mobile apps intuitive, seamless, and fast. Do [check them](https://cloudinary.com/solutions) [out](https://cloudinary.com/solutions). Happy hacking!