├── .gitignore
├── .netlify
├── README.md
├── examples
└── resume.pdf
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── scripts
└── export.js
├── src
├── components
│ ├── canvas.js
│ ├── daterange.js
│ ├── example.js
│ ├── page.js
│ ├── skillstools.js
│ └── summary_line.js
├── index.js
└── registerServiceWorker.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # result output
4 | out/
5 |
6 | src/components/personal/
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
--------------------------------------------------------------------------------
/.netlify:
--------------------------------------------------------------------------------
1 | {"site_id":"25f02818-c760-40e3-9cba-b5058162549a","path":"build/"}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-resume 📄
2 | ==============
3 |
4 | > Generate resumes using [React](https://github.com/facebook/react), [`puppeteer`](https://github.com/GoogleChrome/puppeteer), and [`styled-components`](https://github.com/styled-components/styled-components).
5 |
6 | Inspired by: https://github.com/salomonelli/best-resume-ever
7 |
8 | **HTML preview:** https://react-resume.netlify.com/
9 |
10 | **PDF output:** [`examples/resume.pdf`](examples/resume.pdf)
11 |
12 | ## Included batteries:
13 |
14 | - [`create-react-app`](https://github.com/facebookincubator/create-react-app)
15 | + React
16 | + babel
17 | + webpack
18 | + eslint
19 | + and other cool stuff that `create-react-app` provides.
20 | - [`puppeteer`](https://github.com/GoogleChrome/puppeteer)
21 | - [`styled-components`](https://github.com/styled-components/styled-components)
22 |
23 | Usage
24 | =====
25 |
26 | 1. Clone this repository: `git clone https://github.com/dashed/react-resume.git`
27 |
28 | 2. Run `yarn install` (or `npm install`)
29 |
30 | 3. Run `create-react-app` in development mode: `yarn start` (or `npm start`)
31 |
32 | 4. Open http://localhost:3000 to view it in the browser.
33 |
34 | 5. Edit `src/`
35 |
36 | 6. Export PDF: `yarn pdf` (or `npm run pdf`)
37 |
38 | 7. Generated resume is in: `out/resume.pdf`
39 |
40 | License
41 | =======
42 |
43 | MIT.
44 |
--------------------------------------------------------------------------------
/examples/resume.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dashed/react-resume/408fd4d814d8811266f3e872eca716faad4252d2/examples/resume.pdf
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-resume",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "invariant": "^2.2.2",
7 | "normalize.css": "^7.0.0",
8 | "react": "^16.0.0",
9 | "react-dom": "^16.0.0",
10 | "react-scripts": "^1.1.0",
11 | "sanitize.css": "^5.0.0",
12 | "styled-components": "^2.4.0",
13 | "styled-components-tachyons": "^0.0.5"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test --env=jsdom",
19 | "eject": "react-scripts eject",
20 | "pdf": "node scripts/export.js",
21 | "pretty": "prettier --write --tab-width 4 'src/**/*.js' 'scripts/**/*.js'"
22 | },
23 | "devDependencies": {
24 | "mkdirp": "^0.5.1",
25 | "prettier": "^1.12.1",
26 | "puppeteer": "^1.0.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dashed/react-resume/408fd4d814d8811266f3e872eca716faad4252d2/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | react-resume
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/export.js:
--------------------------------------------------------------------------------
1 | // node imports
2 |
3 | const http = require("http");
4 | const path = require("path");
5 |
6 | // 3rd-party imports
7 |
8 | const mkdirp = require("mkdirp");
9 | const puppeteer = require("puppeteer");
10 |
11 | // export code
12 |
13 | const SERVER = "http://localhost:3000/";
14 | const OUT_DIR = path.join(__dirname, "../out/");
15 | const PDF_FILENAME = "resume.pdf";
16 |
17 | function sleep(ms = 0) {
18 | return new Promise(r => setTimeout(r, ms));
19 | }
20 |
21 | const fetchResponse = () => {
22 | return new Promise((resolve, reject) => {
23 | try {
24 | const request = http.request(SERVER, response =>
25 | resolve(response.statusCode)
26 | );
27 | request.on("error", err => reject(err));
28 | request.end();
29 | } catch (err) {
30 | reject(err);
31 | }
32 | });
33 | };
34 |
35 | const reachableServer = () => {
36 | console.log("Connected to server ...");
37 |
38 | return fetchResponse().then(statusCode => {
39 | if (statusCode === 200) {
40 | // 200 OK
41 | return true;
42 | }
43 |
44 | throw Error(
45 | `Unable to connect to server. Received status code: ${statusCode}`
46 | );
47 | });
48 | };
49 |
50 | const makeOutDir = () => {
51 | return new Promise((resolve, reject) => {
52 | mkdirp(OUT_DIR, err => {
53 | if (!err) {
54 | resolve();
55 | return;
56 | }
57 |
58 | reject(err);
59 | });
60 | });
61 | };
62 |
63 | const convert = async () => {
64 | await reachableServer();
65 |
66 | console.log("Exporting ...");
67 |
68 | const browser = await puppeteer.launch({ args: ["--no-sandbox"] });
69 |
70 | const page = await browser.newPage();
71 |
72 | await page.goto(SERVER, { waitUntil: "networkidle2" });
73 |
74 | await makeOutDir();
75 |
76 | await sleep(2000);
77 |
78 | await page.pdf({
79 | path: path.join(OUT_DIR, PDF_FILENAME),
80 | format: "letter"
81 | });
82 |
83 | await browser.close();
84 |
85 | console.log("Finished exports.");
86 | };
87 |
88 | convert().catch(reason => {
89 | console.error(`${reason}`);
90 | process.exit(1);
91 | });
92 |
--------------------------------------------------------------------------------
/src/components/canvas.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import styled from "styled-components";
4 |
5 | // component
6 |
7 | const Canvas = styled.div`
8 | min-width: fit-content;
9 | padding: 15px;
10 |
11 | background-color: #e9ecef;
12 |
13 | -webkit-print-color-adjust: exact;
14 | print-color-adjust: exact;
15 |
16 | @media print {
17 | display: block;
18 |
19 | background-color: #fff;
20 | }
21 | `;
22 |
23 | export default Canvas;
24 |
--------------------------------------------------------------------------------
/src/components/daterange.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import React from "react";
4 | import invariant from "invariant";
5 |
6 | import styled from "styled-components";
7 |
8 | // component
9 |
10 | const NOT_SET = {};
11 |
12 | const Container = styled.span``;
13 |
14 | const DateRange = ({ start, end = NOT_SET }) => {
15 | invariant(start, "expected start");
16 |
17 | if (end === NOT_SET) {
18 | return (
19 | {`${start}\u00A0\u00A0\u2014\u00A0\u00A0Present`}
21 | );
22 | }
23 |
24 | return (
25 | {`${start}\u00A0\u00A0\u2014\u00A0\u00A0${end}`}
26 | );
27 | };
28 |
29 | export default DateRange;
30 |
--------------------------------------------------------------------------------
/src/components/example.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import React from "react";
4 |
5 | import styled, { injectGlobal } from "styled-components";
6 |
7 | import "styled-components-tachyons/variables.css";
8 | import tachyons from "styled-components-tachyons";
9 |
10 | // local imports
11 |
12 | import PageBase from "./page";
13 | import Canvas from "./canvas";
14 |
15 | import DateRange from "./daterange";
16 | import SkillsTools from "./skillstools";
17 | import SummaryLine from "./summary_line";
18 |
19 | // global styles
20 |
21 | injectGlobal`
22 | @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,200i,300,300i,400,400i,600,600i,700,700i,900,900i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese');
23 |
24 | @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro');
25 |
26 | body {
27 | font-family: 'Source Sans Pro', sans-serif;
28 | font-size: 13px;
29 |
30 | font-kerning: normal;
31 | }
32 |
33 | a {
34 | color: #0366d6;
35 | text-decoration: none;
36 | }
37 | `;
38 |
39 | // components
40 |
41 | const Page = PageBase.extend`
42 | padding: 0.5in;
43 |
44 | display: flex;
45 | flex-direction: column;
46 | `;
47 |
48 | const Header = styled.div`
49 | ${tachyons};
50 |
51 | font-weight: bold;
52 | font-size: 32px;
53 | text-align: center;
54 | `;
55 |
56 | const Contact = styled.div`
57 | ${tachyons};
58 |
59 | display: flex;
60 | `;
61 |
62 | const ContactGutter = styled.div`
63 | flex-grow: 10;
64 | `;
65 |
66 | const Link = styled.a`
67 | font-family: "Source Code Pro", monospace;
68 |
69 | font-weight: 500;
70 | `;
71 |
72 | const Phone = styled.span`
73 | font-family: "Source Code Pro", monospace;
74 |
75 | font-weight: 500;
76 | `;
77 |
78 | const SubHeader = styled.div`
79 | ${tachyons};
80 | font-weight: bold;
81 | font-size: 18px;
82 | `;
83 |
84 | const WorkExperience = styled.div`
85 | ${tachyons};
86 | `;
87 |
88 | const LocationTime = styled.div`
89 | ${tachyons};
90 | display: flex;
91 | `;
92 |
93 | const LocationTimeGutter = styled.div`
94 | flex-grow: 10;
95 | `;
96 |
97 | const Position = styled.div`
98 | ${tachyons};
99 | font-style: italic;
100 | `;
101 |
102 | const SubSubHeader = styled.div`
103 | ${tachyons};
104 | font-weight: bold;
105 | `;
106 |
107 | const Education = styled.div``;
108 |
109 | const EducationLine = styled.div`
110 | ${tachyons};
111 | display: flex;
112 | `;
113 |
114 | const EducationGutter = styled.div`
115 | flex-grow: 10;
116 | `;
117 |
118 | const OpenSource = styled.div`
119 | ${tachyons};
120 | `;
121 |
122 | const OpenSourceLine = styled.div`
123 | ${tachyons};
124 | `;
125 |
126 | const Gutter = styled.div`
127 | flex-grow: 10;
128 | `;
129 |
130 | const Personal = () => {
131 | return (
132 |
412 | );
413 | };
414 |
415 | export default Personal;
416 |
--------------------------------------------------------------------------------
/src/components/page.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import styled from "styled-components";
4 |
5 | // component
6 |
7 | const Page = styled.div`
8 | width: 8.5in;
9 | height: 11in;
10 |
11 | margin-right: auto;
12 | margin-left: auto;
13 |
14 | position: relative;
15 | overflow: hidden;
16 |
17 | background-color: ${({ bgcolor }) => (bgcolor ? bgcolor : "#fff")};
18 | box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.2);
19 |
20 | margin-top: 15px;
21 | margin-bottom: 15px;
22 |
23 | &:first-of-type {
24 | margin-top: 0px;
25 | }
26 |
27 | &:last-of-type {
28 | margin-bottom: 0px;
29 | }
30 |
31 | @media print {
32 | box-shadow: none;
33 |
34 | margin: 0;
35 | box-sizing: border-box;
36 | page-break-after: always;
37 |
38 | &:first-of-type {
39 | margin: 0;
40 | }
41 |
42 | &:last-of-type {
43 | margin: 0;
44 | }
45 | }
46 | `;
47 |
48 | export default Page;
49 |
--------------------------------------------------------------------------------
/src/components/skillstools.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import React from "react";
4 |
5 | import styled from "styled-components";
6 | import tachyons from "styled-components-tachyons";
7 |
8 | // component
9 |
10 | const Container = styled.div`
11 | ${tachyons};
12 | `;
13 |
14 | const SkillsTools = ({ children }) => {
15 | return (
16 |
17 | {`Skills / Tools used:\u00A0\u00A0`}
18 | {children}
19 |
20 | );
21 | };
22 |
23 | export default SkillsTools;
24 |
--------------------------------------------------------------------------------
/src/components/summary_line.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import React from "react";
4 |
5 | import styled from "styled-components";
6 | import tachyons from "styled-components-tachyons";
7 |
8 | // component
9 |
10 | const Container = styled.div`
11 | ${tachyons};
12 | display: flex;
13 | text-align: justify;
14 | `;
15 |
16 | const Bullet = styled.div`
17 | ${tachyons};
18 | `;
19 |
20 | const SummaryLine = ({ children }) => {
21 | return (
22 |
23 | {`\u2014`}
24 | {children}
25 |
26 | );
27 | };
28 |
29 | export default SummaryLine;
30 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // 3rd-party imports
2 |
3 | import "normalize.css";
4 | import "sanitize.css";
5 |
6 | import React from "react";
7 | import ReactDOM from "react-dom";
8 |
9 | // local imports
10 |
11 | import registerServiceWorker from "./registerServiceWorker";
12 |
13 | const customResolve = path => {
14 | return import(`${path}`);
15 | };
16 |
17 | customResolve("./components/personal/personal")
18 | .then(({ default: Resume }) => {
19 | ReactDOM.render(, document.getElementById("root"));
20 |
21 | registerServiceWorker();
22 | })
23 | .catch(reason => {
24 | import("./components/example").then(({ default: Resume }) => {
25 | ReactDOM.render(, document.getElementById("root"));
26 |
27 | registerServiceWorker();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === "localhost" ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === "[::1]" ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener("load", () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === "installed") {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log(
60 | "New content is available; please refresh."
61 | );
62 | } else {
63 | // At this point, everything has been precached.
64 | // It's the perfect time to display a
65 | // "Content is cached for offline use." message.
66 | console.log("Content is cached for offline use.");
67 | }
68 | }
69 | };
70 | };
71 | })
72 | .catch(error => {
73 | console.error("Error during service worker registration:", error);
74 | });
75 | }
76 |
77 | function checkValidServiceWorker(swUrl) {
78 | // Check if the service worker can be found. If it can't reload the page.
79 | fetch(swUrl)
80 | .then(response => {
81 | // Ensure service worker exists, and that we really are getting a JS file.
82 | if (
83 | response.status === 404 ||
84 | response.headers.get("content-type").indexOf("javascript") ===
85 | -1
86 | ) {
87 | // No service worker found. Probably a different app. Reload the page.
88 | navigator.serviceWorker.ready.then(registration => {
89 | registration.unregister().then(() => {
90 | window.location.reload();
91 | });
92 | });
93 | } else {
94 | // Service worker found. Proceed as normal.
95 | registerValidSW(swUrl);
96 | }
97 | })
98 | .catch(() => {
99 | console.log(
100 | "No internet connection found. App is running in offline mode."
101 | );
102 | });
103 | }
104 |
105 | export function unregister() {
106 | if ("serviceWorker" in navigator) {
107 | navigator.serviceWorker.ready.then(registration => {
108 | registration.unregister();
109 | });
110 | }
111 | }
112 |
--------------------------------------------------------------------------------