├── .prettierrc ├── public ├── favicon.png ├── our-team.png └── logo_transparent.png ├── styles ├── ATS.module.css ├── globals.css ├── Home.module.css ├── antd.less └── ListingsPage.module.css ├── pages ├── _app.js ├── api │ ├── pipeline.js │ ├── jobListing │ │ └── [id].js │ ├── applicants │ │ └── [id].js │ ├── cv │ │ └── [id].js │ ├── jobs │ │ └── [id].js │ ├── me.js │ ├── cv.js │ ├── jobs.js │ ├── applicants.js │ ├── auth.js │ └── users.js ├── signup.js ├── jobs.js ├── login.js ├── ats.js ├── index.js └── jobs │ └── [id].js ├── .eslintrc.json ├── .gitignore ├── middleware └── database.js ├── next.config.js ├── package.json ├── README.md ├── components ├── addJobModal.js ├── editJobModal.js ├── applicantView.js ├── applicants.js ├── jobListings.js ├── insertApplicantModal.js └── viewApplicantModal.js └── LICENSE /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baykamsay/simple-ats/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/our-team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baykamsay/simple-ats/HEAD/public/our-team.png -------------------------------------------------------------------------------- /public/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baykamsay/simple-ats/HEAD/public/logo_transparent.png -------------------------------------------------------------------------------- /styles/ATS.module.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | height: 64px; 3 | margin: 0 56px 0 0; 4 | float: left; 5 | } 6 | 7 | .siteLayoutBackground { 8 | background: #fff; 9 | } 10 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | a { 8 | color: inherit; 9 | text-decoration: none; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/antd.less"; 2 | 3 | // eslint-disable-next-line react/prop-types 4 | function MyApp({ Component, pageProps }) { 5 | return ; 6 | } 7 | 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /pages/api/pipeline.js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../middleware/database"; 3 | 4 | const handler = nextConnect(); 5 | 6 | handler.use(middleware); 7 | 8 | handler.get(async (req, res) => { 9 | let doc = await req.db.collection("pipeline").findOne(); 10 | res.json(doc.pipeline); 11 | }); 12 | 13 | export default handler; 14 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | width: 100%; 12 | flex: 1; 13 | } 14 | 15 | .footer { 16 | width: 100%; 17 | height: 100px; 18 | border-top: 1px solid #eaeaea; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/jobListing/[id].js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../../middleware/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.get(async (req, res) => { 10 | let doc = await req.db 11 | .collection("jobs") 12 | .findOne({ _id: ObjectId(req.query.id) }); 13 | res.json(doc); 14 | }); 15 | 16 | export default handler; 17 | -------------------------------------------------------------------------------- /pages/api/applicants/[id].js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../../middleware/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.get(async (req, res) => { 10 | let doc = await req.db 11 | .collection("applicants") 12 | .findOne({ _id: ObjectId(req.query.id) }); 13 | res.json(doc); 14 | }); 15 | 16 | export default handler; 17 | -------------------------------------------------------------------------------- /pages/api/cv/[id].js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../../middleware/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.get(async (req, res) => { 10 | let doc = await req.db 11 | .collection("cvs") 12 | .findOne({ _id: ObjectId(req.query.id) }); 13 | doc.file = doc.file.buffer; 14 | res.json(doc); 15 | }); 16 | 17 | export default handler; 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "prettier", 6 | "prettier/react" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 11, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | }, 15 | "env": { 16 | "es6": true, 17 | "browser": true, 18 | "node": true 19 | }, 20 | "rules": { "react/react-in-jsx-scope": "off", "react/prop-types": 0 }, 21 | "globals": { "React": "writable" } 22 | } 23 | -------------------------------------------------------------------------------- /.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.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /middleware/database.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | import nextConnect from "next-connect"; 3 | 4 | const client = new MongoClient( 5 | process.env.DB_URL, // put your db url in .env.local 6 | { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | } 10 | ); 11 | 12 | async function database(req, res, next) { 13 | if (!client.isConnected()) await client.connect(); 14 | req.dbClient = client; 15 | req.db = client.db("ATS"); 16 | return next(); 17 | } 18 | 19 | const middleware = nextConnect(); 20 | 21 | middleware.use(database); 22 | 23 | export default middleware; 24 | -------------------------------------------------------------------------------- /pages/api/jobs/[id].js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../../middleware/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.get(async (req, res) => { 10 | let doc = await req.db 11 | .collection("jobs") 12 | .findOne({ _id: ObjectId(req.query.id) }); 13 | const applicantIds = doc.applicants; 14 | const applicants = await Promise.all( 15 | applicantIds.map( 16 | async (applicantId) => 17 | await req.db.collection("applicants").findOne({ _id: applicantId }) 18 | ) 19 | ); 20 | res.json(applicants); 21 | }); 22 | 23 | export default handler; 24 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withSass = require("@zeit/next-sass"); 2 | const withLess = require("@zeit/next-less"); 3 | const withCSS = require("@zeit/next-css"); 4 | 5 | const isProd = process.env.NODE_ENV === "production"; 6 | 7 | // fix: prevents error when .less files are required by node 8 | if (typeof require !== "undefined") { 9 | require.extensions[".less"] = (file) => {}; 10 | } 11 | 12 | module.exports = withCSS({ 13 | cssModules: true, 14 | cssLoaderOptions: { 15 | importLoaders: 1, 16 | localIdentName: "[local]___[hash:base64:5]", 17 | }, 18 | ...withLess( 19 | withSass({ 20 | lessLoaderOptions: { 21 | javascriptEnabled: true, 22 | }, 23 | }) 24 | ), 25 | }); 26 | -------------------------------------------------------------------------------- /pages/api/me.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const jwtSecret = process.env.JWT_SECRET; 3 | 4 | export default (req, res) => { 5 | if (req.method === "GET") { 6 | if (!("token" in req.cookies)) { 7 | res.status(401).json({ message: "Unable to auth" }); 8 | return; 9 | } 10 | let decoded; 11 | const token = req.cookies.token; 12 | if (token) { 13 | try { 14 | decoded = jwt.verify(token, jwtSecret); 15 | } catch (e) { 16 | console.error(e); 17 | } 18 | } 19 | 20 | if (decoded) { 21 | res.json(decoded); 22 | return; 23 | } else { 24 | res.status(401).json({ message: "Unable to auth" }); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /pages/api/cv.js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../middleware/database"; 3 | import { Binary } from "mongodb"; 4 | import formidable from "formidable"; 5 | 6 | const handler = nextConnect(); 7 | 8 | handler.use(middleware); 9 | 10 | handler.post(async (req, res) => { 11 | const form = formidable({ multiples: true }); 12 | 13 | await form.parse(req, (err, fields, files) => { 14 | console.log("fields:", fields); 15 | console.log("files:", files); 16 | }); 17 | 18 | console.log(form); 19 | 20 | let data = {}; 21 | data.file = Binary(req.body); 22 | 23 | let doc = await req.db.collection("cvs").insertOne(data); 24 | 25 | res.json({ message: doc.insertedId }); 26 | }); 27 | 28 | export default handler; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-ats", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@zeit/next-css": "^1.0.1", 12 | "@zeit/next-less": "^1.0.1", 13 | "@zeit/next-sass": "^1.0.1", 14 | "antd": "^4.5.2", 15 | "babel-plugin-import": "^1.13.0", 16 | "bcrypt": "^5.0.0", 17 | "file-saver": "^2.0.2", 18 | "formidable": "^1.2.2", 19 | "isomorphic-unfetch": "^3.0.0", 20 | "js-cookie": "^2.2.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "less": "^3.12.2", 23 | "mongodb": "^3.6.0", 24 | "next": "9.5.1", 25 | "next-connect": "^0.8.1", 26 | "react": "16.13.1", 27 | "react-dom": "16.13.1", 28 | "react-markdown": "^4.3.1", 29 | "swr": "^0.3.0", 30 | "uuid": "^8.3.0" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^7.6.0", 34 | "eslint-config-prettier": "^6.11.0", 35 | "eslint-plugin-react": "^7.20.5", 36 | "prettier": "2.0.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /styles/antd.less: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.less"; 2 | 3 | @primary-color: #1890ff; // primary color for all components 4 | @link-color: #1890ff; // link color 5 | @success-color: #52c41a; // success state color 6 | @warning-color: #faad14; // warning state color 7 | @error-color: #f5222d; // error state color 8 | @font-size-base: 14px; // major text font size 9 | @heading-color: rgba(0, 0, 0, 0.85); // heading text color 10 | @text-color: rgba(0, 0, 0, 0.65); // major text color 11 | @text-color-secondary: rgba(0, 0, 0, 0.45); // secondary text color 12 | @disabled-color: rgba(0, 0, 0, 0.25); // disable state color 13 | @border-radius-base: 2px; // major border radius 14 | @border-color-base: #d9d9d9; // major border color 15 | @box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 16 | 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers 17 | @heading-1-size: ceil(@font-size-base * 4); 18 | @heading-2-size: ceil(@font-size-base * 3); 19 | @heading-3-size: ceil(@font-size-base * 2.5); 20 | @heading-4-size: ceil(@font-size-base * 2); 21 | @heading-5-size: ceil(@font-size-base * 1.5); 22 | @typography-title-font-weight: 600; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple ATS 2 | 3 | Simple ATS is an applicant tracking system with a job listings page. 4 | 5 | ## Features 6 | Applicant side: a landing page, job listings page, and pages for each job listing that are generated from markdown. 7 | Company side: View applicants, access form data, add notes and ratings, create new job listings. 8 | 9 | ## Installation 10 | This is a template repository so just click the "Use this template" button! 11 | 12 | ## Usage 13 | This project uses a MongoDB database. Create your own database and then fill out the environment variables with the following: 14 | - DB_URL= `Your database connection url here` 15 | - JWT_SECRET= `Your jwt secret here` 16 | - URL= `Your hosted url here` 17 | 18 | For demo purposes the api is unprotected. Protecting your api is highly recommended. 19 | The signup route is also unprotected, you should secure this route in production. 20 | 21 | ## Demo 22 | The demo is available at https://ats.demos.baykam.me/. 23 | To access the applicant tracking system visit https://ats.demos.baykam.me/ats. 24 | 25 | ## Contributing 26 | Pull requests are welcome. 27 | 28 | ## License 29 | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 30 | -------------------------------------------------------------------------------- /pages/api/jobs.js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../middleware/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.get(async (req, res) => { 10 | let doc = await req.db.collection("jobs").find().toArray(); 11 | res.json(doc); 12 | }); 13 | 14 | handler.post(async (req, res) => { 15 | let data = req.body; 16 | data = JSON.parse(data); 17 | await req.db.collection("jobs").insertOne({ 18 | title: data.title, 19 | location: data.location, 20 | description: data.description, 21 | applicants: [], 22 | }); 23 | res.json({ message: "ok" }); 24 | }); 25 | 26 | handler.put(async (req, res) => { 27 | let data = req.body; 28 | data = JSON.parse(data); 29 | 30 | await req.db 31 | .collection("jobs") 32 | .updateOne( 33 | { _id: ObjectId(data.id) }, 34 | { 35 | $set: { 36 | title: data.title, 37 | location: data.location, 38 | description: data.description, 39 | }, 40 | }, 41 | function (err) { 42 | if (err) throw err; 43 | } 44 | ); 45 | res.json({ message: "ok" }); 46 | }); 47 | 48 | handler.delete(async (req, res) => { 49 | let id = req.body; 50 | id = JSON.parse(id); 51 | await req.db 52 | .collection("jobs") 53 | .deleteOne({ _id: ObjectId(id) }, function (err) { 54 | if (err) throw err; 55 | }); 56 | res.json({ message: "ok" }); 57 | }); 58 | 59 | export default handler; 60 | -------------------------------------------------------------------------------- /pages/api/applicants.js: -------------------------------------------------------------------------------- 1 | import nextConnect from "next-connect"; 2 | import middleware from "../../middleware/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const handler = nextConnect(); 6 | 7 | handler.use(middleware); 8 | 9 | handler.get(async (req, res) => { 10 | let doc = await req.db.collection("applicants").find().toArray(); 11 | res.json(doc); 12 | }); 13 | 14 | handler.post(async (req, res) => { 15 | let data = req.body; 16 | data = JSON.parse(data); 17 | const listing = data.listing; 18 | delete data.listing; 19 | data.stage = "Applied"; 20 | data.notes = ""; 21 | data.rating = 0; 22 | data.cv = ObjectId(data.cv[0].response.message); 23 | let doc = await req.db.collection("applicants").insertOne(data); 24 | await req.db 25 | .collection("jobs") 26 | .updateOne( 27 | { _id: ObjectId(listing) }, 28 | { $push: { applicants: doc.insertedId } } 29 | ); 30 | res.json({ message: "ok" }); 31 | }); 32 | 33 | handler.put(async (req, res) => { 34 | let data = req.body; 35 | data = JSON.parse(data); 36 | 37 | await req.db 38 | .collection("applicants") 39 | .updateOne( 40 | { _id: ObjectId(data.id) }, 41 | { $set: { stage: data.stage, notes: data.notes, rating: data.rating } }, 42 | function (err) { 43 | if (err) throw err; 44 | } 45 | ); 46 | res.json({ message: "ok" }); 47 | }); 48 | 49 | handler.delete(async (req, res) => { 50 | let id = req.body; 51 | id = JSON.parse(id); 52 | await req.db 53 | .collection("applicants") 54 | .deleteOne({ _id: ObjectId(id) }, function (err) { 55 | if (err) throw err; 56 | }); 57 | await req.db 58 | .collection("jobs") 59 | .updateMany({}, { $pull: { applicants: ObjectId(id) } }); 60 | res.json({ message: "ok" }); 61 | }); 62 | 63 | export default handler; 64 | -------------------------------------------------------------------------------- /components/addJobModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Modal, Button, Form, Input } from "antd"; 3 | 4 | const layout = { 5 | labelCol: { span: 4 }, 6 | wrapperCol: { span: 20 }, 7 | }; 8 | 9 | export default function AddJobModal(props) { 10 | const [form] = Form.useForm(); 11 | async function handleSubmit(e) { 12 | await fetch("/api/jobs", { 13 | method: "post", 14 | body: JSON.stringify(e), 15 | }); 16 | form.resetFields(); 17 | props.close(); 18 | } 19 | return ( 20 | 31 |
38 | 39 | 40 | 41 | 46 | 47 | 48 | 53 | 54 | 55 | 61 | 64 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /pages/signup.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Router from "next/router"; 3 | import cookie from "js-cookie"; 4 | 5 | const Signup = () => { 6 | const [signupError, setSignupError] = useState(""); 7 | const [username, setUsername] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | // const [passwordConfirmation, setPasswordConfirmation] = useState(""); 10 | 11 | function handleSubmit(e) { 12 | e.preventDefault(); 13 | fetch("/api/users", { 14 | method: "POST", 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | body: JSON.stringify({ 19 | username, 20 | password, 21 | }), 22 | }) 23 | .then((r) => r.json()) 24 | .then((data) => { 25 | if (data && data.error) { 26 | setSignupError(data.message); 27 | } 28 | if (data && data.token) { 29 | //set cookie 30 | cookie.set("token", data.token, { expires: 2 }); 31 | Router.push("/"); 32 | } 33 | }); 34 | } 35 | return ( 36 |
37 |

Sign Up

38 | 47 | 48 |
49 | 50 | 59 | 60 |
61 | 62 | 63 | {signupError &&

{signupError}

} 64 |
65 | ); 66 | }; 67 | 68 | export default Signup; 69 | -------------------------------------------------------------------------------- /pages/jobs.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | import styles from "../styles/Home.module.css"; 4 | import listingStyles from "../styles/ListingsPage.module.css"; 5 | import { Row, Col, Divider, List } from "antd"; 6 | 7 | function Jobs({ jobs }) { 8 | return ( 9 |
10 | 11 | Jobs 12 | 13 | 14 | 15 |
16 | 17 | 18 |

Job Openings

19 | 20 | ( 24 | 27 | 28 | Apply 29 | 30 | , 31 | ]} 32 | > 33 | {item.title}} /> 34 | {item.location} 35 | 36 | )} 37 | /> 38 | 39 |
40 |
41 | 42 | 51 |
52 | ); 53 | } 54 | 55 | export async function getServerSideProps() { 56 | return fetch(`${process.env.URL}api/jobs`).then( 57 | async (res) => { 58 | const jobs = await res.json(); 59 | return { 60 | props: { 61 | jobs, 62 | }, 63 | }; 64 | }, 65 | () => { 66 | return { 67 | props: { 68 | jobs: [{}], 69 | }, 70 | }; 71 | } 72 | ); 73 | } 74 | 75 | export default Jobs; 76 | -------------------------------------------------------------------------------- /pages/api/auth.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require("mongodb").MongoClient; 2 | const assert = require("assert"); 3 | const bcrypt = require("bcrypt"); 4 | const jwt = require("jsonwebtoken"); 5 | const jwtSecret = process.env.JWT_SECRET; // put your jwt secret in env.local 6 | 7 | // const saltRounds = 10; 8 | const dbName = "ATS"; 9 | const client = new MongoClient( 10 | process.env.DB_URL, // put your db url in .env.local 11 | { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true, 14 | } 15 | ); 16 | 17 | function findUser(db, username, callback) { 18 | const collection = db.collection("user"); 19 | collection.findOne({ username }, callback); 20 | } 21 | 22 | function authUser(db, username, password, hash, callback) { 23 | // const collection = db.collection("user"); 24 | bcrypt.compare(password, hash, callback); 25 | } 26 | 27 | export default (req, res) => { 28 | if (req.method === "POST") { 29 | //login 30 | try { 31 | assert.notEqual(null, req.body.username, "Email required"); 32 | assert.notEqual(null, req.body.password, "Password required"); 33 | } catch (bodyError) { 34 | res.status(403).send(bodyError.message); 35 | } 36 | 37 | client.connect(function (err) { 38 | assert.equal(null, err); 39 | console.log("Connected to MongoDB server =>"); 40 | const db = client.db(dbName); 41 | const username = req.body.username; 42 | const password = req.body.password; 43 | 44 | findUser(db, username, function (err, user) { 45 | if (err) { 46 | res.status(500).json({ error: true, message: "Error finding User" }); 47 | return; 48 | } 49 | if (!user) { 50 | res.status(404).json({ error: true, message: "User not found" }); 51 | return; 52 | } else { 53 | authUser(db, username, password, user.password, function ( 54 | err, 55 | match 56 | ) { 57 | if (err) { 58 | res.status(500).json({ error: true, message: "Auth Failed" }); 59 | } 60 | if (match) { 61 | const token = jwt.sign( 62 | { userId: user.userId, username: user.username }, 63 | jwtSecret 64 | ); 65 | res.status(200).json({ token }); 66 | return; 67 | } else { 68 | res.status(401).json({ error: true, message: "Auth Failed" }); 69 | return; 70 | } 71 | }); 72 | } 73 | }); 74 | }); 75 | } else { 76 | // Handle any other HTTP method 77 | res.statusCode = 401; 78 | res.end(); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /styles/ListingsPage.module.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"); 2 | 3 | h1, 4 | h2, 5 | h3, 6 | h4, 7 | h5, 8 | h6, 9 | p { 10 | font-family: "Roboto", sans-serif; 11 | color: #141414; 12 | } 13 | 14 | h1 { 15 | font-weight: 700; 16 | font-size: 4rem; 17 | margin-bottom: 0; 18 | } 19 | 20 | h2 { 21 | font-weight: 500; 22 | font-size: 3rem; 23 | } 24 | 25 | h3 { 26 | font-weight: 500; 27 | font-size: 2.5rem; 28 | } 29 | 30 | h4 { 31 | font-weight: 400; 32 | font-size: 2rem; 33 | } 34 | 35 | h5 { 36 | font-weight: 400; 37 | font-size: 1.5rem; 38 | } 39 | 40 | p { 41 | font-size: 1rem; 42 | color: #141414; 43 | } 44 | 45 | li { 46 | font-size: 1rem; 47 | color: #141414; 48 | } 49 | 50 | a.link { 51 | font-family: "Roboto", sans-serif; 52 | font-size: 1.2rem; 53 | } 54 | 55 | .primary { 56 | color: #ffffff; 57 | } 58 | 59 | .secondary { 60 | color: #141414; 61 | } 62 | 63 | .primary_b { 64 | background-color: #ffffff; 65 | } 66 | 67 | .secondary_b { 68 | background-color: #141414; 69 | } 70 | 71 | .margin_bottom { 72 | margin-bottom: 0; 73 | } 74 | 75 | .mission { 76 | font-weight: 400; 77 | font-size: 2rem; 78 | margin-bottom: 0; 79 | } 80 | 81 | a.button { 82 | display: inline-block; 83 | padding: 0.35em 1.2em; 84 | border: 0.1em solid #ffffff; 85 | margin: 1rem 0.3em 0.3em 0; 86 | border-radius: 0.12em; 87 | box-sizing: border-box; 88 | text-decoration: none; 89 | font-family: "Roboto", sans-serif; 90 | font-weight: 300; 91 | color: #ffffff; 92 | text-align: center; 93 | transition: all 0.2s; 94 | } 95 | a.button:hover { 96 | color: #000000; 97 | background-color: #ffffff; 98 | } 99 | @media all and (max-width: 30em) { 100 | a.button { 101 | display: block; 102 | margin: 0.4em auto; 103 | } 104 | } 105 | 106 | a.button_s { 107 | display: inline-block; 108 | padding: 0.35em 1.2em; 109 | border: 0.1em solid #141414; 110 | margin: 1rem 0.3em 0.3em 0; 111 | border-radius: 0.12em; 112 | box-sizing: border-box; 113 | text-decoration: none; 114 | font-family: "Roboto", sans-serif; 115 | font-weight: 300; 116 | color: #141414; 117 | text-align: center; 118 | transition: all 0.2s; 119 | scale: 2; 120 | } 121 | a.button_s:hover { 122 | color: #ffffff; 123 | background-color: #141414; 124 | } 125 | @media all and (max-width: 30em) { 126 | a.button_s { 127 | display: block; 128 | margin: 0.4em auto; 129 | } 130 | } 131 | 132 | .list { 133 | font-family: "Roboto", sans-serif; 134 | color: #141414; 135 | font-size: 1rem; 136 | } 137 | -------------------------------------------------------------------------------- /components/editJobModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Modal, Button, Form, Input } from "antd"; 3 | 4 | const layout = { 5 | labelCol: { span: 4 }, 6 | wrapperCol: { span: 20 }, 7 | }; 8 | 9 | export default function EditJobModal(props) { 10 | const [form] = Form.useForm(); 11 | 12 | useEffect(() => { 13 | form.resetFields(); 14 | }, [props.data]); 15 | 16 | async function handleSubmit(e) { 17 | e.id = props.data._id; 18 | await fetch("/api/jobs", { 19 | method: "put", 20 | body: JSON.stringify(e), 21 | }); 22 | props.close(); 23 | } 24 | 25 | async function deleteListing() { 26 | await fetch("/api/jobs", { 27 | method: "delete", 28 | body: JSON.stringify(props.data._id), 29 | }); 30 | props.close(); 31 | } 32 | 33 | return ( 34 | 44 |
51 | 57 | 58 | 59 | 65 | 66 | 67 | 73 | 74 | 75 | 81 | 88 | 96 | 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /pages/api/users.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require("mongodb").MongoClient; 2 | const assert = require("assert"); 3 | const bcrypt = require("bcrypt"); 4 | const v4 = require("uuid").v4; 5 | const jwt = require("jsonwebtoken"); 6 | const jwtSecret = process.env.JWT_SECRET; // put your jwt secret in env.local 7 | 8 | const saltRounds = 10; 9 | const dbName = "ATS"; 10 | const client = new MongoClient( 11 | process.env.DB_URL, // put your db url in .env.local 12 | { 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | } 16 | ); 17 | 18 | function findUser(db, username, callback) { 19 | const collection = db.collection("user"); 20 | collection.findOne({ username }, callback); 21 | } 22 | 23 | function createUser(db, username, password, callback) { 24 | const collection = db.collection("user"); 25 | bcrypt.hash(password, saltRounds, function (err, hash) { 26 | // Store hash in your password DB. 27 | collection.insertOne( 28 | { 29 | userId: v4(), 30 | username, 31 | password: hash, 32 | }, 33 | function (err, userCreated) { 34 | assert.equal(err, null); 35 | callback(userCreated); 36 | } 37 | ); 38 | }); 39 | } 40 | 41 | export default (req, res) => { 42 | if (req.method === "POST") { 43 | // signup 44 | try { 45 | assert.notEqual(null, req.body.username, "Username required"); 46 | assert.notEqual(null, req.body.password, "Password required"); 47 | } catch (bodyError) { 48 | return res.status(403).json({ error: true, message: bodyError.message }); 49 | } 50 | 51 | // verify username does not exist already 52 | client.connect(function (err) { 53 | assert.equal(null, err); 54 | console.log("Connected to MongoDB server =>"); 55 | const db = client.db(dbName); 56 | const username = req.body.username; 57 | const password = req.body.password; 58 | 59 | findUser(db, username, function (err, user) { 60 | if (err) { 61 | res.status(500).json({ error: true, message: "Error finding User" }); 62 | return; 63 | } 64 | if (!user) { 65 | // proceed to Create 66 | createUser(db, username, password, function (creationResult) { 67 | if (creationResult.ops.length === 1) { 68 | const user = creationResult.ops[0]; 69 | const token = jwt.sign( 70 | { userId: user.userId, username: user.username }, 71 | jwtSecret 72 | // , { 73 | // expiresIn: 3000, //50 minutes 74 | // } 75 | ); 76 | res.status(200).json({ token }); 77 | return; 78 | } 79 | }); 80 | } else { 81 | // User exists 82 | res.status(403).json({ error: true, message: "Username exists" }); 83 | return; 84 | } 85 | }); 86 | }); 87 | } else { 88 | // Handle any other HTTP method 89 | res.status(200).json({ users: ["John Doe"] }); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /components/applicantView.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import fetch from "isomorphic-unfetch"; 3 | import useSWR, { mutate } from "swr"; 4 | import { List, Spin, Card, Tag, Row, Col, Rate } from "antd"; 5 | import ViewApplicantModal from "./viewApplicantModal"; 6 | 7 | export default function ApplicantView(props) { 8 | const { data, error } = useSWR(`/api/jobs/${props.data}`, async function ( 9 | args 10 | ) { 11 | const res = await fetch(args); 12 | return res.json(); 13 | }); 14 | const [modalVisible, setModalVisible] = useState(false); 15 | const [applicantData, setApplicantData] = useState({}); 16 | function setColor(stage) { 17 | let color; 18 | switch (stage) { 19 | case "Applied": 20 | color = "magenta"; 21 | break; 22 | case "Interview": 23 | color = "gold"; 24 | break; 25 | case "Offer": 26 | color = "green"; 27 | break; 28 | default: 29 | color = "blue"; 30 | break; 31 | } 32 | return color; 33 | } 34 | if (error) return
failed to load
; 35 | if (!data) 36 | return ( 37 |
45 | 46 |
47 | ); 48 | return ( 49 |
50 | { 54 | mutate(`/api/jobs/${props.data}`); 55 | setModalVisible(false); 56 | }} 57 | pipeline={props.pipeline} 58 | /> 59 | ( 64 | 65 | { 70 | setApplicantData(item); 71 | setModalVisible(true); 72 | }} 73 | > 74 | 75 | 76 | {item.name} 77 | 78 | 79 | {item.stage} 80 | 81 | 82 | 83 | 84 | 88 | {item.introduction} 89 | 90 | 91 | 92 | 93 | )} 94 | /> 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /components/applicants.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Layout, 4 | Menu, 5 | Typography, 6 | Divider, 7 | Spin, 8 | Row, 9 | Col, 10 | Button, 11 | } from "antd"; 12 | import fetch from "isomorphic-unfetch"; 13 | import useSWR from "swr"; 14 | import styles from "../styles/ATS.module.css"; 15 | import homeStyle from "../styles/Home.module.css"; 16 | import { PlusOutlined } from "@ant-design/icons"; 17 | import InsertApplicantModal from "./insertApplicantModal"; 18 | import ApplicantView from "./applicantView"; 19 | 20 | const { Content, Sider } = Layout; 21 | const { Title } = Typography; 22 | 23 | function Applicants(props) { 24 | const [ 25 | insertApplicantModalVisible, 26 | setInsertApplicantModalVisible, 27 | ] = useState(false); 28 | const [selectedListing, setSelectedListing] = useState(props.data.initialId); 29 | const { data, error } = useSWR("/api/jobs", async function (args) { 30 | const res = await fetch(args); 31 | return res.json(); 32 | }); 33 | if (error) return
failed to load
; 34 | if (!data) 35 | return ( 36 |
37 | 38 |
39 | ); 40 | return ( 41 | 45 | { 48 | setInsertApplicantModalVisible(false); 49 | }} 50 | /> 51 | 52 | 53 | 54 | Applicants 55 | 56 | 57 | 58 | 68 | 69 | 70 | 71 | 72 | 73 | { 78 | setSelectedListing(e.item.props.data); 79 | }} 80 | > 81 | {data.map((job, i) => ( 82 | 83 | {job.title} 84 | 85 | ))} 86 | 87 | 88 | 93 | 97 | 98 | 99 | 100 | ); 101 | } 102 | 103 | export default Applicants; 104 | -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Router from "next/router"; 3 | import cookie from "js-cookie"; 4 | import Head from "next/head"; 5 | import { Layout, Form, Input, Button, message, Typography } from "antd"; 6 | 7 | const { Text } = Typography; 8 | const layout = { 9 | labelCol: { span: 8 }, 10 | wrapperCol: { span: 16 }, 11 | }; 12 | const tailLayout = { 13 | wrapperCol: { offset: 8, span: 16 }, 14 | }; 15 | const { Content, Footer } = Layout; 16 | 17 | const Login = () => { 18 | function handleSubmit(e) { 19 | const username = e.username; 20 | const password = e.password; 21 | //call api 22 | fetch("/api/auth", { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | }, 27 | body: JSON.stringify({ 28 | username, 29 | password, 30 | }), 31 | }) 32 | .then((r) => { 33 | return r.json(); 34 | }) 35 | .then(async (data) => { 36 | if (data && data.error) { 37 | message.error(data.message); 38 | } 39 | if (data && data.token) { 40 | //set cookie 41 | cookie.set("token", data.token, { 42 | expires: 2, 43 | secure: true, 44 | sameSite: "strict", 45 | }); 46 | Router.push("/ats"); 47 | } 48 | }); 49 | } 50 | 51 | useEffect(() => { 52 | Router.prefetch("/ats"); 53 | }, []); 54 | 55 | return ( 56 |
57 | 58 | Login - Simple ATS 59 | 60 | 61 | 62 | 63 | 70 |
81 | 88 | 89 | 90 | 91 | 98 | 99 | 100 | 101 | 102 | 105 | 106 | 107 | Demo Username: guest
Demo Password: guest 108 |
109 |
110 |
111 |
Baykam Say ©2020
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default Login; 118 | -------------------------------------------------------------------------------- /components/jobListings.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import fetch from "isomorphic-unfetch"; 3 | import useSWR, { mutate } from "swr"; 4 | import { 5 | Layout, 6 | List, 7 | Card, 8 | Typography, 9 | Divider, 10 | Button, 11 | Row, 12 | Col, 13 | Spin, 14 | } from "antd"; 15 | import styles from "../styles/ATS.module.css"; 16 | import homeStyle from "../styles/Home.module.css"; 17 | import { PlusOutlined } from "@ant-design/icons"; 18 | import AddJobModal from "./addJobModal"; 19 | import EditJobModal from "./editJobModal"; 20 | 21 | const { Content } = Layout; 22 | const { Title } = Typography; 23 | 24 | export default function JobListings() { 25 | const [newJobModalVisible, setNewJobModalVisible] = useState(false); 26 | const [editJobModalVisible, setEditJobModalVisible] = useState(false); 27 | const [selectedListing, setSelectedListing] = useState({}); 28 | const { data, error } = useSWR("/api/jobs", async function (args) { 29 | const res = await fetch(args); 30 | return res.json(); 31 | }); 32 | if (error) return
failed to load
; 33 | if (!data) 34 | return ( 35 |
36 | 37 |
38 | ); 39 | 40 | return ( 41 | 45 | { 48 | mutate("/api/jobs"); 49 | setNewJobModalVisible(false); 50 | }} 51 | /> 52 | { 56 | mutate("/api/jobs"); 57 | setEditJobModalVisible(false); 58 | }} 59 | /> 60 | 66 | 67 | 68 | 69 | Job Listings 70 | 71 | 72 | 73 | 83 | 84 | 85 | 86 | ( 98 | 99 | { 102 | setSelectedListing(item); 103 | setEditJobModalVisible(true); 104 | }} 105 | title={item.title} 106 | > 107 | {item.description.length <= 88 108 | ? item.description 109 | : `${item.description.substr(0, 85)}...`} 110 | 111 | 112 | )} 113 | /> 114 | 115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /pages/ats.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Head from "next/head"; 3 | import fetch from "isomorphic-unfetch"; 4 | import useSWR from "swr"; 5 | import Router from "next/router"; 6 | import cookie from "js-cookie"; 7 | import { Layout, Menu, Spin, Row, Col, Button } from "antd"; 8 | import styles from "../styles/ATS.module.css"; 9 | import homeStyle from "../styles/Home.module.css"; 10 | import Applicants from "../components/applicants"; 11 | import JobListings from "../components/jobListings"; 12 | 13 | const { Header, Content } = Layout; 14 | 15 | function ATS({ staticProps }) { 16 | const { data, revalidate } = useSWR("/api/me", async function (args) { 17 | const res = await fetch(args); 18 | return res.json(); 19 | }); 20 | const [componentId, setComponentId] = useState("1"); 21 | 22 | useEffect(() => { 23 | if (data && !data.username && !cookie.get("token")) { 24 | Router.replace("/login"); 25 | } 26 | }, [data]); 27 | 28 | if (!data || !data.username) 29 | return ( 30 |
31 | 32 |
33 | ); 34 | 35 | return ( 36 |
37 | 38 | Simple ATS 39 | 40 | 41 | 42 | 43 |
50 | 51 | 52 | Logo 57 | { 62 | setComponentId(item.key); 63 | }} 64 | > 65 | Applicants 66 | Job Listings 67 | Settings 68 | 69 | 70 | 71 | 79 | 80 | 81 |
82 | 87 | {componentId === "1" && } 88 | {componentId === "2" && } 89 | 90 |
91 |
92 | ); 93 | } 94 | 95 | export async function getServerSideProps() { 96 | return fetch(`${process.env.URL}api/jobs`).then( 97 | async (res) => { 98 | const jobs = await res.json(); 99 | const res2 = await fetch(`${process.env.URL}api/pipeline`); 100 | const pipeline = await res2.json(); 101 | return { 102 | props: { 103 | staticProps: { 104 | initialId: jobs[0]._id, 105 | pipeline: pipeline, 106 | }, 107 | }, 108 | }; 109 | }, 110 | () => { 111 | return { 112 | props: { 113 | staticProps: { 114 | initialId: 0, 115 | pipeline: [], 116 | }, 117 | }, 118 | }; 119 | } 120 | ); 121 | } 122 | 123 | export default ATS; 124 | -------------------------------------------------------------------------------- /components/insertApplicantModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Modal, Button, Form, Input, Spin, Select, Upload } from "antd"; 3 | import useSWR from "swr"; 4 | import homeStyle from "../styles/Home.module.css"; 5 | 6 | const layout = { 7 | labelCol: { span: 6 }, 8 | wrapperCol: { span: 18 }, 9 | }; 10 | 11 | const validateMessages = { 12 | required: "This field is required!", 13 | types: { 14 | email: "Not a valid email!", 15 | number: "Not a valid number!", 16 | url: "Not a valid url!", 17 | }, 18 | }; 19 | 20 | const normFile = (e) => { 21 | if (Array.isArray(e)) { 22 | return e; 23 | } 24 | return e && e.fileList; 25 | }; 26 | 27 | export default function InsertApplicantModal(props) { 28 | const [form] = Form.useForm(); 29 | 30 | async function handleSubmit(e) { 31 | const eString = JSON.stringify(e, (k, v) => (v === undefined ? null : v)); 32 | await fetch("/api/applicants", { 33 | method: "post", 34 | body: eString, 35 | }); 36 | form.resetFields(); 37 | props.close(); 38 | } 39 | 40 | const { data, error } = useSWR("/api/jobs", async function (args) { 41 | const res = await fetch(args); 42 | return res.json(); 43 | }); 44 | 45 | if (error) return
failed to load
; 46 | if (!data) 47 | return ( 48 |
49 | 50 |
51 | ); 52 | 53 | return ( 54 | 64 |
71 | 76 | 83 | 84 | 85 | 86 | 87 | 92 | 93 | 94 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 126 | 127 | 128 | 134 | 137 | 138 |
139 |
140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /components/viewApplicantModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Modal, Descriptions, Form, Input, Button, Rate, Select } from "antd"; 3 | import fetch from "isomorphic-unfetch"; 4 | import { saveAs } from "file-saver"; 5 | 6 | export default function ViewApplicantModal(props) { 7 | const [form] = Form.useForm(); 8 | 9 | useEffect(() => { 10 | form.resetFields(); 11 | }, [props.data]); 12 | 13 | async function handleSubmit(e) { 14 | e.id = props.data._id; 15 | await fetch("/api/applicants", { 16 | method: "put", 17 | body: JSON.stringify(e), 18 | }); 19 | props.close(); 20 | } 21 | 22 | async function deleteApplicant() { 23 | await fetch("/api/applicants", { 24 | method: "delete", 25 | body: JSON.stringify(props.data._id), 26 | }); 27 | props.close(); 28 | } 29 | 30 | async function downloadCV() { 31 | const res = await fetch(`/api/cv/${props.data.cv}`); 32 | let resJson = await res.json(); 33 | const arr = new Uint8Array(resJson.file.data); 34 | 35 | let blob = new Blob([arr], { type: "application/pdf" }); 36 | saveAs(blob, "cv.pdf"); 37 | } 38 | 39 | return ( 40 | 50 |
56 | 57 | 58 | 64 | 71 | 72 | 73 | 74 | {props.data.email} 75 | 76 | 77 | 84 | 85 | 86 | {props.data.phone ? ( 87 | {props.data.phone} 88 | ) : ( 89 | "-" 90 | )} 91 | 92 | 93 | {props.data.linkedin ? ( 94 | 95 | {props.data.linkedin} 96 | 97 | ) : ( 98 | "-" 99 | )} 100 | 101 | 102 | {props.data.website ? ( 103 | 104 | {props.data.website} 105 | 106 | ) : ( 107 | "-" 108 | )} 109 | 110 | 111 | {props.data.introduction ? props.data.introduction : "-"} 112 | 113 | 114 | 119 | 123 | 124 | 125 | 126 | 131 | 132 | 133 | 134 | 135 |
136 | 143 | 151 |
152 |
153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | import styles from "../styles/Home.module.css"; 4 | import listingStyles from "../styles/ListingsPage.module.css"; 5 | import { Row, Col, Button } from "antd"; 6 | import { DownOutlined } from "@ant-design/icons"; 7 | 8 | export default function Home() { 9 | return ( 10 |
11 | 12 | Home 13 | 14 | 15 | 16 |
17 | 25 | 30 |

Example Jobs

31 |
32 | Join Us – add small and meaningful slogan here 33 |
34 | 35 | View Open Jobs 36 | 37 | 38 | 43 |

44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed 45 | interdum hendrerit pulvinar. Cras accumsan imperdiet augue in 46 | ornare. Ut ac ex eu leo aliquet porttitor ac id ligula. 47 |

48 |

54 | Nulla pellentesque lorem turpis, in rhoncus urna euismod sit amet. 55 | Aenean pharetra justo felis, quis congue dui faucibus non. Nulla 56 | blandit lacinia dolor, sit amet mattis massa fermentum ac.in. 57 |

58 | 59 |
60 | 61 | 62 |

Our Mission

63 | 64 | 65 |

70 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed 71 | interdum hendrerit pulvinar. Cras accumsan imperdiet augue in 72 | ornare. 73 |

74 | 75 |
76 | 81 | 82 | 83 | 84 | 92 |

Our Team

93 | 100 | Our Team 101 | 102 | Photo by{" "} 103 | 108 | Anna Earl 109 | {" "} 110 | on{" "} 111 | 116 | Unsplash 117 | 118 | 119 | 120 | 121 |
122 | 123 | 124 | Join Us 125 | 126 | 127 |
128 | 129 | 138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /pages/jobs/[id].js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | import styles from "../../styles/Home.module.css"; 4 | import listingStyles from "../../styles/ListingsPage.module.css"; 5 | import { 6 | Row, 7 | Col, 8 | Spin, 9 | Form, 10 | Input, 11 | Button, 12 | Divider, 13 | message, 14 | Upload, 15 | } from "antd"; 16 | import { useRouter } from "next/router"; 17 | import useSWR from "swr"; 18 | import fetch from "isomorphic-unfetch"; 19 | import ReactMarkdown from "react-markdown"; 20 | 21 | const layout = { 22 | labelCol: { span: 6 }, 23 | wrapperCol: { span: 18 }, 24 | }; 25 | 26 | const validateMessages = { 27 | required: "This field is required!", 28 | types: { 29 | email: "Not a valid email!", 30 | number: "Not a valid number!", 31 | url: "Not a valid url!", 32 | }, 33 | }; 34 | 35 | const normFile = (e) => { 36 | if (Array.isArray(e)) { 37 | return e; 38 | } 39 | return e && e.fileList; 40 | }; 41 | 42 | function Jobs() { 43 | const [form] = Form.useForm(); 44 | const router = useRouter(); 45 | const { id } = router.query; 46 | 47 | const { data, error } = useSWR(`/api/jobListing/${id}`, async function ( 48 | args 49 | ) { 50 | const res = await fetch(args); 51 | return res.json(); 52 | }); 53 | 54 | async function handleSubmit(e) { 55 | e.listing = id; 56 | const eString = JSON.stringify(e, (k, v) => (v === undefined ? null : v)); 57 | await fetch("/api/applicants", { 58 | method: "post", 59 | body: eString, 60 | }); 61 | form.resetFields(); 62 | message.success("Your application has been submitted"); 63 | } 64 | 65 | if (error) 66 | return ( 67 |
68 | This page does not exist,{" "} 69 | 70 | click here to go back. 71 | 72 |
73 | ); 74 | if (!data) 75 | return ( 76 |
77 | 78 |
79 | ); 80 | 81 | return ( 82 |
83 | 84 | Jobs 85 | 86 | 87 | 88 |
89 | 90 | 91 |

{data.title}

92 |
{data.location}
93 | 94 | 95 |

Apply For This Job

96 |
104 | 105 | 106 | 107 | 112 | 113 | 114 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 133 | 134 | 135 | 140 | 141 | 142 | 154 | 155 | 156 | 162 | 165 | 166 |
167 | 168 |
169 |
170 | 171 | 180 |
181 | ); 182 | } 183 | 184 | export default Jobs; 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2020 Baykam Say 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. --------------------------------------------------------------------------------