DEV Community

Cover image for Make a PDF with React & Make.cm and avoid the pain of ongoing service management [Part 2/2]
James Lee for Make.cm

Posted on • Updated on • Originally published at make.cm

Make a PDF with React & Make.cm and avoid the pain of ongoing service management [Part 2/2]

If it's your first time here check out Part 1 of this series here.


In Part 1 we created our certificate template and imported it into Make. With that done we can focus on building our certificate generator app.

3. Creating our App

Okay refresher time. What are we making again?

A react app with:

  • A form to capture the name and course
  • A function to generate our certificate
  • A preview of our PDF, once generated

app-preview

We're making this

For our App structure we're building the following. Our styling just be handled with standard CSS.

/certificate-app
  /src
    /components
      /Form
        index.js
        styles.css
      /Header
        index.js
        styles.css
      /Preview
        index.js
        styles.css
    App.css
    App.js
    index.js
Enter fullscreen mode Exit fullscreen mode

I'd suggest going ahead and creating these files, we'll loop back on them later.

Prepping our App

For our App let's get started by installing the necessary dependencies and then spinning up our server.

$ yarn add axios react-pdf
Enter fullscreen mode Exit fullscreen mode
$ yarn start
Enter fullscreen mode Exit fullscreen mode

Our dependencies:

  • Axios: will handle our POST request to Make
  • react-pdf: will allow us to render the resulting PDF that Make sends us to the front end

Our App.js will be structured like this.

I've already setup a simple useState hook to capture the formData (so you don't need to!) that we'll hook up to our <Form/> component that we'll create in the next step.

import { useState } from 'react';
import axios from 'axios';
import 'minireset.css';

import './App.css';

// import Header from './components/Header'
// import Form from './components/Form'
// import Preview from './components/Preview'

