Creating a simple CV template with ReactJS

Cristóbal Díaz
10 min readApr 24, 2022

I created this simple site cristobal-diaz.web.app for deploying my CV using ReactJS, PDF Make and Material UI. All the site is deployed in Firebase Hosting and it’s automatically implemented using GitHub Actions.

Installing Dependencies

First of all, I created a basic React App using npx create-react-app and after that, I installed the Material UI and PDF Make dependencies using npm install [package]. My new package.json file has these dependencies:

"dependencies": {
"@material-ui/core": "^4.12.1",
"@material-ui/icons": "^4.11.2",
"@octokit/core": "^3.5.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"pdfmake": "^0.2.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-grid-carousel": "^1.0.1",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.2"
}

Creating a Project Structure

It’s easier to manage your code when it’s well organized and you separate it in folders that help you to fastly find where a file is. In this project, I simply let all the files generated by npx create-react-app in the src folder and I created a new file called Main.js where I started to render all my subcomponents.

// Main.js
import React from "react";
import CommonList from "./components/CommonList";
import { Brightness4, Brightness7, PictureAsPdf } from '@material-ui/icons';
import { Button, Chip, Paper, Grid } from '@material-ui/core';
import PersonalInformation from "./components/PersonalInformation";
import { personalData, sections } from "./Data";
import TagsList from "./components/TagsList";
import { gray } from "./assets/colors";
import { printDoc } from "./utils/generatePDF";
const { Octokit } = require("@octokit/core");
const Main = ({ onToggleDark, currentTheme }) => (
<>
<PrintButton currentTheme={currentTheme} />
<ToggleButton currentTheme={currentTheme} onToggleDark={onToggleDark} />
<PersonalInformation
{...personalData}
/>
<CommonList
{...sections[0]}
/>
<CommonList
{...sections[1]}
/>
<Grid container>
<Grid item xs={12} sm={6}>
<CommonList
{...sections[2]}
/>
</Grid>
<Grid item xs={12} sm={6}>
<CommonList
{...sections[3]}
/>
</Grid>
</Grid>
<Grid container style={{ marginBottom: '70px' }}>
<Grid item xs={12} sm={6}>
<CommonList
{...sections[4]}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TagsList
{...sections[5]}
/>
</Grid>
</Grid>
<UpdatedFooter />
</>
);
export default Main;const ToggleButton = ({ currentTheme, onToggleDark }) => {
return (
<Chip
color="primary"
className="float-button-2"
id="dark-toggle-btn"
component={Paper}
icon={currentTheme.palette.type === "dark" ?
<Brightness7 style={{ color: gray[800] }} /> :
<Brightness4 />
}
label={
<Button
onClick={onToggleDark}
style={{ color: currentTheme.palette.type === "dark" ? gray[800] : "#fff" }}
>
Toggle Theme
</Button>
}
/>
)
}
const PrintButton = ({ currentTheme }) => {
const printPdf = () => {
printDoc({ personalData, sections });
}
return (
<Chip
color="secondary"
className="float-button"
id="print-cv-btn"
component={Paper}
icon={<PictureAsPdf style={{ color: currentTheme.palette.type === "dark" ? gray[800] : "#fff" }} />}
label={<Button onClick={printPdf} style={{ color: currentTheme.palette.type === "dark" ? gray[800] : "#fff" }}>Save as PDF</Button>}
/>
)
}
const UpdatedFooter = () => {
const [version, setVersion] = React.useState("");
const getVersion = async () => {
const token = process.env.REACT_APP_GITHUB_TOKEN;
const githubAPI = "https://api.github.com";;
const octokit = new Octokit({ auth: token });
const owner = "crismatters";
const repo = "cv";
try {
var tagsURI = `${githubAPI}/repos/${owner}/${repo}/commits`;
const response = await octokit.request(` /* width: 100%; */
GET ${tagsURI}`);
setVersion(response.data[0].commit.committer.date);
} catch (e) {
console.log(e);
setVersion((new Date().toISOString()))
}
}
React.useEffect(() => {
getVersion();
}, [])
return (
<Grid component={Paper} id="version-footer">
<b>This is my most recent CV version </b>
and it was updated at {version.split("T")[0]}.
<br />
<i>Cristóbal Díaz</i>
</Grid>
)
}

