├── .eslintrc
├── public
├── favicon.ico
└── preview.jpeg
├── next-sitemap-generator.js
├── hooks
├── index.jsx
├── useCustomAnalytics.jsx
└── useCustomToast.jsx
├── components
├── Buttons
│ ├── index.jsx
│ ├── ResumeUpload.jsx
│ └── DownloadJSON.jsx
├── DataDisplay
│ ├── Positions.jsx
│ ├── Overview.jsx
│ ├── Education.jsx
│ └── index.jsx
├── TopBar
│ └── index.jsx
└── Header
│ └── index.jsx
├── constants
└── index.js
├── next.config.js
├── pages
├── index.js
├── _app.js
└── api
│ ├── events.js
│ └── parse.js
├── functions
└── index.js
├── styles
└── dataDisplay.css
├── .gitignore
├── package.json
└── README.md
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KnlnKS/resume-parser/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/preview.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KnlnKS/resume-parser/HEAD/public/preview.jpeg
--------------------------------------------------------------------------------
/next-sitemap-generator.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteUrl: "https://resume-parser.vercel.app/",
3 | generateRobotsTxt: true,
4 | };
5 |
--------------------------------------------------------------------------------
/hooks/index.jsx:
--------------------------------------------------------------------------------
1 | import useCustomToast from './useCustomToast';
2 | import useCustomAnalytics from './useCustomAnalytics';
3 |
4 | export const useToast = useCustomToast;
5 | export const useAnalytics = useCustomAnalytics;
6 |
--------------------------------------------------------------------------------
/components/Buttons/index.jsx:
--------------------------------------------------------------------------------
1 | import ResumeUpload from './ResumeUpload';
2 | import DownloadJSON from './DownloadJSON';
3 |
4 | export const ResumeUploadButton = ResumeUpload;
5 | export const DownloadJSONButton = DownloadJSON;
6 |
--------------------------------------------------------------------------------
/constants/index.js:
--------------------------------------------------------------------------------
1 |
2 | export const API_URL = "/api/parse";
3 | export const ANALYTICS_URL = "/api/events";
4 | export const TOKEN_URL = "/api/token";
5 | export const MAX_FILE_SIZE = 3 * 1000 * 1000; // 3 MB
6 | export const TOAST_DURATION = 3000;
7 |
--------------------------------------------------------------------------------
/hooks/useCustomAnalytics.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | import { ANALYTICS_URL } from "../constants";
4 |
5 | const useCustomAnalytics = (event) => {
6 | useEffect(() => {
7 | fetch(ANALYTICS_URL, { method: "POST", body: event });
8 | }, []);
9 | };
10 |
11 | export default useCustomAnalytics;
12 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withPWA = require("next-pwa");
2 |
3 | const settings = {
4 | i18n: {
5 | locales: ["en"],
6 | defaultLocale: "en",
7 | },
8 | };
9 |
10 | module.exports =
11 | process.env.NODE_ENV === "production"
12 | ? withPWA({
13 | pwa: {
14 | dest: "public",
15 | },
16 | ...settings,
17 | })
18 | : settings ;
19 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import DataDisplay from "../components/DataDisplay";
2 | import Header from "../components/Header";
3 | import TopBar from "../components/TopBar";
4 | import { useAnalytics } from "../hooks";
5 |
6 | export default function Home() {
7 | useAnalytics("Page View");
8 | return (
9 | <>
10 |
11 |
12 |
13 | >
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/dataDisplay.css";
2 | import "@fontsource/open-sans/400.css";
3 | import "@fontsource/inter/700.css";
4 |
5 | import { ChakraProvider, extendTheme } from "@chakra-ui/react";
6 |
7 | const theme = extendTheme({
8 | fonts: {
9 | heading: "Inter",
10 | body: "Open Sans",
11 | },
12 | });
13 |
14 | function MyApp({ Component, pageProps }) {
15 | return (
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default MyApp;
23 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts a unix timestamp into a human readable date.
3 | * @param {number} timestamp
4 | * @returns Human readable timestamp string in format "Month Date, Year" e.g. "August 31, 2020"
5 | */
6 | export const timestampToDate = (timestamp) => {
7 | let readableDate = new Date(timestamp);
8 |
9 | if (!isNaN(readableDate)) {
10 | return readableDate.toLocaleDateString(window.navigator.language, {
11 | timeZone: "UTC",
12 | year: "numeric",
13 | month: "long",
14 | day: "numeric",
15 | });
16 | } else {
17 | return "n/a";
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/styles/dataDisplay.css:
--------------------------------------------------------------------------------
1 | .category {
2 | background: lightgray;
3 | }
4 |
5 | h2 {
6 | text-align: center;
7 | }
8 |
9 | input[type='file'] {
10 | appearance: none;
11 | background-color: initial;
12 | cursor: default;
13 | align-items: baseline;
14 | color: inherit;
15 | text-overflow: ellipsis;
16 | white-space: pre;
17 | text-align: start !important;
18 | padding: initial;
19 | border: initial;
20 | border-color: initial;
21 | overflow: hidden !important;
22 | border-radius: initial;
23 | height: 100%;
24 | position: absolute;
25 | top: 0;
26 | left: 0;
27 | opacity: 0;
28 | cursor: pointer;
29 | }
30 |
--------------------------------------------------------------------------------
/components/Buttons/ResumeUpload.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@chakra-ui/react";
2 |
3 | const ResumeUpload = ({ parseStatus, handleFileInput }) => (
4 |
22 | );
23 |
24 | export default ResumeUpload;
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # SEO
38 | public/robots.txt
39 | public/sitemap.xml
40 |
41 | # PWA
42 | public/*.js
43 | public/*.js.map
44 |
--------------------------------------------------------------------------------
/components/Buttons/DownloadJSON.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@chakra-ui/react';
2 |
3 | const DownloadJSON = ({ parseStatus, parsedData }) => {
4 | const downloadFile = async () => {
5 | const { file, ...data } = parsedData;
6 | const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
7 | const link = document.createElement('a');
8 | link.href = await URL.createObjectURL(blob);
9 | link.download = `${file}.json`;
10 | document.body.appendChild(link);
11 | link.click();
12 | document.body.removeChild(link);
13 | };
14 |
15 | return (
16 | <>
17 | {parseStatus === 'success' && (
18 |
21 | )}
22 | >
23 | );
24 | };
25 |
26 | export default DownloadJSON;
27 |
--------------------------------------------------------------------------------
/pages/api/events.js:
--------------------------------------------------------------------------------
1 | import redis from "redis";
2 |
3 | export default async function handler(req, res) {
4 | if (req.method !== "POST") {
5 | return res.status(405).send("Only POST Please");
6 | } else if (!req.body && typeof req.body !== "string") {
7 | return res.status(400).send("Missing Parameter");
8 | }
9 |
10 | const client = redis.createClient({
11 | host: process.env.NEXT_PUBLIC_REDIS_HOST,
12 | port: process.env.NEXT_PUBLIC_REDIS_PORT,
13 | password: process.env.NEXT_PUBLIC_REDIS_PASSWORD,
14 | });
15 |
16 | const event = req.body;
17 |
18 | client.incr(event);
19 | client.get(event, (err, data) => {
20 | if (err) {
21 | return res.status(500).send({ message: `Error logging ${event} event.` });
22 | }
23 | console.log(err ? err : `Logged ${data} ${event} events.`);
24 | });
25 | client.quit();
26 |
27 | return res.status(200).send({ message: `Logged ${event} event.` });
28 | }
29 |
--------------------------------------------------------------------------------
/hooks/useCustomToast.jsx:
--------------------------------------------------------------------------------
1 | import { useToast } from "@chakra-ui/react";
2 | import { MAX_FILE_SIZE, TOAST_DURATION } from "../constants";
3 |
4 | const useCustomToast = () => {
5 | const toast = useToast();
6 |
7 | const successToast = () =>
8 | toast({
9 | title: "Resume successfully parsed!",
10 | status: "success",
11 | duration: TOAST_DURATION,
12 | isClosable: true,
13 | });
14 | const errorToast = (err) =>
15 | toast({
16 | title: "Resume parsing failed!",
17 | description: JSON.stringify(err.message),
18 | status: "error",
19 | duration: TOAST_DURATION,
20 | isClosable: true,
21 | });
22 | const fileSizeErrorToast = () =>
23 | toast({
24 | title: "Resume upload error!",
25 | description: `File too big. Maximum size of ${
26 | MAX_FILE_SIZE / 1000000
27 | }MB.`,
28 | status: "error",
29 | duration: TOAST_DURATION,
30 | isClosable: true,
31 | });
32 |
33 | return [successToast, errorToast, fileSizeErrorToast];
34 | };
35 |
36 | export default useCustomToast;
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "resume-parser",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "run-s build:next sitemap",
8 | "build:next": "next build",
9 | "sitemap": "next-sitemap --config next-sitemap-generator.js",
10 | "start": "next start",
11 | "lint": "next lint"
12 | },
13 | "dependencies": {
14 | "@chakra-ui/icons": "^1.0.14",
15 | "@chakra-ui/react": "^1.0.0",
16 | "@emotion/react": "^11.0.0",
17 | "@emotion/styled": "^11.0.0",
18 | "@fontsource/inter": "^4.5.0",
19 | "@fontsource/open-sans": "^4.5.0",
20 | "cheerio": "1.0.0-rc.10",
21 | "cryptr": "6.0.2",
22 | "form-data": "4.0.0",
23 | "formidable": "1.2.2",
24 | "framer-motion": "^4.0.0",
25 | "net": "1.0.2",
26 | "next": "13.0.5",
27 | "react": "18.2.0",
28 | "react-dom": "18.2.0",
29 | "redis": "3.1.2",
30 | "tls": "0.0.1"
31 | },
32 | "devDependencies": {
33 | "eslint": "7.32.0",
34 | "eslint-config-next": "13.0.5",
35 | "next-pwa": "5.2.24",
36 | "next-sitemap": "1.6.148",
37 | "npm-run-all": "4.1.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/components/DataDisplay/Positions.jsx:
--------------------------------------------------------------------------------
1 | import { Table, Thead, Tbody, Tr, Th, Td, Heading } from '@chakra-ui/react';
2 | import React from 'react';
3 | import { timestampToDate } from "../../functions";
4 |
5 | const Positions = ({ data }) => {
6 | return (
7 | <>
8 |
9 | Employment
10 |
11 |
12 |
13 |
14 | | Company |
15 | Job Title |
16 | Start |
17 | End |
18 | Is current? |
19 | Summary |
20 |
21 |
22 |
23 | {data && (
24 | <>
25 | {data?.map((position, ind) => {
26 | return (
27 |
28 | | {position?.org} |
29 | {position?.title} |
30 | {timestampToDate(position?.start?.timestamp)} |
31 | {timestampToDate(position?.end?.timestamp)} |
32 | {position?.isCurrent ? 'Yes' : 'No'} |
33 | {position?.summary} |
34 |
35 | );
36 | })}
37 | >
38 | )}
39 |
40 |
41 | >
42 | );
43 | };
44 | export default Positions;
45 |
--------------------------------------------------------------------------------
/components/DataDisplay/Overview.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Table, Tbody, Tr, Td, Heading, Link } from "@chakra-ui/react";
4 | import { ExternalLinkIcon } from "@chakra-ui/icons";
5 |
6 | const Overview = ({ data }) => (
7 | <>
8 |
9 | Overview
10 |
11 |
12 |
13 | {/* */}
14 |
15 | | Name(s) |
16 | {data?.name || "none"} |
17 |
18 |
19 | | Last Position |
20 | {data?.position || "none"} |
21 |
22 | {/* */}
23 |
24 | | Email |
25 | {data?.email || "none"} |
26 | Phone |
27 | {data?.phone || "none"} |
28 |
29 |
30 | | Links |
31 |
32 | {data?.links?.map((link, i) => (
33 |
34 | {i > 0 && ", "}
35 |
36 | {link.url}
37 |
38 |
39 | )) || "none"}
40 | |
41 |
42 |
43 |
44 | >
45 | );
46 |
47 | export default Overview;
48 |
--------------------------------------------------------------------------------
/components/TopBar/index.jsx:
--------------------------------------------------------------------------------
1 | import { ExternalLinkIcon } from "@chakra-ui/icons";
2 | import { Box, Center, Heading, Link, Text } from "@chakra-ui/react";
3 | import React from "react";
4 |
5 | const TopBar = () => {
6 | return (
7 | <>
8 |
9 |
10 | Resume Parser
11 |
12 |
13 |
14 |
15 | How well does your resume get parsed?
16 |
17 |
18 | {`This tool uses Lever's resume parsing API to parse resumes. Use this
19 | to see how well your resume is read by Application Tracking Systems
20 | (ATS) when applying to jobs. Companies that use Lever for job apps
21 | include: Figma, Palantir, Netflix, Twitch, Yelp and several others.`}
22 |
23 |
24 |
25 | Made with ❤️ by{" "}
26 |
32 | KnlnKS
33 | {" "}
34 | and{" "}
35 |
41 | kevin51jiang
42 |
43 | .
44 |
45 | >
46 | );
47 | };
48 | export default TopBar;
49 |
--------------------------------------------------------------------------------
/pages/api/parse.js:
--------------------------------------------------------------------------------
1 | import formidable from "formidable";
2 | import FormData from "form-data";
3 | import fs from "fs";
4 |
5 | export const config = {
6 | api: {
7 | bodyParser: false,
8 | },
9 | };
10 |
11 | export default async function handler(req, res) {
12 | if (req.method !== "POST") {
13 | // Return a "method not allowed" error
14 | res.status(405).send("Only POST Please");
15 | return;
16 | }
17 |
18 | const { fields, files } = await new Promise(function (resolve, reject) {
19 | const form = new formidable.IncomingForm({ keepExtensions: true });
20 | form.parse(req, function (err, fields, files) {
21 | if (err) return reject(err);
22 | resolve({ fields, files });
23 | });
24 | });
25 |
26 | if (!files.resume.path) {
27 | res.status(400).send("No resume uploaded!");
28 | return;
29 | }
30 |
31 | const formData = new FormData();
32 | formData.append("resume", fs.createReadStream(files.resume.path));
33 |
34 | await fetch("https://jobs.lever.co/parseResume", {
35 | method: "POST",
36 | headers: {
37 | Origin: "https://jobs.lever.co",
38 | Referer: "https://jobs.lever.co/parse",
39 | },
40 | body: formData,
41 | })
42 | .then((response) => {
43 | if (!response.ok) {
44 | res
45 | .status(response.status)
46 | .send(
47 | response.status == 500
48 | ? "Could not parse resume"
49 | : "Could not connect to Lever"
50 | );
51 |
52 | return;
53 | }
54 | return response.json();
55 | })
56 | .then((response) => response && res.json(response))
57 | .catch(console.error);
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/components/Header/index.jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import React from "react";
3 |
4 | const Header = () => (
5 |
6 |
7 |
11 | Resume Parser
12 |
13 |
17 |
21 |
22 |
23 |
24 |
28 |
32 |
36 |
37 |
38 |
42 |
46 |
47 | );
48 |
49 | export default Header;
50 |
--------------------------------------------------------------------------------
/components/DataDisplay/Education.jsx:
--------------------------------------------------------------------------------
1 | import { Table, Tbody, Tr, Td, Heading } from "@chakra-ui/react";
2 | import React from "react";
3 | import { timestampToDate } from "../../functions";
4 |
5 | const Education = ({ data }) => {
6 | return (
7 | <>
8 |
9 | Schools
10 |
11 |
12 |
13 | {data && data?.length > 0 && (
14 | <>
15 | {data?.map((school, ind) => {
16 | return (
17 |
18 |
19 | | Time Period |
20 |
21 | {timestampToDate(school?.start?.timestamp)} to{" "}
22 | {timestampToDate(school?.end?.timestamp)}
23 | |
24 | Is Current? |
25 | {school?.isCurrent ? "Yes" : "No"} |
26 |
27 |
28 | | Organization |
29 | {school?.org} |
30 | Degree |
31 | {school?.degree} |
32 |
33 |
34 | | GPA |
35 | {school?.gpa || "n/a"} |
36 | Summary |
37 | {school?.summary} |
38 |
39 |
40 | );
41 | })}
42 | >
43 | )}
44 |
45 |
46 | >
47 | );
48 | };
49 | export default Education;
50 |
--------------------------------------------------------------------------------
/components/DataDisplay/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { API_URL, MAX_FILE_SIZE } from "../../constants";
4 | import { useToast } from "../../hooks";
5 |
6 | import Overview from "./Overview";
7 | import { ResumeUploadButton, DownloadJSONButton } from "../Buttons";
8 | import { Box, Center } from "@chakra-ui/react";
9 |
10 | const DataDisplay = () => {
11 | const [parsedData, setParsedData] = useState();
12 | /**
13 | * Acceptable statuses:
14 | * - none:
15 | * - uploading:
16 | * - success:
17 | * - error:
18 | */
19 | const [parseStatus, setParseStatus] = useState("none");
20 | const [successToast, errorToast, fileSizeErrorToast] = useToast();
21 |
22 | const handleErrors = (response) => {
23 | if (!response.ok) {
24 | return response.text().then((text) => {
25 | throw new Error(`Error ${response.status} - ${text}`);
26 | });
27 | }
28 | return response;
29 | };
30 |
31 | const handleFileInput = async (e) => {
32 | const file = e.target.files[0];
33 |
34 | // don't break the app if they cancel on the file selection page
35 | if (!file) {
36 | return;
37 | } else if (file.size > MAX_FILE_SIZE) {
38 | fileSizeErrorToast();
39 | return;
40 | }
41 |
42 | const formData = new FormData();
43 | formData.append("resume", file);
44 |
45 | setParseStatus("uploading");
46 | fetch(API_URL, {
47 | method: "POST",
48 | body: formData,
49 | })
50 | .then(handleErrors)
51 | .then((response) => response.json())
52 | .then((data) => setParsedData({ file: file.name, ...data }))
53 | .then(() => {
54 | setParseStatus("success");
55 | successToast();
56 | })
57 | .catch((err) => {
58 | setParseStatus("error");
59 | errorToast(err);
60 | });
61 | };
62 |
63 | return (
64 | <>
65 |
66 |
67 |
68 |
69 |
70 | {parsedData ? (
71 |
72 |
73 |
74 |
75 |
76 | ) : (
77 | ""
78 | )}
79 | >
80 | );
81 | };
82 | export default DataDisplay;
83 |
--------------------------------------------------------------------------------