function App() {
  const [formData, setFormData] = useState({
    name: '',
    course: '',
  });

  return (
    <div className="App">
      <div className="container">
        {/* <Header /> */}

        <section>
          <div>
            {/* FORM */}
            <button type="button">Make my certificate</button>
          </div>
          <div>
            {/* PREVIEW */}
            {/* DOWNLOAD */}
          </div>
        </section>

        <footer>Built with React and Make.cm</footer>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's get some base styles out of the way, so in App.css remove what's in there and paste this in.

@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500&family=Poppins:wght@800&display=swap');

:root {
  --blue: #0379ff;
  --light-blue: #9ac9ff;
  --dark-blue: #0261cc;
  --white: #fff;
  --black: #101820;
  --blackAlpha: #10182010;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
    'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
    'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-size: 16px;
}

.App {
  font-family: 'IBM Plex Sans';
}

.container {
  width: 100%;
  margin: 0 auto;
}

@media (min-width: 1024px) {
  .container {
    width: 1024px;
  }
}

section {
  width: 100%;
  display: grid;
  grid-template-columns: 2fr 1fr;
  padding-left: 8.5rem;
}

button {
  font-size: 1.25rem;
  background-color: var(--blue);
  border-radius: 6px;
  border: 0;
  padding: 1rem 2rem;
  font-weight: bold;
  color: var(--white);
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

footer {
  padding-top: 4rem;
}

.download {
  background-color: var(--dark-blue);
  color: white;
  font-size: 1.25rem;
  border-radius: 6px;
  border: 0;
  padding: 1rem 2rem;
  font-weight: bold;
  margin-top: 2rem;
  text-align: right;
  text-decoration: none;
}
Enter fullscreen mode Exit fullscreen mode

While we're at it let's create the <Header /> component. Go to your components/Header/index.js and paste the following

import './styles.css';

const Header = () => (
  <header>
    <Icon />
    <h1>Certificate Maker</h1>
  </header>
);

const Icon = () => (
  <svg
    width="99"
    height="139"
    viewBox="0 0 99 139"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path d="M0 0H99V138.406L52.1955 118.324L0 138.406V0Z" fill="#0379FF" />
    <path
      d="M25.4912 83.2515C25.4912 79.4116 27.0222 75.7289 29.7474 73.0137C32.4727 70.2985 36.1689 68.7731 40.0229 68.7731C43.877 68.7731 47.5732 70.2985 50.2984 73.0137C53.0236 75.7289 54.5546 79.4116 54.5546 83.2515M40.0229 59.724C40.0229 55.8841 41.5539 52.2014 44.2791 49.4862C47.0044 46.7709 50.7006 45.2455 54.5546 45.2455C58.4087 45.2455 62.1049 46.7709 64.8301 49.4862C67.5553 52.2014 69.0863 55.8841 69.0863 59.724V83.2515"
      stroke="#fff"
      strokeWidth="10.6193"
    />
  </svg>
);

export default Header;
Enter fullscreen mode Exit fullscreen mode

And then the same in components/Header/styles.css

header {
  display: flex;
  justify-content: flex-start;
}

h1 {
  font-family: 'Poppins';
  color: var(--blue);
  padding: 2rem;
  font-size: 2.5rem;
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to uncomment the import and the component for your new Header in your App.js.

Creating the form component

Our <Form/> component will capture the custom name and course inputs that will be sent to Make. We will use our formData and setFormData hook from App.js to set the initial state and handle any changes to that state.

Paste the following in your src/components/Form/index.js file.

import './styles.css';

const Form = ({ formData, setFormData }) => {
  function handleChange(evt) {
    const value = evt.target.value;
    setFormData({
      ...formData,
      [evt.target.name]: value,
    });
  }

  return (
    <form>
      <div>
        <label htmlFor="name">Awarded to</label>
        <input
          type="text"
          id="name"
          name="name"
          placeholder={formData.name === '' && 'Name Surname'}
          value={formData.name}
          onChange={handleChange}
        />
      </div>
      <div>
        <label htmlFor="course">for completing</label>
        <input
          id="course"
          name="course"
          placeholder={
            formData.course === '' && 'Creating PDFs with React & Make.cm'
          }
          value={formData.course}
          onChange={handleChange}
        />
      </div>
    </form>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

It'll look pretty ugly so let's add some styles at src/components/Form/styles.css

label {
  font-size: 1.2rem;
  display: block;
  margin-bottom: 1rem;
}

input {
  border: 0;
  padding: 0;
  display: block;
  width: 100%;
  font-size: 2rem;
  margin-bottom: 2rem;
  color: var(--blue);
}

input:focus {
  outline: none;
}

input::placeholder {
  color: var(--light-blue);
}

input:focus::placeholder,
input:active::placeholder {
  color: var(--blue);
}

input[name='name'] {
  font-family: 'Poppins';
  font-size: 3rem;
}

input[name='course'] {
  font-family: 'IBM Plex Sans';
  font-weight: 500;
  font-size: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

Finally lets uncomment the import and the component for your Form in your App.js and pass in formData and setFormData so we can move our state around.

import { useState } from 'react';
import axios from 'axios';
import 'minireset.css';

import './App.css';

import Header from './components/Header';
import Form from './components/Form';
// import Preview from './components/Preview'

function App() {
  const [formData, setFormData] = useState({
    name: '',
    course: '',
  });

  return (
    <div className="App">
      <div className="container">
        <Header />

        <section>
          <div>
            <Form formData={formData} setFormData={setFormData} />
            <button type="button">Make my certificate</button>
          </div>
          <div>
            {/* Preview */}
            {/* Download */}
          </div>
        </section>

        <footer>Built with React and Make.cm</footer>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Creating the request

Now that we've got our <Form/> working lets setup our request to Make. For this we'll do the following

  • Create the onClick event
  • Create our request
  • Handle some state management
  • Be able to do something with the generated certificate

On our <button> in App.js let's set an onClick event that triggers a function called generateCertificate.

<button type="button" onClick={generateCertificate}>
  Make my certificate
</button>
Enter fullscreen mode Exit fullscreen mode

For our generateCertificate function we can do the following.

We pass in the event (e) and prevent the default action.

function generateCertificate(e) {
  e.preventDefault();
}
Enter fullscreen mode Exit fullscreen mode

We need to then setup the various const's for our request to Make.

For our request we will be performing a synchronous POST request.

The request can be handled synchronously because the template that we will be generating will resolve in under 30 sec.

If we were generating something that was computationally heavier (ie. a PDF booklet with a lot of images or generating a video from our template) we would need to use Make's async API. But in this case a sync request is fine.

URL

To find your API URL navigate to your imported certificate in Make and copy the apiUrl from the API playground.

api playground in Make app

The structure of our URL is as follows.

https://api.make.cm/make/t/[template-id]/sync
Enter fullscreen mode Exit fullscreen mode
  • make: As we are calling the Make API
  • t: To specify a template
  • [template-id]: To specify the id of the template to generate
  • sync: The request type to perform (ie. sync or async
function generateCertificate(e) => {
  e.preventDefault();

  const url = [MAKE-API-URL]
}
Enter fullscreen mode Exit fullscreen mode

Headers

We can then specify our headers for our request. In this case we just need to specify the Content-Type and our X-MAKE-API-KEY.

The Make API key can also be found from the API playground of your imported template (see in the above photo). If you want you can generate a new one.

function generateCertificate(e) => {
  e.preventDefault();

  const url = [MAKE_API_URL];

  const headers = {
    'Content-Type': 'application/json',
    'X-MAKE-API-KEY': [MAKE_API_KEY],
  }
}
Enter fullscreen mode Exit fullscreen mode

Data

Now let's specify the body of our request. In this case we want an A4 PDF certificate with the name and course that is encapsulated in our formData state, and then we add our date to the request as well.

The body structure for the Make API is split up into 4 areas that will be used to generate our certificate:

  • format (required): The file type to be generated. In our case pdf.
  • size or customSize (required): The width, height and unit that the final generated file will come out as. In this case A4
  • data: A custom object of data that will be available for your template to consume via the custom window object templateProps. For our certificate we will be sending the following
    • name (from formData)
    • course (from formData)
    • date (calculated from today's date)
  • postProcessing: A set of parameters to augment the asset, post generation. For our PDF we want to optimize it for our users.
function generateCertificate(e) => {
  e.preventDefault();

  const url = [MAKE_API_URL];

  const headers = {
    'Content-Type': 'application/json',
    'X-MAKE-API-KEY': [MAKE_API_KEY],
  }

  const data = {
    size: 'A4',
    'format': 'pdf',
    'data': {
      ...formData,
      date: new Date().toDateString().split(' ').slice(1).join(' ')
    },
    'postProcessing': {
      optimize: true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With all of our consts ready we can create our POST request with axios.

function generateCertificate(e) => {
  e.preventDefault();

  const url = [MAKE_API_URL];

  const headers = {
    'Content-Type': 'application/json',
    'X-MAKE-API-KEY': [MAKE_API_KEY],
  }

    const data = {
    size: 'A4',
    'format': 'pdf',
    'data': {
      ...formData,
      date: new Date().toDateString().split(' ').slice(1).join(' ')
    },
    'postProcessing': {
      optimize: true
    }
  }

  axios.post(url, data, {
    headers: headers
  })
  .then((response) => {
    console.log(response)
  }, (error) => {
    console.log(error);
  });
}
Enter fullscreen mode Exit fullscreen mode

Test out the event by clicking the button.

Give it a second to generate and check your console and you should have a result like this. Your newly made PDF is the resultUrl in the data object.

{
    "data": {
        "resultUrl": "https://exports.make.cm/d012845b-b116-4468-ab00-e2c79b006e21.pdf?AWSAccessKeyId=ASIATSPIFSU4EQL7GW6O&Expires=1615921029&Signature=pf3X%2FYOAjWKXtkfnG49U%2BjGVwxI%3D&x-amz-security-token=IQoJb3JpZ2luX2VjENf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0yIkgwRgIhAK98rku7U6iKoY3TJ9xUJZGh9%2ByL%2By99JT96sCoP8ZZzAiEAvMdU%2F%2FNTCSygV28zNx4m5xe4UgHxbFyC%2BWKDKt92YLAq0QEIEBAAGgwyNDU4MzY5MTE5MjgiDK5SSXVBnx5YHlpkQCquAcdfUJX7cnCvxHwTCPzJLeJZB1Yg5x5nsjHI9DC63TJ5LXbaDLWbMllosnBMJ3u0%2BjUNuvvxkIt%2Bw5mY%2FNrYytY0%2BXVjukcbZO%2BZ0gx8kaTtVRJBrKP5TCwDHZu20%2FpKckR8muPL3OuNewH5g1BEkCqls6w72qdz7aaxEsvGwV5wzeVLJdotgQy6LQ%2FlcsyLqG7RiGyZouahjvnijpbIRYtfeTI5qXPCLtUl0SyfaDC8rcGCBjrfAXZicx8A6iCEhLBQwF8LtgPqgBQlTcwfapNQQ1gnUwlSnCBm6Lsm0kpsFnqHT0ockINp2STRJkkovS7lkKgOIP49ApSk9MRYJFy%2F8%2BfDeYToQ9K3y0aS2qY7HHigQwAX1dgjmWpL27aZEXriG%2F2uxcjEXwKzWySFNkQjlzVuTVHA3rucrMnZfuP3fPH82A10nce%2BTNx%2BLXKZgZz8rv50J3eQwLBVcq3phIGmnY%2B5meivIAqOCL1iYrMRqTZfNLdAxOqWdlMiGinYKGUZufsdpfr0xuq73unvmQ3MuDfDCDA%3D",
        "requestId": "d012845b-b116-4468-ab00-e2c79b006e21"
    },
    "status": 200,
    "statusText": "",
    "headers": {
        "content-length": "1055",
        "content-type": "text/plain; charset=utf-8"
    },
    "config": {
        "url": "https://api.make.cm/make/t/c43e9d1a-f0aa-4bf7-bf73-6be3084187d8/sync",
        "method": "post",
        "data": "{\"size\":\"A4\",\"format\":\"pdf\",\"data\":{\"name\":\"Name Surname\",\"course\":\"Creating things\",\"date\":\"Mar 16 2021\"}}",
        "headers": {
            "Accept": "application/json, text/plain, */*",
            "Content-Type": "application/json",
            "X-MAKE-API-KEY": "47bad936bfb6bb3bd9b94ae344132f8afdfff44c"
        },
        "transformRequest": [
            null
        ],
        "transformResponse": [
            null
        ],
        "timeout": 0,
        "xsrfCookieName": "XSRF-TOKEN",
        "xsrfHeaderName": "X-XSRF-TOKEN",
        "maxContentLength": -1,
        "maxBodyLength": -1
    },
    "request": {}
}
Enter fullscreen mode Exit fullscreen mode

Congrats! You just performed your first request outside of Make! 🎉

There is a bit of a lag between clicking the button and getting a result so let's set up some really simple state management so we give our users at least some feedback.

Let's set up a simple loading state for when we send our request.

In App.js create the following useState hook caleed isLoading.

In our generateCertificate function we'll set isLoading to true when our function fires and then false when our request finishes (or our request errors for whatever reason).

const [formData, setFormData] = useState({
  name: '',
  course: '',
});
const [isLoading, setIsLoading] = useState(false)

const generateCertificate = (e) => {
  e.preventDefault();

  setIsLoading(true)

    ...

  axios.post(url, data, {
    headers: headers
  })
  .then((response) => {
    console.log(response);
    setIsLoading(false)
  }, (error) => {
    console.log(error);
    setIsLoading(false)
  });
}
Enter fullscreen mode Exit fullscreen mode

We'll update the button in our return so it disables when isLoading is true.

<button type="button" disabled={isLoading} onClick={generateCertificate}>
  {isLoading ? 'Making...' : 'Make my certificate'}
</button>
Enter fullscreen mode Exit fullscreen mode

Console logging is great but let's actually put that certificate somewhere.

We can create another hook called certificate to capture our result.

// App.js

const [formData, setFormData] = useState({
  name: '',
  course: '',
});
const [isLoading, setIsLoading] = useState(false)
const [certificate, setCertificate] = useState(null)

const generateCertificate = (e) => {
  ...

  axios.post(url, data, {
    headers: headers
  })
  .then((response) => {
    setIsLoading(false)
    setCertificate(response.data.resultUrl)
  }, (error) => {
    console.log(error);
    setIsLoading(false)
  });
}
Enter fullscreen mode Exit fullscreen mode

Finally let's create a simple Download button for when the result is available.

<div className="App">
  <div className="container">
    <Header />

    <section>
      <div>
        <Form formData={formData} setFormData={setFormData} />
        <button
          type="button"
          disabled={isLoading}
          onClick={generateCertificate}
        >
          {isLoading ? 'Making...' : 'Make my certificate'}
        </button>
      </div>
      <div>
        {/* Preview (optional) */}
        {certificate && (
          <a
            className="download"
            target="_blank"
            rel="noreferrer"
            href={certificate}
          >
            Download
          </a>
        )}
      </div>
    </section>

    <footer>Built with React and Make.cm</footer>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Isn't it a thing of beauty! 🥰

Creating the preview component (optional)

This step is completely optional but I think it rounds out the whole application. We're going to use react-pdf to create a preview of our certificate once it has generated.

We should've installed react-pdf at the start, but if you haven't yet you can just run this in your terminal.

yarn add react-pdf
Enter fullscreen mode Exit fullscreen mode

For our <Preview/> component we're going to be passing the certificate and isLoading props into our component and when the certificate has been generated react-pdf will create a preview of that.

Paste the following into components/Preview/index.js.

import { Document, Page, pdfjs } from 'react-pdf';
import './styles.css';

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;

const Preview = ({ certificate, isLoading }) => {
  return (
    <div className="pdf">
      {!certificate && (
        <div className="loader">
          {isLoading ? 'Making...' : 'Make one and see!'}
        </div>
      )}
      {certificate && (
        <Document file={certificate} loading="Loading...">
          <Page pageNumber={1} />
        </Document>
      )}
    </div>
  );
};

export default Preview;
Enter fullscreen mode Exit fullscreen mode

For our styles in components/Preview/styles.css

.pdf {
  border: 0.25rem solid var(--black);
  border-radius: 1rem;
  box-shadow: 1rem 1rem 0 var(--blackAlpha);
  padding-bottom: 137.3%;
  position: relative;
  overflow: hidden;
  margin-bottom: 3rem;
}

.pdf div {
  position: absolute;
  font-weight: 500;
}

.pdf .loader {
  padding: 1.5rem;
}

.react-pdf__Page__canvas {
  width: 100% !important;
  height: initial !important;
}
Enter fullscreen mode Exit fullscreen mode

And then in the App.js we can import it and pass the props down.

import { useState } from 'react';
import axios from 'axios';
import 'minireset.css';

import './App.css';

import Header from './components/Header'
import Form from './components/Form'
import Preview from './components/Preview'

function App() {
  ...

  return (
    <div className="App">
      <div className="container">
        <Header />

        <section>
          <div>
            <Form formData={formData} setFormData={setFormData} />
            <button type="button">Make my certificate</button>
          </div>
          <div>
            <Preview certificate={certificate} isLoading={isLoading} />
            {certificate && (
              <a
                className="download"
                target="_blank"
                rel="noreferrer"
                href={certificate}
              >
                Download
              </a>
            )}
          </div>
        </section>

        <footer>
          Built with React and Make.cm
        </footer>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Cleaning it up

The only thing left to do at this stage is secure my Make key and API URL.

For this we can use dotenv just so we're not committing keys into Github and beyond. While it won't stop people from being able to see this info on the client I think it just keeps the surface area a lot smaller.

yarn add dotenv
Enter fullscreen mode Exit fullscreen mode

Add a file on the root called .env.development

REACT_APP_MAKE_KEY = [YOUR_MAKE_KEY];
REACT_APP_MAKE_URL = [YOUR_MAKE_URL];
Enter fullscreen mode Exit fullscreen mode

And then in your App.js you can point to your environment variables like so

const url = process.env.REACT_APP_MAKE_URL;

const headers = {
  'Content-Type': 'application/json',
  'X-MAKE-API-KEY': process.env.REACT_APP_MAKE_KEY,
};
Enter fullscreen mode Exit fullscreen mode

If you make any changes to your .env files remember to restart your local server.

And, that's it! 🙌

Thank you so much for following on with the first of many guides about how to use Make.cm and get the most out of the API.

I know it was a long one, but I didn't want to give you some click baity title about CREATING A PDF IN UNDER 5 MIN. If you missed it in Part 1 here are some links to the resources that I used to Make this application.

GitHub logo makecm / certificate-app

A simple react application to generate a PDF certificate using Make.cm

GitHub logo makecm / certificate-template

A simple certificate template that can be forked and imported into Make.cm

If you have any questions or issues along the way let me know at @jamesrplee on Twitter and I'll be happy to help you out.

Thank you so much and happy Making,

James

Top comments (0)