In the same folder, I created a second file called Data.js where I put all the information that will be shown in the CV site structured in JSON format. This file contains 6 objects that are exported in a list called sections, which makes reference to each of the sections displayed in the site.

// Data.js 
import { LinkedIn, Mail, GitHub, LocationCity, School, Book, Language, Cloud, Archive, TrendingUp } from "@material-ui/icons";
export const personalData = {
image: 'https://drive.google.com/uc?export=view&id=1DgiCuvXNeAvIQ0GZXILIaUWrSyjfNiAY',
name: 'Cristóbal Silva Díaz',
title: 'Systems Engineer',
contacts: [
{ icon: <Mail color="primary" />, value: 'crismatters@outlook.com', mail: true },
{ icon: <Mail color="primary" />, value: 'crismatters.sls@gmail.com', mail: true },
{ icon: <LocationCity color="primary" />, value: 'Aguascalientes, Mexico', location: true },
{ icon: <LinkedIn color="primary" />, value: 'linkedin.com/in/crismatters' },
{ icon: <GitHub color="primary" />, value: 'github.com/crismatters' },
{ icon: <Language color="primary" />, value: 'crismatters.medium.com' }
]
};
const experiencesList = {
type: 'common-list',
title: 'Experiences',
icon: <Archive />,
items: [
{
title: 'Technical Solutions Engineer',
authority: 'Google',
description: `Support Engineer, with specialization on Infrastructure as a Service and Security Products at Google Cloud Platform.
Hands on experience with virtualization, authorization and perimeter control services. Support of Compute Engine, Kubernetes Engine, VPC Service Controls, IAM services.`,
authorityWebSite: 'https://about.google/',
meta: '2021.07 - Present',
},
{
title: 'DevOps Engineer',
authority: 'Softtek',
description: `DevOps Engineer focused in configuration management automation
and containerization of applications. In the area I also gave support to
web applications development using MongoDB, React JS, Node JS and
Express JS. Some tools used are RedHat Ansible, SaltStack, Chef Infra, Kubernetes and Gitlab CI.`,
authorityWebSite: 'https://www.softtek.com/',
meta: '2019.12 - 2021.07',
},
]
};
const educationList = {
title: 'Education',
type: "common-list",
icon: <School color="primary" />,
items: [
{
title: 'Information Sytems Engineering',
authority: 'Universidad Politécnica de Aguascalientes',
authorityWebSite: 'https://upa.edu.mx/',
meta: '2016.08 - 2020.04',
description: `Systems Engineering specialized in networking.
Some knowledge acquired: web development, services and
users administration using Linux/Unix systems and Cisco
devices configuration.`
},
{
title: 'App Development Technician',
authority: 'CECyTEA Ferrocarriles',
authorityWebSite: 'https://cecytea.edu.mx/plantel-ferrocarriles/',
meta: '2013.08 - 2016.06',
description: `Technical career oriented to applications development and
networks design.`
},
]
};
const certificatesList = {
type: 'common-list',
title: 'Certificates',
description: '',
icon: <Book />,
items: [
{
title: 'Associate Cloud Engineer',
authority: 'Google Cloud',
authorityWebSite: 'https://www.credential.net/94244ca3-ccd5-45ba-afd1-c8179779888e?key=4a8b6fba59ad0bea41f8d47d5253d4d0494e4aecd4bf3e0386a7581d7949cc08&record_view=true',
meta: '2022.01 - 2024.01'
},
{
title: 'A Beginner\'s Guide to Linux Kernel Development (LFD103)',
authority: 'The Linux Foundation',
authorityWebSite: 'https://www.credly.com/badges/72edb0d8-7590-40c2-9936-0c92b622bf4b',
meta: '2021.05'
},
{
title: 'Cyber Security Foundation Professional Certificate (CSFPC)',
authority: 'CertiProf',
authorityWebSite: 'https://www.credly.com/badges/f867dcc7-ac36-4c3e-8232-9743785f4878',
meta: '2021.05'
},
{
title: 'Scrum Foundation Professional Certificate (SFPC)',
authority: 'CertiProf',
authorityWebSite: 'https://www.credly.com/badges/93926aab-d28f-4547-86dc-a7e18af0da78',
meta: '2021.05'
},
]
};
const coursesList = {
type: 'common-list',
title: 'Courses',
description: '',
icon: <TrendingUp />,
items: [
{
title: 'Architecting with Google Kubernetes Engine: Foundations',
authority: 'Coursera',
authorityWebSite: 'https://drive.google.com/file/d/1k7lgzDvD_F1Gbc81X7KSRBVUT3U-k-WA/view?usp=sharing',
meta: '2021.08'
},
{
title: 'Reliable Google Cloud Infrastructure: Design and Process',
authority: 'Coursera',
authorityWebSite: 'https://drive.google.com/file/d/1gxYXaNQM4dvaiEW9mDpm9vPM4fmhWvtf/view?usp=sharing',
meta: '2021.07'
},
{
title: 'Elastic Google Cloud Infrastructure: Scaling and Automationn',
authority: 'Coursera',
authorityWebSite: 'https://drive.google.com/file/d/1ddwszSWqwgOdFp2aRxwZnNMJuFoi3kkq/view?usp=sharing',
meta: '2021.07'
},
{
title: 'Essential Google Cloud Infrastructure: Core Services',
authority: 'Coursera',
authorityWebSite: 'https://drive.google.com/file/d/1csCAfDpZKWxNL1hs2x6kCr-w1fXWPoxM/view?usp=sharing',
meta: '2021.07'
},
{
title: 'Certified Kubernetes Administrator',
authority: 'Linux Academy',
authorityWebSite: 'https://drive.google.com/file/d/13uUfvzGjuABAfmXzcnv1jYumwopj-e12/view?usp=sharing',
meta: '2020.11'
},
{
title: 'RedHat Certified Specialist in Ansible Automation',
authority: 'Linux Academy',
authorityWebSite: 'https://drive.google.com/file/d/19fZdkkyBALFPA7R_TGfmLEvKRKnpVRuI/view?usp=sharing',
meta: '2020.08'
},
{
title: 'RedHat Certified Engineer',
authority: 'Linux Academy',
authorityWebSite: 'https://drive.google.com/file/d/1Mr-osApoMnZjMBpBx34z--u3zAS6yUhJ/view?usp=sharing',
meta: '2020.07'
},
{
title: 'SaltStack Certified Engineer',
authority: 'Linux Academy',
authorityWebSite: 'https://drive.google.com/file/d/1-G7L6h_CrGpiCwgAbmLTd0hU12RgBOvg/view?usp=sharing',
meta: '2020.02'
},
]
}
const languagesList = {
type: 'common-list',
title: 'Languages',
icon: <Language />,
items: [
{
authority: 'Spanish',
authorityMeta: 'Native'
},
{
authority: 'English',
authorityMeta: 'Fluent'
},
{
authority: 'German',
authorityMeta: 'Beginner'
},
]
};
const skillsList = {
title: 'Skills Proficiency',
type: "tags-list",
icon: <Cloud />,
items: [
"Linux", "Windows", "Wireshark", "NMap", "MySQL", "PostgreSQL", "MongoDB", "JavaScript",
"RedHat Ansible", "SaltStack", "Kubernetes", "GitlabCI", "Docker", "Google Cloud", "Azure",
"Jira", "ReactJS", "Agile", "Scrum", "SAFe DevOps"
]
};
export const sections = [experiencesList, educationList, certificatesList, coursesList, languagesList, skillsList];

