Make a PDF with React & Make.cm and avoid the pain of ongoing service management [Part 2/2]
James Lee
Posted on March 25, 2021
If it's your first time here check out Part 1 of this series here.
Make a PDF with React & Make.cm and avoid the pain of ongoing service management [Part 1/2]
James Lee for Make.cm ・ Mar 25 '21
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
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
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
$ yarn start
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;
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;
}
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;
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;
}
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;
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;
}
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;
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>
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();
}
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.
The structure of our URL is as follows.
https://api.make.cm/make/t/[template-id]/sync
-
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
orasync
function generateCertificate(e) => {
e.preventDefault();
const url = [MAKE-API-URL]
}
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],
}
}
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)
- name (from
-
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
}
}
}
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);
});
}
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": {}
}
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)
});
}
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>
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)
});
}
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>
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
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;
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;
}
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;
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
Add a file on the root called .env.development
REACT_APP_MAKE_KEY = [YOUR_MAKE_KEY];
REACT_APP_MAKE_URL = [YOUR_MAKE_URL];
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,
};
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.
makecm / certificate-app
A simple react application to generate a PDF certificate using Make.cm
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
Posted on March 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.