├── .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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {data && ( 24 | <> 25 | {data?.map((position, ind) => { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | })} 37 | 38 | )} 39 | 40 |
CompanyJob TitleStartEndIs current?Summary
{position?.org}{position?.title}{timestampToDate(position?.start?.timestamp)}{timestampToDate(position?.end?.timestamp)}{position?.isCurrent ? 'Yes' : 'No'}{position?.summary}
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 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {/* */} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 41 | 42 | 43 |
Name(s){data?.name || "none"}
Last Position{data?.position || "none"}
Email{data?.email || "none"}Phone{data?.phone || "none"}
Links 32 | {data?.links?.map((link, i) => ( 33 | 34 | {i > 0 && ", "} 35 | 36 | {link.url} 37 | 38 | 39 | )) || "none"} 40 |
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 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | })} 42 | 43 | )} 44 | 45 |
Time Period 21 | {timestampToDate(school?.start?.timestamp)} to{" "} 22 | {timestampToDate(school?.end?.timestamp)} 23 | Is Current?{school?.isCurrent ? "Yes" : "No"}
Organization{school?.org}Degree{school?.degree}
GPA{school?.gpa || "n/a"}Summary{school?.summary}
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 | --------------------------------------------------------------------------------