Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event

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?



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?



This is what were building!

Check out the live version here!

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

GitHub logo

makecm
/
photo-booth-app

Simple React app to generate unique images with Cloudinary, Make.cm and React



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



Why a photo booth?

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

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

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.



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

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

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

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). To get started let’s go ahead create our app from the terminal.

$ 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: simple CSS reset
  • axios: to handle our API requests to Cloudinary and Make
  • react-device-detect: to determine our download procedures for mobile and desktop devices
  • dot-env: 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
$ 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).

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

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

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.

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

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.

yarn start

empty application

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 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.
// 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).

// 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



Building our component

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

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

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

// 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

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

// 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 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

To send our assets to our newly created bucket, we’ll be using the Cloudinary’s unsigned option for using the Upload API, 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

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.

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



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.

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.

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.

// 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



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

// 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

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.



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.

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



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.

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

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.

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

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.

// 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:
  3. customSize: specifies the size of our generated filed
  4. format: specifies the file type to be generated to
  5. 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.
  6. fileName: this can be whatever you want it to be
  7. We then call our make client (that we created and imported just before) and send our data to it.
  8. 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.

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.

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

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.

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.

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.

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.

// App.js

import { isMobile } from "react-device-detect";
// 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

// 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

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.

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

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.

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



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.

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

// 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
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



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.

// App.js

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

Inside of our return we can then add our button.

// 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 ???.

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 logo

makecm
/
photo-booth-app

Simple React app to generate unique images with Cloudinary, Make.cm and React

GitHub logo

makecm
/
photo-booth-template

A generative image template built for the Make a Photo Booth guide.

Build a “Name Picker” app – Intro to React, Hooks & Context API

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

If you have any questions, got stuck somewhere or want to pass on some feedback, jump onto twitter and message me directly @jamesrplee or you can also reach me at @makecm_.

Happy Making ?


Print Share Comment Cite Upload Translate
APA
James Lee | Sciencx (2024-03-28T10:47:09+00:00) » Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event. Retrieved from https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/.
MLA
" » Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event." James Lee | Sciencx - Thursday May 27, 2021, https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/
HARVARD
James Lee | Sciencx Thursday May 27, 2021 » Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event., viewed 2024-03-28T10:47:09+00:00,<https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/>
VANCOUVER
James Lee | Sciencx - » Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event. [Internet]. [Accessed 2024-03-28T10:47:09+00:00]. Available from: https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/
CHICAGO
" » Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event." James Lee | Sciencx - Accessed 2024-03-28T10:47:09+00:00. https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/
IEEE
" » Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event." James Lee | Sciencx [Online]. Available: https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/. [Accessed: 2024-03-28T10:47:09+00:00]
rf:citation
» Make a Photo Booth with React, Cloudinary & Make.cm to boost your next virtual event | James Lee | Sciencx | https://www.scien.cx/2021/05/27/make-a-photo-booth-with-react-cloudinary-make-cm-to-boost-your-next-virtual-event/ | 2024-03-28T10:47:09+00:00
https://github.com/addpipe/simple-recorderjs-demo