Creating a simple CV template with ReactJS
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.js3 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.

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!