The assets folder (inside of src) is a folder where I think that static content could be saved (such as images, styles and this kind of stuff). The colors.js file defines 5 objects that represents colors of my preference with different opacity and this helps to easily call these values basing on the selected theme.

export const blue = {
900: "#174EA6",
800: "#185ABC",
700: "#1967D2",
600: "#1A73E8",
500: "#4285F4",
400: "#669DF6",
300: "#8AB4F8",
200: "#AECBFA",
100: "#D2E3FC",
50: "#E8F0FE"
}
export const red = {
900: "#A50E0E",
800: "#B31412",
700: "#C5221F",
600: "#D93025",
500: "#EA4335",
400: "#EE675C",
300: "#F28B82",
200: "#F6AEA9",
100: "#FAD2CF",
50: "#FCE8E6"
}
export const yellow = {
900: "#E37400",
800: "#EA8600",
700: "#F29900",
600: "#F9AB00",
500: "#FBBC04",
400: "#FCC934",
300: "#FDD663",
200: "#FDE293",
100: "#FEEFC3",
50: "#FEF7E0"
}
export const green = {
900: "#0D652D",
800: "#137333",
700: "#188038",
600: "#1E8E3E",
500: "#34A853",
400: "#5BB974",
300: "#81C955",
200: "#A8DAB5",
100: "#CEEAD6",
50: "#E6F4EA"
}
export const gray = {
900: "#202124",
800: "#3C4043",
700: "#5F6368",
600: "#80868B",
500: "#9AA0A6",
400: "#BDC1C6",
300: "#DADCE0",
200: "#E8EAED",
100: "#F1F3F4",
50: "#F8F9FA"
}

