Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event
Tags: react, generative, tutorial, hooks
Published: 2021-05-27 03:55:56

## What are we building?

In the interest of open source, today I’m going to take you through creating your own photo booth using the same technologies that were used in building the 2021 Red Hat Summit photo booth.

[Wait, why a photo booth?](#why-a-photo-booth)

### This is what were building!

{% youtube beXBkDTzcjg %}

<br />

[Check out the live version here!](https://photo-booth.make.cm/)

If you're impatient like me, here's the Github repository so you can get a running start!

{% github makecm/photo-booth-app no-readme %}

### The stack

- **React**: Framework we used to build both of our application and template
- **Make**: To host our template and generate it into a sharable PNG
- **Cloudinary**: To host the uploaded photo at a public URL and transform the image prior to sending to the Make template

### Putting it all together

**1. Template (React)**
We’ll be importing our templates, ready made, from the Make Gallery.

**2. App (React, Make, Cloudinary, Axios)**
For our application, we will be building out the following functionality:

- Uploading and transforming our image with Cloudinary
- Generating our unique photo booth image with Make
- Creating a generative Preview with custom React hooks

---

If you're interested in the why, read on - however if you just want to crack in, jump down to [0. Getting Started](#0-getting-started)


---

## Why a photo booth?
{% collapsible Read more %} 
COVID-19 changed many things for millions of people around the globe. It transformed work, dramatically influencing how we adapt office productivity, travel (or not travel), even the way we interact with others. It was a major decentralization event in our history.

For Red Hat, the leader in enterprise open source software, they too underwent change - notably, their events evolved. The largest event for them was (and still is) the Red Hat Summit, which brings a global community of customers, partners, and open source contributors together for a multi-day event. At the Red Hat Summit, attendees share, learn and experience a branded manifestation of Red Hat and inspires an audience with the potential of what enterprise open source technology unlocks. It’s about quality not quantity but the Summit regularly attracted ~5,000 in person attendees and was repeated globally through ~20 physical satellite events known as the Red Hat Forum which attract up to 2,000 people each.

For the 2020 Summit (and more recently the 2021 event), Red Hat adapted by (appropriately) virtualizing the event - additionally lowering the barrier to entry for attendees (foregoing registration fees), which saw attendance skyrocket.

Replicating the excitement of an in-person event is non-trivial.  How could they to generate that sense of community when their audience was attending from home?

### Enter: Photo booth, stage left.

Successfully engaging physical events are abundant with in-person brand activations. Sticker walls, colouring in stations, competitions, trivia, interactive exhibits, t-shirt screen printing , and even photo-booths. There are so many great ways to make a space exciting and engage your audience.

The idea of allowing attendees to create sharable and unique user generated content is not a revolutionary idea (see Facebook profile picture frames), however it is an effective way for people to know that they are not alone. That’s why Red Hat deployed strategically placed UGC activations throughout campaigns in 2020 and into 2021 (spearheaded by their Summit experiences) to stoke the fire of community and inclusiveness - made all the more simple with technologies like Make 😀.

{% twitter 1387579580613992450 %}


<br/>

Summit 2020 was a massive success, over 40,000 people attended and 17,000 unique Make requests were served from the event photo booth, with many taking to social media. Special shout out has to go to former Red Hat CEO and current IBM CEO Jim Whitehurst for sharing.

![Jim Whitehurst IG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46b7wykarilest2r00aa.png)

<br/>

In 2020 we helped Red Hat execute their first digital photo booth using Make.cm technology inside an iframe on their Summit event site. In 2021 we’re delighted that Red Hat were able build their own the interactive experience seamlessly and directly into  several parts of the Summit experience itself.

{% endcollapsible %}

---

## 0. Getting Started

### Importing our template

Our template is relatively simple for this guide, so instead of spending the time building it we’re going to just import it straight from the Gallery.

Jump across to http://make.cm/gallery

![make gallery](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r4p7lhuz3y7m0l38q3km.png)

Select the **Photo Booth Template**, hit the **Import this Template** button and follow the prompts to sign in/up, creating your template repository on Github and finally importing it into Make.

![make gallery import view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xiieq9wbmlyzt3znea0k.png)

With all of that complete we will end up on the dashboard of our new Photo Booth template, which will look something like the below image.

![imported template dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7f9klyp7v2knsm2y1xk7.png)

While you’re on the dashboard you can do a few things:
- Test out your new template endpoint by sending a few requests in the API playground.
- Navigate to the Github repository that Make created for you. Pull it down, make some changes and push it back up.
- View previously sent requests in the Generation Requests table

### Setting up our app

For our application we're going to be using [Create React App (CRA)](https://create-react-app.dev/docs/getting-started/). To get started let's go ahead create our app from the terminal.

```bash
$ npx create-react-app photo-booth-app
```
We can then sanitize our newly created react app. You will need to fix up some broken imports in your `App.js` and `index.js`.

```
/node_modules
/public
/src
  App.css
  App.js
  App.test.js 🗑
  index.css 🗑
  index.js
  logo.svg 🗑
  reportWebVitals.js 🗑
  setupTests.js 🗑
  .gitignore
  package.json
  README.md
  yarn.lock
```

While we’re at it, let’s install the dependencies we’ll need.
- [**minireset.css**](https://jgthms.com/minireset.css/): simple CSS reset
- [**axios**](https://github.com/axios/axios): to handle our API requests to Cloudinary and Make
- [**react-device-detect**](https://www.npmjs.com/package/react-device-detect): to determine our download procedures for mobile and desktop devices
- [**dot-env**](https://www.npmjs.com/package/dotenv): to store our Make and Cloudinary keys. While I know they’ll still end up in the built bundle, I’d love to keep them out of my git repo if I decide to push it up

```bash
$ yarn add minireset.css axios react-device-detect dotenv
```

Once those have installed, import `minireset.css` into our App. (we’ll import the others in-situ when we get to them).

```jsx
// App.js

import 'minireset.css';
import './App.css';

function App() {
  return <div className="App">{/* OUR APP CODE */}</div>;
}

export default App;
```

---

## 1. Constructing our app structure

We can get started in building out the structure of our photo booth. Our work will fall into three directories:

1. `components`: To house our Uploader and Preview components (and their dependencies).
2. `providers`: We will use React’s Context and Hooks APIs to create a provider to handle our global app state. We did this so we didn’t need to worry about unnecessary prop drilling.
3. `make`: We separated out the unchangeable parts to the make request so that we can focus on crafting the body of our request to Make.

```
/node_modules
/public
/src
  /components    <-- 1
    /Preview
      index.js
      styles.css
    /Uploader
      index.js
      styles.css
  /providers     <-- 2
    appState.js
  /make          <-- 3
    client.js
  App.css
  App.js
  index.js
  .env.development
  .gitignore
  package.json
  README.md
  yarn.lock
```

Once we’ve got that we can then add in the main bones of our application in our `App.js`, which will look like this.

```jsx
import './App.css';

function App() {
  return (
    <div className="App">
      <header>
        <div>
          {/* <Icon /> */}
          <h1>React Photo Booth</h1>
        </div>
      </header>
      <div className="container">
        {/* <Uploader /> */}
        {/* <Preview /> */}
      </div>
    </div>
  );
}

export default App;
```

Let’s go ahead and drop in our main styles in `App.css`, we won’t be touching this at all - but just good to have from the start.

[Click here to view and copy the App.css](https://github.com/makecm/photo-booth-app/blob/main/src/App.css)

<br/>

And while we’re at it let’s round out the header with the proper `Icon`.

Create an `assets` folder under `src` and drop in your `icon.svg`.

```xml
<svg width="39" height="43" className="icon" viewBox="0 0 39 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.3823 6.52948C26.0644 6.52948 24.8026 7.05119 23.8739 7.9765C22.9455 8.90145 22.4259 10.1537 22.4259 11.4573H16.7185C16.7185 8.63327 17.8446 5.92704 19.8456 3.93336C21.8462 1.94004 24.5575 0.822083 27.3823 0.822083C30.2072 0.822083 32.9184 1.94004 34.9191 3.93336C36.9201 5.92704 38.0461 8.63327 38.0461 11.4573V24.1022H32.3387V11.4573C32.3387 10.1537 31.8191 8.90145 30.8908 7.9765C29.962 7.05119 28.7002 6.52948 27.3823 6.52948ZM19.5722 19.1744C18.2543 19.1744 16.9925 19.6961 16.0638 20.6214C15.1354 21.5464 14.6158 22.7987 14.6158 24.1022H8.90919H8.9084C8.9084 21.2782 10.0345 18.572 12.0355 16.5783C14.0361 14.585 16.7474 13.467 19.5722 13.467C22.3971 13.467 25.1083 14.585 27.109 16.5783C29.11 18.572 30.236 21.2782 30.236 24.1022H24.5286C24.5286 22.7987 24.009 21.5464 23.0806 20.6214C22.1519 19.6961 20.8901 19.1744 19.5722 19.1744ZM9.03181 25.7146C9.37419 27.941 10.4196 30.016 12.0357 31.6262C14.0363 33.6195 16.7476 34.7374 19.5724 34.7374C22.3973 34.7374 25.1085 33.6195 27.1092 31.6262C28.7253 30.016 29.7706 27.941 30.113 25.7146H24.256C24.0136 26.4107 23.6148 27.051 23.0808 27.583C22.1521 28.5083 20.8903 29.03 19.5724 29.03C18.2545 29.03 16.9927 28.5083 16.064 27.583C15.53 27.051 15.1312 26.4107 14.8888 25.7146H9.03181ZM38.0516 25.7146H32.3439L32.3438 37.1143L6.67065 37.1142L6.67067 11.4204L15.1068 11.4205C15.1128 9.41093 15.6137 7.45451 16.5409 5.71273L0.962921 5.71263L0.962891 42.822L38.0516 42.8221L38.0516 25.7146Z" fill="#667EEA"/>
</svg>
```

In our `App.js` we can import it as a `ReactComponent` and drop it into the `header`.

```jsx
import './App.css';

import { ReactComponent as Icon } from './assets/icon.svg'

function App() {
  return (
    <div className="App">
      <header>
        <div>
          <Icon />
          <h1>React Photo Booth</h1>
        </div>
      </header>
      <div className="container">
        {/* <Uploader /> */}
        {/* <Preview /> */}
      </div>
    </div>
  );
}

export default App;
```

Let's run our server and see what we get.

```bash
yarn start
```

![empty application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7fi5grlcm8tfhbt6ufb3.png)

With all of that work our application does absolutely nothing and looks like a dogs breakfast. Let's start to change that.

---

## 2. Creating our appState provider

To handle our application state and important data we decided to use a custom hook and React's Context API to provide the state to all of our components, instead of drilling the props and useState functions down to the children components.

I’m not going to go into a tonne of detail on this - however after watching this [super easy to follow guide](https://egghead.io/courses/build-a-name-picker-app-intro-to-react-hooks-context-api-1ded) released by Simon Vrachliotis last year I really started to understand how and when to deploy this type of approach.

To get started lets create a file called `appState.js` in our `providers` directory.

1. Inside of that we’ll create a context called `AppStateContext` - which in this context (no pun intended) is our application state.
2. To make this context available to our components we need to create a provider, which we’ll call `AppStateProvider`.
3. Finally we’re going to wrap our context in a super simple custom hook called `useAppState`. This allows us to access our context from wherever we are in the component tree.


```jsx
// providers/appState.js

import React, { createContext, useContext } from "react";

// 1
const AppStateContext = createContext();

// 2
export function AppStateProvider({ children }) {
  // Declare our hooks and global data here
  // [state, setState] = useState(null)

  const value = {
    // Import it into the value object here
  };


  return (
    <AppStateContext.Provider value={value}>
      {children}
    </AppStateContext.Provider>
  );
}

// 3
export function useAppState() {
  const context = useContext(AppStateContext);
  if (!context) {
    throw new Error(
      "You probably forgot a <AppStateProvider> context provider!"
    );
  }
  return context;
}
```

To wrap up we need to wrap our App in our `AppStateProvider` in the `index.js` so that we can access all of the good stuff in the future (once again, no pun intended).

```jsx
// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { AppStateProvider } from "./providers/appState";

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

With that done we can actually moving on to building out our components.

---

## 3. Uploader

Our `Uploader` component will allow users to choose their photo from their device and then we will pre-optimize it and send it to our Cloudinary bucket (that we will set up soon).

Our final component will look something like this and have the following:
- **Blank state** for the default view when nothing has been uploaded to Cloudinary
- **Loading/disabled state** when sending to Cloudinary - also includes a progressive loader and a spinner

{% youtube woJcW2sZsyM %}

<br/>

### Building our component

Inside of the `components/Uploader` directory lets add an `index.js` file with the following structure.

```jsx
import React from "react";
import axios from "axios";

import './styles.css';

import { useAppState } from "../../providers/appState";

const Uploader = () => {
  return (
    <>
      <div className="Uploader">
        <input
          type="file"
          id="fileupload"
          accept="image/*"
          title="Upload your Photo"
        />
        <label
          htmlFor="fileupload"
        >
          Upload your photo
        </label>
      </div>
    </>
  );
}

export default Uploader;
```

Let’s just get the CSS out of the way by adding a `styles.css` file into our `Uploader` directory.

> Fun fact - for `input`'s with the `type=”file”` you are extremely limited in the ability to style it with CSS. So we can actually rely on the `label` to do all of the heavy lifting for our big Upload button.

[Click here to view and copy the Uploader CSS](https://github.com/makecm/photo-booth-app/blob/main/src/components/Uploader/styles.css)

<br/>


Once we’ve got that, let’s add it to our `App.js`.

```jsx
// App.js

import './App.css';
import { ReactComponent as Icon } from './assets/icon.svg'
import Uploader from './components/Uploader'

function App() {
  return (
    <div className="App">
      <header>
        <div>
          <Icon />
          <h1>React Photo Booth</h1>
        </div>
      </header>
      <div className="container">
        <Uploader />
        <div>
          {/* <Preview /> */}
        </div>
      </div>
    </div>
  );
}

export default App;
```

Our App should look something like this.

![blank Upload component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfmyjxeuq0wnzmshuw68.png)
 
With that done, let’s setup our `useState` hooks in our `appState` that we can provide to our `Uploader` component.
- `imageUrl`: this is where we will store our public URL that Cloudinary returns to us
- `isUploading`: this is to trigger our uploading state for our component
- `progressIncrement`: this is to contain the current progress of the upload process to Cloudinary

```jsx
// providers/appState.js

export function AppStateProvider({ children }) {
  const [imageUrl, setImageUrl] = useState(null);
  const [isUploading, setIsUploading] = useState(false);
  const [progressIncrement, setProgress] = useState(null);

  const value = {
    imageUrl,
    setImageUrl,
    isUploading,
    setIsUploading,
    progressIncrement,
    setProgress,
  };

  ...
}
```

Inside of our `Uploader` component we can then access these values and functions from our provider by using our custom `useAppState()` hook.

```jsx
// components/Uploader/index.js

import React from "react";
import axios from "axios";

import './styles.css';

import { useAppState } from "../../providers/appState";

const Uploader = () => {
  const {
    setImageUrl,
    isUploading,
    setIsUploading,
    progressIncrement,
    setProgress,
  } = useAppState();

  return (
    <>
      <div className="Uploader">
        <input
          type="file"
          id="fileupload"
          accept="image/*"
          title="Upload your Photo"
        />
        <label
          htmlFor="fileupload"
        >
          Upload your photo
        </label>
      </div>
    </>
  );
}

export default Uploader;
```

### Creating our Cloudinary Account

With that ready to go let’s go ahead and create our Cloudinary account. To do so jump across to [Cloudinary](https://cloudinary.com/) and sign up for free.

For the purposes of this tutorial the free plan is pretty comprehensive and will be more than enough for our purposes. When you sign up, Cloudinary will assign you a cloud name (the name of your bucket), but you can change that if you want.

![Cloudinary Sign up form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hy7uih6ccasybgpkltce.png)

To send our assets to our newly created bucket, we’ll be using the Cloudinary’s [unsigned option for using the Upload API](https://cloudinary.com/documentation/upload_images#unsigned_upload), which was deemed to be the easiest method for uploading to Cloudinary. While it is a little less secure than signing our method it does allow us the quickest path to MVP.

For more robust production ready solutions I’d do some more research into signed methods of Upload.

By using the unsigned upload option we need the following information:
- `cloud_name`: the name of our bucket
- `upload_preset`:  defines what upload options we want to apply to our assets

While our `cloud_name` has already been created for us (on account sign up), to create an `upload_preset` go to:
- Your Settings (cog icon)
- Upload Settings
- Scroll down to the Upload Presets section.

By default there should already be a default one called `ml_default`.

Create another preset and set the signing method to `unsigned`. Everything else can remain as is.

![Cloudinary Upload Preset creation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fdg2o2plgdo646nlx37g.png) 

With your upload preset created, copy its name (along with the cloud name that can be found on the dashboard of your Cloudinary account) and paste those into a `.env.development` file (that you can create on the root directory).

> When we update these you will need to restart your local server again.

```
// .env.development

REACT_APP_CLOUDINARY_UPLOAD_PRESET=xxx
REACT_APP_CLOUDINARY_CLOUD_NAME=yyy
```

### Optimizing and sending our photo to Cloudinary

Now that we’ve got our bucket setup we can create our function to handle the file upload. Ultimately we’re doing the following:

1. Trigger our `isUploading` state.
2. Get our file.
3. Optimise and base64 our file so that we can send it to Cloudinary - for this we’ll be creating a callback function called `getBase64Image` to do the heavy lifting (which I’ll talk to in a second).
4. Send it via `axios` and store the `progressIncrement` that is periodically returned.
5. Store the response in our `imageUrl` state once finished.

We’ll call our function `onInputChange` and fire it `onChange` of our `input`.

```jsx
// components/Uploader/index.js

import React from "react";
import axios from "axios";
import './styles.css';
import { useAppState } from "../../providers/appState";

const Uploader = () => {
  const {
    imageUrl,
    setImageUrl,
    isUploading,
    setIsUploading,
    progressIncrement,
    setProgress,
  } = useAppState();

  const onInputChange = (event) => {
    // 1

    setIsUploading(true);

    // 2
    for (const file of event.target.files) {
      const uploadPreset = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET;
      const cloudName = process.env.REACT_APP_CLOUDINARY_CLOUD_NAME;
      const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`;

      // 3
      getBase64Image(file, (base64Value) => {
        const data = {
          upload_preset: uploadPreset,
          file: base64Value,
        };
        // 4
        // Cloudinary provides us a progressEvent that we can hook into and store the current value in our state
        const config = {
          onUploadProgress: function (progressEvent) {
            const progress = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
            setProgress(progress);
          },
        };

        axios
          .post(url, data, config)
          .then((response) => {
            // 5
            setIsUploading(false);
            setImageUrl(response.data.url);
          })

          .catch((error) => {
            console.log(error);
            setIsUploading(false);
          });
      });
    }
  };

  return (
    <>
      <div className="Uploader">
        <input
          type="file"
          id="fileupload"
          accept="image/*"
          title="Upload your Photo"
          onChange={onInputChange}
        />
        <label
          htmlFor="fileupload"
        >
          Upload your photo
        </label>
      </div>
    </>
  );
}

export default Uploader;
```

And this is what our `getBase64Image` function looks like. Paste this just above the `onInputChange` function.

1. We read the file as a DataURI
2. Create the bounds of our image and then calculate our canvas. In this case I’m creating a canvas as a max width and height of 1600px and then calculating the image based on that.
3. Compose our image on our canvas
4. Base64 our image as a JPG and pass it back to our onInputChange function

```jsx
const getBase64Image = (file, callback) => {
    // 1
    const reader = new FileReader();
    reader.readAsDataURL(file);

    reader.onload = (event) => {
      // 2
      let width = "";
      let height = "";

      const MAX_WIDTH = 1600;
      const MAX_HEIGHT = 1600;

      const img = new Image();
      img.style.imageOrientation = "from-image";
      img.src = event.target.result;

      img.onload = () => {
        width = img.width;
        height = img.height;

        if (width / MAX_WIDTH > height / MAX_HEIGHT) {
          if (width > MAX_WIDTH) {
            height *= MAX_WIDTH / width;
            width = MAX_WIDTH;
          }
        } else {
          if (height > MAX_HEIGHT) {
            width *= MAX_HEIGHT / height;
            height = MAX_HEIGHT;
          }
        }
        // 3
        const canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");

        canvas.width = width;
        canvas.height = height;

        canvas.style.imageOrientation = "from-image";
        ctx.fillStyle = "rgba(255,255,255,0.0)";
        ctx.fillRect(0, 0, 700, 600);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.drawImage(img, 0, 0, width, height);

        // 4
        const data = ctx.canvas.toDataURL("image/jpeg");
        callback(data);
      };
    };
    reader.onerror = function (error) {
      console.log("Error: ", error);
    };
  };
```

With that in place, crack open your react dev tools and check out our state in our `AppStateProvider` and try and upload an image. Ultimately we should see our `isUploading`  change, our `progressIncrement` tick up as it uploads and our imageUrl populate once uploading is finished.

{% youtube v6LC62jhnd0 %}

<br />

### Transforming our image

Cloudinary also offers us the ability to make on the fly adjustments to the images we’ve uploaded with their [Transformations API](https://cloudinary.com/documentation/transformation_reference).

For our photo booth case I want to always make sure that no matter where a face is in the image, that it will always be ‘wholly’ visible in the format.

To do that I’m going to push our response URL into a function called `imagePosition` prior to storing it in our state so that it has the necessary transformation on it.

All we’re doing here is splitting our url apart at the `/` and then inserting our transformation into the URL and joining it back together.

```jsx
const imagePosition = (url) => {
  const arr = new URL(url).href.split("/");
  const transformation = 'w_1080,h_1080,c_thumb,g_face/w_1000';
  console.log('hey')

  arr.splice(6, 0, transformation)
  const joinedArr = arr.join('/')

  return joinedArr
};
```

Finally instead of pushing our `response.data.url` straight into our imageUrl state, we’ll first run it through our `imagePosition` function.

```jsx
// components/Uploader/index.js

...
axios
  .post(url, data, config)
  .then((response) => {
    setIsUploading(false);
    setImageUrl(imagePosition(response.data.url));
  })

  .catch((error) => {
    console.log(error);
    setIsUploading(false);
  });
});
```

#### What difference does the transformation make?!

In the case I just used above here is what happens to my image with and without transformations.


![cloudinary trasnform differences](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9bi000sd0ceoi0rvzy5.png)
 
<br />

### Finalizing our states

Our uploader works, it just looks awful, so let’s create our uploading state.

1. Create 2 `spans` inside of our `label` and toggle between the two depending on our `isUploading` state.
2. Add some specific styling to our label background when `progressIncrement` increases. We can use a super simple, yet effective ‘hack’ with linear-gradient.
3. Add our `disabled` prop to our `input` so we can lock it when a file is uploading

```jsx
return (
    <>
      <div className="Uploader">
        <input
          type="file"
          id="fileupload"
          accept="image/*"
          onChange={onInputChange}
          title="Upload your Photo"
          {/* 3 */}
          disabled={isUploading}
        />
        <label
          htmlFor="fileupload"
          {/* 2 */}
          style={{
            background: `linear-gradient(90deg, #4C51BF ${progressIncrement}%, #667EEA ${progressIncrement}%)`
          }}
        >
          {/* 1 */}
          <span
            className="upload"
            style={{
              transform: isUploading && 'translateY(300%)'
            }}
          >
            Upload your photo
          </span>
          <span
            className="uploading"
            style={{
              top: isUploading ? '0' : '-180%'
            }}
          >
            Uploading
              <Spinner styles={{
              marginLeft: '1rem'
            }} />
          </span>
        </label>
      </div>
    </>
  );
```

To cap it off we’ll need to setup our `Spinner` component that we call in our `Uploading` span. Inside of the `Uploader` directory create a new file called `spinner.js`.

```jsx
// components/Uploader/spinner.js

import React from "react";

export default function Spinner({ size, styles }) {
  return (
    <div
      className={`${size === 'small' ? 'small' : ''} Spinner`}
      style={styles}
    />
  );
}
```

And don’t forget to import it at the top of the `Uploader` component

```jsx
import Spinner from './spinner'
```

With that complete you should have a functional `<Uploader />` component, returning you a beautifully transformed `imageUrl` and reflecting the proper state to the user.

{% youtube -gbxh0Yjveo %}

<br/>

---

## 4. Generating with Make.cm

Now that we’ve got our image from Cloudinary, let’s generate our photo so we can do something with it.

Let’s jump over to our `.env.development` file and add two new variables.

> When we update these you will need to restart your local server again.


```
// .env.development

REACT_APP_CLOUDINARY_UPLOAD_PRESET=xxx
REACT_APP_CLOUDINARY_CLOUD_NAME=yyy
REACT_APP_MAKE_KEY=
REACT_APP_MAKE_URL=
```

To find your API key and URL jump across to Make and select your photo booth template that you imported earlier. If you’re yet to import your template [go here and import it](https://make.cm/gallery/photo-booth).

Once you’re on the template dashboard you can grab the key and URL from the API playground view and paste it into your `.env.development` file.

![make API playground](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ysmpbo4kvyjjv4yxios0.png)
 

### Creating our hooks

With that done we’ll create the `useState` hooks we’ll need to handle our Make request and the response of our generated asset in our `appState`.

Our `isGenerating` hook will handle our loading state for when the request is in flight, while our `generatedAvatar` will store the result that Make sends back to our application.

```jsx
// providers/appState.js

...
const [isGenerating, setIsGenerating] = useState(false);
const [generatedAvatar, setGeneratedAvatars] = useState(null);

const value = {
  ...
  isGenerating,
  setIsGenerating,
  generatedAvatar,
  setGeneratedAvatars,
}
```

Like we’ve done before, consume our newly created hooks in the `useAppState()` hook in the `App.js` file.

```jsx
function App() {
  const {
    ...
    isGenerating,
    setIsGenerating,
    generatedAvatar,
    setGeneratedAvatars,
  } = useAppState();

  ...
}
```

### Developing our axios client and request

Like we did for the `Uploader` component, we will use `axios` to handle our Make POST request to generate our photo booth template into a PNG.

In our `make` directory let’s create a `client.js` file.

With our `client` we’ll use `axios.create` to create a default instance for our request. I opted to do this  because it keeps all of the headers and procedural code out of our `App.js`.

It also gives us a `client` that we can re-use down the track for different implementations.

```jsx
// make/client.js

import axios from "axios";

export const client = axios.create({
  headers: {
    'Content-Type': 'application/json',
    'X-MAKE-API-KEY': process.env.REACT_APP_MAKE_KEY
  }
});

const url = process.env.REACT_APP_MAKE_URL

export function make(data) {
  return client.post(url, data)
}
```

We can then import our `make` client into our `App.js`.

```jsx
import { useEffect } from 'react';
import { make } from "./make/client"
```

We will then use a React `useEffect` to trigger our request to Make. `useEffect`'s are great because you can trigger it based on a value updating. In our case we want to trigger the useEffect on the the `imageUrl` updating.

```jsx
// App.js

function App() {
  ...

  useEffect(() => {
      ...
  }, [imageUrl]);
```

With our `useEffect` in place we want to create our function to send our avatar to Make for generation.

1. First set our `isGenerating` state to `true` so that we can trigger a loading state.
2. We can then define our `data` that we want to pass to our Make template. This is split up into 4 areas:
- `customSize`: specifies the size of our generated filed
- `format`: specifies the file type to be generated to
- `data`: specifies any data we want to send to our template pre-generation. In this case our template knows to accept a `photo` string. We will then set that to our `imageUrl`.
- `fileName`: this can be whatever you want it to be
3. We then call our `make` client (that we created and imported just before) and send our `data` to it.
4. We wait and then store the `response` into our `generatedAvatar` state and turn off our `isGenerating` state

We also need to add any other dependencies into our `useEffect` as we will get a linting error.

```jsx
useEffect(() => {
  if (imageUrl !== null) {
    // 1
    setIsGenerating(true);

    // 2
    const data = {
      customSize: {
        width: previewSize.width,
        height: previewSize.height,
        unit: 'px',
      },
      format: "png",
      fileName: "image",
      data: {
        photo: imageUrl,
      }
    };

    // 3
    make(data)
      .then((response) => {
        // 4
        console.log(response.data.resultUrl)
        setGeneratedAvatar(response.data.resultUrl);
        setIsGenerating(false);
      })
      .catch((error) => {
        console.log(error);
        setIsGenerating(false);
      });
  }
}, [
  imageUrl,
  previewSize.height,
  previewSize.width,
  setIsGenerating,
  setGeneratedAvatar
]);
```

If you try it now, crack open the console and see what comes through.

{% youtube l6OEyhrtyZ8 %}

<br/>

🥳 Looks great, doesn’t it?

### Creating our Download button

With our logic all setup let’s create a button to be able to download our photo booth file once it’s ready. In the `return` of our `App.js` we can add a simple `a` tag and set the `generatedAvatar` that Make returns to us as the `href`.

One thing we’ll want to do is make sure that this button only shows once our request to Make is in flight. So we know that when our `imageUrl` exists we can show this button.

On the inverse we want to remove our `Uploader` once it’s finished its job of uploading. So we can check to see if `imageUrl` is not populated.

```jsx
return (
  <div className="App">
      {!imageUrl && (<Uploader />)}
      {imageUrl && (
        <div className="controlPanel">
          <a
            className={`download ${isGenerating ? 'disabled' : 'false'}`}
            target="_blank"
            rel="noreferrer noopener"
            href={generatedAvatar && generatedAvatar}
          >
            {isGenerating && (
              <Spinner styles={{ marginRight: '1rem' }} size="small" />
            )}
            {isGenerating ? "Generating..." : "Download"}
          </a>
        </div>
      )}
    </div>
  </div>
);
```

We're recycling the `Spinner` component we created for the `Uploader`, so remember to import it into your `App.js`.

```jsx
import Spinner from './components/Uploader/spinner'
```

Now, when you upload a photo to Cloudinary it will automatically trigger the request to Make and then store the result in our Download button.

{% youtube RewOptJYUsg %}

<br />

Amazing 🔥

### Mobile v Desktop download

There is one problem, however…

If a user was to use our photo booth on a mobile, their browser wouldn’t know where to download the image to (especially on an iPhone). So what we need to do is change our download behavior depending on if you’re accessing the photo booth on a mobile/tablet device or a desktop.

The Make API actually provides you a parameter to be able to control the behavior of ‘displaying’ your generated artwork, called `contentDisposition`.

With `contentDisposition` Make will set a header on our response to tell the browser to either display the file as an `attachment` (so downloading it and saving it locally - default) or `inline` (which opens it in a new tab). In this case we would want to do the following:

- **If mobile**: display our file as `inline` (so that a user can save it to Photos or something similar)
- **If desktop**: display our file as an `attachment` (and drop it straight to our local file system - most probably our Downloads folder).

The final piece to this puzzle is how we’re going to detect if our user is using the photo booth from a mobile or a desktop. For this implementation I’m going to use [react-device-detect](https://www.npmjs.com/package/react-device-detect).

> Caveat here, this may not be the best way to do this for certain implementations, but it’s API surface area is low and easy to manage.

```jsx
// App.js

import { isMobile } from "react-device-detect";
```

```jsx
// App.js

useEffect(() => {
  if (imageUrl !== null) {
    setIsGenerating(true);

    const data = {
      customSize: {
        width: previewSize.width,
        height: previewSize.height,
        unit: 'px',
      },
      format: "png",
      fileName: "image",
      contentDisposition: isMobile ? "inline" : "attachment",
      data: {
        photo: imageUrl,
      }
    };

    make(data)
      .then((response) => {
        console.log(response.data.resultUrl)
        setGeneratedAvatar(response.data.resultUrl);
        setIsGenerating(false);
      })
      .catch((error) => {
        console.log(error);
        setIsGenerating(false);
      });
  }
}, [imageUrl]);
```

Now users will be able to strike a pose on their phone and get their newly minted photo straight to their phone.

---

## 5. Preview

The last major piece to this puzzle is giving our user a preview of what they’re creating, of which I see two ways we can handle it:

**1.** We persist our Loading state on the Upload button until the Make request is fulfilled and then just set the returned image into a container.

- *Pros*: easier to develop, shows the user the actual file.
- *Cons*: the user could be waiting a while (for both Cloudinary, Make and the application to fulfil the requests).

**2.** We create a Preview component and give the user a visual preview (of what Make is about to send us) straight after our Cloudinary image is returned to our application.

- *Pros*: We can break up the loading states between Cloudinary and Make, we can create a more visually interesting preview display.
- *Cons*: Takes longer to develop, what the user sees in the app may be slightly different to what Make sends back (especially since this template is using generative shapes).

> For me, I see option 2 as a no brainer - I don’t want a user to be sitting and looking at a loading spinner for 10s, and it also means that we can do some visually interesting effects with the overlay.

For our Preview we will be doing the following:
- Creating our component
- Calculating our preview container so that it always fits to the space

### Creating our component

In our `Preview` directory, create a new `index.js` file and drop the following in

```jsx
// components/Preview/index.js

import './styles.css'
import { useAppState } from "../../providers/appState";
import { ReactComponent as Icon } from '../../assets/icon.svg'

const Preview = () => {
  const {
    imageUrl,
  } = useAppState();

  return (
    <div className={`inner ${imageUrl ? 'uploaded' : 'blank'}`}>
    <div className="Preview">
      <Icon />
      <div className="preview-container">
        {imageUrl && <img alt="avatar" src={imageUrl} />}
      </div>
    </div>
    </div>
  )
}

export default Preview;
```

We can add our CSS into our `styles.css` file in that same directory.

[Click here to view and copy the Preview CSS](https://github.com/makecm/photo-booth-app/blob/main/src/components/Preview/styles.css)

<br/>

Finally, we can add our `Shapes` component into our `Preview` directory. With this component all of the generated assets will have their own unique touch to them.

```jsx
// components/Preview/shapes.js

const Shapes = () => {
  function getRandomLength() {
    return Math.floor(Math.random() * 500 + 100);
  }
  function getRandomGap() {
    return Math.floor(Math.random() * 500 + 900);
  }

  return (
    <div style={{ overflow: 'hidden' }}>
      <svg
        className="svg-shapes"
        xmlns="http://www.w3.org/2000/svg"
        version="1.1"
        viewBox="100 100 600 600"
        preserveAspectRatio="xMidYMid slice"
      >
        {[0, 1].map((item) => (
          <circle
            key={item}
            r={Math.floor(Math.random() * 500) + 100}
            cx={Math.floor(Math.random() * 500)}
            cy={Math.floor(Math.random() * 500)}
            strokeWidth={Math.floor(Math.random() * 1000 + 75)}
            strokeDasharray={`${getRandomLength()} ${getRandomGap()}`}
          />
        ))}
      </svg>
      <svg style={{ pointerEvents: 'none' }}>
        <defs>
          <linearGradient id="bggrad" x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" style={{ stopColor: '#EF6690' }} />
            <stop
              offset="100%"
              style={{ stopColor: '#FF9E90' }}
            />
          </linearGradient>
        </defs>
      </svg>
    </div>
  );
};

export default Shapes;
```

And we can then import our `Shapes` into our `Preview`.

```jsx
import './styles.css'
import { useAppState } from "../../providers/appState";
import { ReactComponent as Icon } from '../../assets/icon.svg'
import Shapes from './Shapes'

const Preview = () => {
  const {
    imageUrl,
  } = useAppState();

  return (
    <div className={`inner ${imageUrl ? 'uploaded' : 'blank'}`}>
    <div className="Preview">
      <Icon />
      <div className="preview-container">
        {imageUrl && <img alt="avatar" src={imageUrl} />}
      </div>
      <Shapes />
    </div>
    </div>
  )
}

export default Preview;
```

Finally, we can add our `Preview` into our `App.js`.

```jsx
import './App.css';
import { ReactComponent as Icon } from './assets/icon.svg'

import Uploader from './components/Uploader'
import Preview from './components/Preview';

function App() {
  ...

  return (
    <div className="App">
      <header>
        <div>
          <Icon />
          <h1>React Photo Booth</h1>
        </div>
      </header>
      <div className="container">
        {!imageUrl && (<Uploader />)}
        <Preview />

        {imageUrl && (
          <div className="controlPanel">
            <a
              className={`download ${isGenerating ? 'disabled' : 'false'}`}
              target="_blank"
              rel="noreferrer noopener"
              href={generatedAvatar && generatedAvatar}
            >
              {isGenerating && (
                <Spinner styles={{ marginRight: '1rem' }} size="small" />
              )}
              {isGenerating ? "Generating..." : "Download"}
            </a>
          </div>
        )}
      </div>
    </div>
  );
}

export default App;
```

Our Preview is there but it will look a bit mangled, so let’s make it better…

![Broken preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23xr5npmb26h7k9w40s2.png)
 
### Calculating our preview size

To make our preview better we're going to calculate the size of it dynamically so that it will always fit in the available space of its parent container.

For that we’re actually going to be creating a custom hook to give us the correct CSS transform controls to match our browser size.

Firstly let’s jump over to the `appState` and we’re going to create a new `const` called `previewSize`. Inside `previewSize` we will create an object for our size.

```jsx
// providers/appState.js

const previewSize = {
  width: 1080,
  height: 1080,
}

const value = {
  ...
  previewSize,
};

```

We’ll then create a new file in our `Preview` directory called `usePreviewSize.js`.  It will allow us to send it the `ref` of an element and with that it will return us some calculated results based on the `previewSize` it consumes from our `useAppState()` hook.

```jsx
// components/Preview/usePreviewSize.js

import { useEffect, useState } from "react";

import { useAppState } from '../../providers/appState'

export function usePreviewSize(previewRef) {
  const [calcSize, setCalcSize] = useState(null)

  const {
    previewSize,
  } = useAppState()

  useEffect(() => {
    function fitPreview() {
      const pixelH = previewSize.height,
        pixelW = previewSize.width,
        containerH = previewRef.current.clientHeight,
        containerW = previewRef.current.clientWidth,
        heightRatio = containerH / pixelH,
        widthRatio = containerW / pixelW,
        fitZoom = Math.min(heightRatio, widthRatio)

      setCalcSize({
        pixelW: pixelW,
        pixelH: pixelH,
        fitZoom: fitZoom,
      })
    } fitPreview()

    window.onresize = resize;

    function resize() {
      fitPreview()
    }
  }, [previewSize, previewRef])

  return calcSize
}
```

In our `Preview` component we can then do the following:
1. Setup our `ref` on our `.inner` div
2. Send it to our `usePreviewSize()` hook
3. Create an object of styles based on the calculations
4. Add that to our `.Preview` div

```jsx
import React, { useRef } from 'react';

import './styles.css'

import { useAppState } from "../../providers/appState";
import { usePreviewSize } from "./usePreviewSize"

import { ReactComponent as Icon } from '../../assets/icon.svg'
import Shapes from './Shapes'

const Preview = () => {
  const {
    imageUrl,
  } = useAppState();

  // 1 & 2
  const previewRef = useRef(null)
  const size = usePreviewSize(previewRef)

  // 3
  const calcStyles = {
    width: size && size.pixelW + 'px',
    height: size && size.pixelH + 'px',
    transform: size && `scale(${size.fitZoom}) translate(-50%, -50%)`,
    filter: imageUrl ? 'blur(0)' : 'blur(30px)',
  }

  return (
    <div className={`inner ${imageUrl ? 'uploaded' : 'blank'}`} ref={previewRef}>
    {/* 4 */}
    <div className="Preview" styles={calcStyles}>
      <Icon />
      <div className="preview-container">
        {imageUrl && <img alt="avatar" src={imageUrl} />}
      </div>
      <Shapes />
    </div>
    </div>
  )
}

export default Preview;
```

And voila! We’ve got a nicely sized preview (and even a cheeky blur effect when in the blank state)

![Final preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sz7bqc0yswvvdvuukszt.png)

---

## 6. Finishing Up

At this point, we’re mostly done! Give yourself a huge pat on the back, because while all of the components are quite simple, there can be a few little hairy issues to overcome.

This part is completely optional, but if you want to round it all out let’s add a button so that a user can start again if they’re not happy with the result.

### Creating our StartAgain button

Let’s first create a function that will reset all of our important state back to the initial values.

```jsx
// App.js

const startAgain = () => {
  setImageUrl(null);
  setProgress(null);
  setGeneratedAvatar(null);
};
```

Inside of our return we can then add our button.

```jsx
// App.js

return (
    <div className="App">
      <header>
        <div>
          <Icon />
          <h1>React Photo Booth</h1>
        </div>
        {imageUrl && (
          <button
            className="reset"
            onClick={function () {
              startAgain();
            }}>
            Try Again
          </button>
        )}
      </header>
      ...
    </div>
  );
```

**Congratulations! You’ve made it to the end 🎉🎉🎉.**

<br/>

{% youtube beXBkDTzcjg %}

<br/>

Thank you so much for following along and I hope you've learnt a few things along the way. Here are some helpful resources that might interest you moving forward:

{% github makecm/photo-booth-app no-readme %}

{% github makecm/photo-booth-template no-readme %}

[**Build a "Name Picker" app - Intro to React, Hooks & Context API**](https://egghead.io/courses/build-a-name-picker-app-intro-to-react-hooks-context-api-1ded)

Or check out the first Make guide on creating a PDF with Make and React.

{% link https://dev.to/jamesrplee/make-a-pdf-with-react-make-cm-and-avoid-the-pain-of-ongoing-service-management-part-1-2-2kef %}

{% link https://dev.to/jamesrplee/make-a-pdf-with-react-make-cm-and-avoid-the-pain-of-ongoing-service-management-part-2-2-dm6 %}

If you have any questions, got stuck somewhere or want to pass on some feedback, jump onto twitter and message me directly [@jamesrplee](https://twitter.com/jamesrplee) or you can also reach me at [@makecm_](https://twitter.com/makecm_).

**Happy Making 🚀**