Inside the src folder I created a subfolder called utils where I created a js file for generating the CV in a PDF format (generatePDF.js). This file imports all the dependencies and fonts from PDFMake and defines a structure for the content of the generated PDF file.

// generatePDF.js
import * as pdfmake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';
import * as colors from '../assets/colors';
export const printDoc = ({ personalData, sections }) => {
var content = [];
content.push(personalDataToContent(personalData));
sectionsToContent(sections, content);
var document = {
pageSize: "A4",
pageOrientation: "portrait",
pageMargins: [30, 30, 30, 30],
styles: {
header: {
fontSize: 18,
bold: true,
margin: [0, 0, 0, 10],
color: colors.blue[700]
},
subheader: {
fontSize: 16,
bold: true,
margin: [0, 10, 0, 5],
color: colors.gray[700]
},
itemTitle: {
fontSize: 14,
margin: [0, 0, 0, 10],
color: colors.blue[700]
},
itemSubtitle: {
fontSize: 12,
margin: [0, 0, 0, 10],
color: colors.red[700]
},
metadata: {
margin: [0, 0, 0, 10],
color: colors.yellow[700]
},
table: {
margin: [0, 5, 0, 15],
},
tableHeader: {
bold: true,
fontSize: 13,
color: colors.blue[700]
},
footer: {
bold: true,
fontSize: 11,
color: 'gray'
},
contact: {
fontSize: 13,
color: colors.blue[700]
}
},
content
};
pdfmake.vfs = pdfFonts.pdfMake.vfs;
pdfmake.createPdf(document).download("CV Cristobal.pdf");
}
const sectionsToContent = (sections, mainDoc) => {
sections.forEach(section => {
if (section.type === "common-list") mainDoc.push(commonListToContent(section));
if (section.type === "tags-list") mainDoc.push(tagsListToContent(section));
});
}
const tagsListToContent = (section) => {
var content = {
layout: 'lightHorizontalLines',
style: "table",
table: {
body: [
[{ text: section.title, style: "header" }],
[{ stack: [{ ul: section.items }] }]
]
}
}
return content;
}
const commonListToContent = (section) => {
var content = {
layout: 'lightHorizontalLines',
style: "table",
table: {
body: [
[{ text: section.title, style: "header", colSpan: 3 }, {}, {}],
]
}
}
section.items.forEach(item => {
content.table.body.push([
{
stack: [
{
type: "none",
ul: [
{ text: item.title ? item.title : item.authority, style: 'itemTitle' },
{ text: item.authorityMeta ? item.authorityMeta : item.authority, style: 'itemSubtitle', link: item.authorityWebSite }
]
}
]
},
{ text: item.description },
{ text: item.meta, style: "metadata" }
]);
});
return content;
}
const personalDataToContent = (personalData) => {
var content = {
style: "table",
table: {
widths: [300, 200],
body: [
[
{
stack: [
{
type: "none",
ul: [
{ text: personalData.name, style: 'header' },
{ text: personalData.title, style: 'subheader' }
]
}
]
},
{
stack: [
{
ul: personalData.contacts.map(contact => ({ text: contact.value, style: "contact", link: (!contact.mail && !contact.location ? "https://" + contact.value : null) }))
},
]
}
],
]
},
layout: 'noBorders'
}
return content;
}

Finally, inside the same folder, there is another important subfolder called components where I created the three main components of this simple app. The structure of the src folder is as below.

src
├── App.js
├── App.test.js
├── assets
│ └── colors.js
├── components
│ ├── CommonList.js
│ ├── PersonalInformation.js
│ └── TagsList.js
├── Data.js
├── index.css
├── index.js
├── Main.js
├── reportWebVitals.js
├── setupTests.js
└── utils
└── generatePDF.js
3 directories, 13 files

The Three Main Components

The first of the components is the CommonList.js file, where a list of elements is rendered. This list generates an Accordion component with a title and inside of it, all the elements of the list will be generated with a basic structure as this:

item: {  title: String // The title of the element  authority: String // The institution (appears like a subtitle) of the element.  description: String // The description of the element  authorityWebsite: String // A URL to create a link in the authority field  meta: String // A small text that appears in the bottom of the element (used for the dates in my CV)
}

The PersonalInformation.js file is maybe the simplest of the three components, since its only function is to show the header of the site with the website, mail, name and title of the person from the CV. The structure for generating this information is the shown below:

personalData: {  image: String // URL that points to an image (main profile picture)  name: String // Name of the person  title: String // Profesional title of the person  contacts: Array[<contact>]
}
contact: { icon: Object // A Material UI Icon Component value: Sting // The text to display in the contact info mail: Boolean // Is this contact info an e-mail address? location: Boolean // Is this contact info a location?
}

Finally, the TagsList.js file is the component with the simplest render, since its only function is to render a list of keywords with a Material UI Chip Component. This file reads only a list of keywords and for each of them generates a new Chip with a random color taken from the colors.js file.

Firebase Hosting and GitHub Actions

Before configuring GitHub Actions, a Firebase Project needs to be created from the Firebase Console. Once the project has been created, the Firebase CLI tools need to be installed in your workstation using npm install firebase-tools or following this document. Once Firebase Tools are installed, the firebase init command will help to configure the project selecting Hosting.

firebase init

Once this has been selected using space you need to select your project by selecting an existing project.

Finally, a public folder needs to be selected where all the static content is generated. ReactJS creates the build folder with this content after you execute npm build inside of your project folder.

After this, a firebase.json and a .firebaserc files will be created and with this, the project is ready to be deployed to firebase hosting using firebase deploy.

// firebase.json
{
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}

The .firebaserc file contains the information of your project.

{  "projects": {    "default": "my-project"  }}

In the main folder of this project, a folder structure was created following the GitHub documentation.

For this workflow, a Personal Access Token and a Firebase Token is needed.

# .github/workflows/deploy.yaml
name: Deploy to firebase
on:
push:
branches: ['master']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
- name: Setup the code
run: npm install
- name: Deploy to Firebase
uses: mohammed-atif/firebase-publish-react@v1.0
with:
firebase-token: ${{ secrets.FIREBASE_TOKEN }}
env:
REACT_APP_GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}

The Code

You can find this project in my GitHub Repository.

Thanks for reading!

--

--