├── .env.temp ├── .gitignore ├── JOBIFY.postman_collection.json ├── README.MD ├── client ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── _redirects │ ├── data.json │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── assets │ ├── css │ │ └── index.css │ ├── images │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── main-alternative.svg │ │ ├── main.svg │ │ └── not-found.svg │ └── wrappers │ │ ├── BigSidebar.js │ │ ├── ChartsContainer.js │ │ ├── DashboardFormPage.js │ │ ├── ErrorPage.js │ │ ├── Job.js │ │ ├── JobInfo.js │ │ ├── JobsContainer.js │ │ ├── LandingPage.js │ │ ├── Navbar.js │ │ ├── PageBtnContainer.js │ │ ├── RegisterPage.js │ │ ├── SearchContainer.js │ │ ├── SharedLayout.js │ │ ├── SmallSidebar.js │ │ ├── StatItem.js │ │ ├── StatsContainer.js │ │ └── Testing.js │ ├── components │ ├── Alert.js │ ├── AreaChart.js │ ├── BarChart.js │ ├── BigSidebar.js │ ├── ChartsContainer.js │ ├── FormRow.js │ ├── FormRowSelect.js │ ├── Job.js │ ├── JobInfo.js │ ├── JobsContainer.js │ ├── Loading.js │ ├── Logo.js │ ├── NavLinks.js │ ├── Navbar.js │ ├── PageBtnContainer.js │ ├── SearchContainer.js │ ├── SmallSidebar.js │ ├── StatItem.js │ ├── StatsContainer.js │ └── index.js │ ├── context │ ├── actions.js │ ├── appContext.js │ └── reducer.js │ ├── index.css │ ├── index.js │ ├── pages │ ├── Error.js │ ├── Landing.js │ ├── ProtectedRoute.js │ ├── Register.js │ ├── dashboard │ │ ├── AddJob.js │ │ ├── AllJobs.js │ │ ├── Profile.js │ │ ├── SharedLayout.js │ │ ├── Stats.js │ │ └── index.js │ └── index.js │ └── utils │ └── links.js ├── controllers ├── authController.js └── jobsController.js ├── db └── connect.js ├── errors ├── bad-request.js ├── custom-api.js ├── index.js ├── not-found.js └── unauthenticated.js ├── middleware ├── auth.js ├── error-handler.js ├── not-found.js └── testUser.js ├── mock-data.json ├── models ├── Job.js └── User.js ├── package-lock.json ├── package.json ├── populate.js ├── routes ├── authRoutes.js └── jobsRoutes.js ├── server.js └── utils ├── attachCookie.js └── checkPermissions.js /.env.temp: -------------------------------------------------------------------------------- 1 | MONGO_URL='your connection string' 2 | JWT_SECRET=jwtSecret 3 | JWT_LIFETIME=1d -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env -------------------------------------------------------------------------------- /JOBIFY.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "76f6c6e1-820c-4eed-bf01-5b96894d9749", 4 | "name": "JOBIFY", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "18152321" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Jobs", 11 | "item": [ 12 | { 13 | "name": "Create Job", 14 | "request": { 15 | "auth": { 16 | "type": "bearer", 17 | "bearer": [ 18 | { 19 | "key": "token", 20 | "value": "{{token}}", 21 | "type": "string" 22 | } 23 | ] 24 | }, 25 | "method": "POST", 26 | "header": [], 27 | "body": { 28 | "mode": "raw", 29 | "raw": "{\n \"company\":\"hulu\",\"position\":\"testing\"\n}", 30 | "options": { 31 | "raw": { 32 | "language": "json" 33 | } 34 | } 35 | }, 36 | "url": { 37 | "raw": "{{URL}}/jobs", 38 | "host": [ 39 | "{{URL}}" 40 | ], 41 | "path": [ 42 | "jobs" 43 | ] 44 | } 45 | }, 46 | "response": [] 47 | }, 48 | { 49 | "name": "Get All Jobs", 50 | "request": { 51 | "auth": { 52 | "type": "bearer", 53 | "bearer": [ 54 | { 55 | "key": "token", 56 | "value": "{{token}}", 57 | "type": "string" 58 | } 59 | ] 60 | }, 61 | "method": "GET", 62 | "header": [], 63 | "url": { 64 | "raw": "{{URL}}/jobs?status=all&jobType=all&sort=latest&page=1&limit=25", 65 | "host": [ 66 | "{{URL}}" 67 | ], 68 | "path": [ 69 | "jobs" 70 | ], 71 | "query": [ 72 | { 73 | "key": "status", 74 | "value": "all" 75 | }, 76 | { 77 | "key": "jobType", 78 | "value": "all" 79 | }, 80 | { 81 | "key": "search", 82 | "value": "", 83 | "disabled": true 84 | }, 85 | { 86 | "key": "sort", 87 | "value": "latest" 88 | }, 89 | { 90 | "key": "page", 91 | "value": "1" 92 | }, 93 | { 94 | "key": "limit", 95 | "value": "25" 96 | } 97 | ] 98 | } 99 | }, 100 | "response": [] 101 | }, 102 | { 103 | "name": "Show Stats", 104 | "request": { 105 | "auth": { 106 | "type": "bearer", 107 | "bearer": [ 108 | { 109 | "key": "token", 110 | "value": "{{token}}", 111 | "type": "string" 112 | } 113 | ] 114 | }, 115 | "method": "GET", 116 | "header": [], 117 | "url": { 118 | "raw": "{{URL}}/jobs/stats", 119 | "host": [ 120 | "{{URL}}" 121 | ], 122 | "path": [ 123 | "jobs", 124 | "stats" 125 | ] 126 | } 127 | }, 128 | "response": [] 129 | }, 130 | { 131 | "name": "Delete Job", 132 | "request": { 133 | "auth": { 134 | "type": "bearer", 135 | "bearer": [ 136 | { 137 | "key": "token", 138 | "value": "{{token}}", 139 | "type": "string" 140 | } 141 | ] 142 | }, 143 | "method": "DELETE", 144 | "header": [], 145 | "url": { 146 | "raw": "{{URL}}/jobs/61b13c009315ca467f020c65", 147 | "host": [ 148 | "{{URL}}" 149 | ], 150 | "path": [ 151 | "jobs", 152 | "61b13c009315ca467f020c65" 153 | ] 154 | } 155 | }, 156 | "response": [] 157 | }, 158 | { 159 | "name": "Update Job", 160 | "request": { 161 | "auth": { 162 | "type": "bearer", 163 | "bearer": [ 164 | { 165 | "key": "token", 166 | "value": "{{token}}", 167 | "type": "string" 168 | } 169 | ] 170 | }, 171 | "method": "PATCH", 172 | "header": [], 173 | "body": { 174 | "mode": "raw", 175 | "raw": "{\n \"company\": \"wow this works!\",\n \"position\": \"backend developer\"\n}", 176 | "options": { 177 | "raw": { 178 | "language": "json" 179 | } 180 | } 181 | }, 182 | "url": { 183 | "raw": "{{URL}}/jobs/61bf64374c66746131fe74c9", 184 | "host": [ 185 | "{{URL}}" 186 | ], 187 | "path": [ 188 | "jobs", 189 | "61bf64374c66746131fe74c9" 190 | ] 191 | } 192 | }, 193 | "response": [] 194 | } 195 | ] 196 | }, 197 | { 198 | "name": "Auth", 199 | "item": [ 200 | { 201 | "name": "Register User", 202 | "event": [ 203 | { 204 | "listen": "test", 205 | "script": { 206 | "exec": [ 207 | "const jsonData = pm.response.json()", 208 | "pm.globals.set(\"token\", jsonData.token);" 209 | ], 210 | "type": "text/javascript" 211 | } 212 | } 213 | ], 214 | "request": { 215 | "method": "POST", 216 | "header": [], 217 | "body": { 218 | "mode": "raw", 219 | "raw": "{\n\"name\":\"john\",\n\"password\":\"secret\",\n \"email\":\"anna@gmail.com\" \n}", 220 | "options": { 221 | "raw": { 222 | "language": "json" 223 | } 224 | } 225 | }, 226 | "url": { 227 | "raw": "{{URL}}/auth/register", 228 | "host": [ 229 | "{{URL}}" 230 | ], 231 | "path": [ 232 | "auth", 233 | "register" 234 | ] 235 | } 236 | }, 237 | "response": [] 238 | }, 239 | { 240 | "name": "Login User", 241 | "event": [ 242 | { 243 | "listen": "test", 244 | "script": { 245 | "exec": [ 246 | "const jsonData = pm.response.json()", 247 | "pm.globals.set(\"token\", jsonData.token);" 248 | ], 249 | "type": "text/javascript" 250 | } 251 | } 252 | ], 253 | "request": { 254 | "method": "POST", 255 | "header": [], 256 | "body": { 257 | "mode": "raw", 258 | "raw": "{\n \"email\":\"john@gmail.com\",\n \"password\":\"secret\"\n}", 259 | "options": { 260 | "raw": { 261 | "language": "json" 262 | } 263 | } 264 | }, 265 | "url": { 266 | "raw": "{{URL}}/auth/login", 267 | "host": [ 268 | "{{URL}}" 269 | ], 270 | "path": [ 271 | "auth", 272 | "login" 273 | ] 274 | } 275 | }, 276 | "response": [] 277 | }, 278 | { 279 | "name": "Update User", 280 | "request": { 281 | "auth": { 282 | "type": "bearer", 283 | "bearer": [ 284 | { 285 | "key": "token", 286 | "value": "{{token}}", 287 | "type": "string" 288 | } 289 | ] 290 | }, 291 | "method": "PATCH", 292 | "header": [], 293 | "body": { 294 | "mode": "raw", 295 | "raw": "{\n \"name\": \"peter\",\n \"email\": \"john@gmail.com\",\n \"lastName\": \"lastName\",\n \"location\": \"my city\"\n}", 296 | "options": { 297 | "raw": { 298 | "language": "json" 299 | } 300 | } 301 | }, 302 | "url": { 303 | "raw": "{{URL}}/auth/updateUser", 304 | "host": [ 305 | "{{URL}}" 306 | ], 307 | "path": [ 308 | "auth", 309 | "updateUser" 310 | ] 311 | } 312 | }, 313 | "response": [] 314 | } 315 | ] 316 | } 317 | ] 318 | } -------------------------------------------------------------------------------- /client/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.2.1", 10 | "moment": "^2.29.4", 11 | "normalize.css": "^8.0.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-icons": "^4.7.1", 15 | "react-router-dom": "^6.4.4", 16 | "react-scripts": "5.0.1", 17 | "recharts": "^2.1.16", 18 | "styled-components": "^5.3.6", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "CI= react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "proxy": "http://localhost:5000" 46 | } 47 | -------------------------------------------------------------------------------- /client/public/_redirects: -------------------------------------------------------------------------------- 1 | /api/* https://mern-course-jobify-e4jv.onrender.com/api/:splat 200 2 | 3 | /* /index.html 200 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/public/data.json: -------------------------------------------------------------------------------- 1 | { "name": "john" } 2 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/mern-course-jobify/edd98b3f3d10ecd0a1c57e7835751e40d0a28c77/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Jobify 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/mern-course-jobify/edd98b3f3d10ecd0a1c57e7835751e40d0a28c77/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/mern-course-jobify/edd98b3f3d10ecd0a1c57e7835751e40d0a28c77/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom' 2 | import { Register, Landing, Error, ProtectedRoute } from './pages' 3 | import { 4 | AllJobs, 5 | Profile, 6 | SharedLayout, 7 | Stats, 8 | AddJob, 9 | } from './pages/dashboard' 10 | 11 | function App() { 12 | return ( 13 | 14 | 15 | 19 | 20 | 21 | } 22 | > 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | 28 | } /> 29 | } /> 30 | } /> 31 | 32 | 33 | ) 34 | } 35 | 36 | export default App 37 | -------------------------------------------------------------------------------- /client/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::after, 3 | ::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | /* fonts */ 8 | @import url('https://fonts.googleapis.com/css2?family=Cabin&family=Roboto+Condensed:wght@400;700&display=swap'); 9 | 10 | html { 11 | font-size: 100%; 12 | } /*16px*/ 13 | 14 | :root { 15 | /* colors */ 16 | --primary-50: #e0fcff; 17 | --primary-100: #bef8fd; 18 | --primary-200: #87eaf2; 19 | --primary-300: #54d1db; 20 | --primary-400: #38bec9; 21 | --primary-500: #2cb1bc; 22 | --primary-600: #14919b; 23 | --primary-700: #0e7c86; 24 | --primary-800: #0a6c74; 25 | --primary-900: #044e54; 26 | 27 | /* grey */ 28 | --grey-50: #f0f4f8; 29 | --grey-100: #d9e2ec; 30 | --grey-200: #bcccdc; 31 | --grey-300: #9fb3c8; 32 | --grey-400: #829ab1; 33 | --grey-500: #627d98; 34 | --grey-600: #486581; 35 | --grey-700: #334e68; 36 | --grey-800: #243b53; 37 | --grey-900: #102a43; 38 | /* rest of the colors */ 39 | --black: #222; 40 | --white: #fff; 41 | --red-light: #f8d7da; 42 | --red-dark: #842029; 43 | --green-light: #d1e7dd; 44 | --green-dark: #0f5132; 45 | 46 | /* fonts */ 47 | --headingFont: 'Roboto Condensed', Sans-Serif; 48 | --bodyFont: 'Cabin', Sans-Serif; 49 | --small-text: 0.875rem; 50 | --extra-small-text: 0.7em; 51 | /* rest of the vars */ 52 | --backgroundColor: var(--grey-50); 53 | --textColor: var(--grey-900); 54 | --borderRadius: 0.25rem; 55 | --letterSpacing: 1px; 56 | --transition: 0.3s ease-in-out all; 57 | --max-width: 1120px; 58 | --fixed-width: 500px; 59 | --fluid-width: 90vw; 60 | --breakpoint-lg: 992px; 61 | --nav-height: 6rem; 62 | /* box shadow*/ 63 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 64 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 65 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 66 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 67 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 68 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 69 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 70 | } 71 | 72 | body { 73 | background: var(--backgroundColor); 74 | font-family: var(--bodyFont); 75 | font-weight: 400; 76 | line-height: 1.75; 77 | color: var(--textColor); 78 | } 79 | 80 | p { 81 | margin-bottom: 1.5rem; 82 | max-width: 40em; 83 | } 84 | 85 | h1, 86 | h2, 87 | h3, 88 | h4, 89 | h5 { 90 | margin: 0; 91 | margin-bottom: 1.38rem; 92 | font-family: var(--headingFont); 93 | font-weight: 400; 94 | line-height: 1.3; 95 | text-transform: capitalize; 96 | letter-spacing: var(--letterSpacing); 97 | } 98 | 99 | h1 { 100 | margin-top: 0; 101 | font-size: 3.052rem; 102 | } 103 | 104 | h2 { 105 | font-size: 2.441rem; 106 | } 107 | 108 | h3 { 109 | font-size: 1.953rem; 110 | } 111 | 112 | h4 { 113 | font-size: 1.563rem; 114 | } 115 | 116 | h5 { 117 | font-size: 1.25rem; 118 | } 119 | 120 | small, 121 | .text-small { 122 | font-size: var(--small-text); 123 | } 124 | 125 | a { 126 | text-decoration: none; 127 | letter-spacing: var(--letterSpacing); 128 | } 129 | a, 130 | button { 131 | line-height: 1.15; 132 | } 133 | button:disabled { 134 | cursor: not-allowed; 135 | } 136 | ul { 137 | list-style-type: none; 138 | padding: 0; 139 | } 140 | 141 | .img { 142 | width: 100%; 143 | display: block; 144 | object-fit: cover; 145 | } 146 | /* buttons */ 147 | 148 | .btn { 149 | cursor: pointer; 150 | color: var(--white); 151 | background: var(--primary-500); 152 | border: transparent; 153 | border-radius: var(--borderRadius); 154 | letter-spacing: var(--letterSpacing); 155 | padding: 0.375rem 0.75rem; 156 | box-shadow: var(--shadow-2); 157 | transition: var(--transition); 158 | text-transform: capitalize; 159 | display: inline-block; 160 | } 161 | .btn:hover { 162 | background: var(--primary-700); 163 | box-shadow: var(--shadow-3); 164 | } 165 | .btn-hipster { 166 | color: var(--primary-500); 167 | background: var(--primary-200); 168 | } 169 | .btn-hipster:hover { 170 | color: var(--primary-200); 171 | background: var(--primary-700); 172 | } 173 | .btn-block { 174 | width: 100%; 175 | } 176 | .btn-hero { 177 | font-size: 1.25rem; 178 | padding: 0.5rem 1.25rem; 179 | } 180 | .btn-danger { 181 | background: var(--red-light); 182 | color: var(--red-dark); 183 | } 184 | .btn-danger:hover { 185 | background: var(--red-dark); 186 | color: var(--white); 187 | } 188 | /* alerts */ 189 | .alert { 190 | padding: 0.375rem 0.75rem; 191 | margin-bottom: 1rem; 192 | border-color: transparent; 193 | border-radius: var(--borderRadius); 194 | text-align: center; 195 | letter-spacing: var(--letterSpacing); 196 | } 197 | 198 | .alert-danger { 199 | color: var(--red-dark); 200 | background: var(--red-light); 201 | } 202 | .alert-success { 203 | color: var(--green-dark); 204 | background: var(--green-light); 205 | } 206 | /* form */ 207 | 208 | .form { 209 | width: 90vw; 210 | max-width: var(--fixed-width); 211 | background: var(--white); 212 | border-radius: var(--borderRadius); 213 | box-shadow: var(--shadow-2); 214 | padding: 2rem 2.5rem; 215 | margin: 3rem auto; 216 | transition: var(--transition); 217 | } 218 | .form:hover { 219 | box-shadow: var(--shadow-4); 220 | } 221 | .form-label { 222 | display: block; 223 | font-size: var(--smallText); 224 | margin-bottom: 0.5rem; 225 | text-transform: capitalize; 226 | letter-spacing: var(--letterSpacing); 227 | } 228 | .form-input, 229 | .form-textarea, 230 | .form-select { 231 | width: 100%; 232 | padding: 0.375rem 0.75rem; 233 | border-radius: var(--borderRadius); 234 | background: var(--backgroundColor); 235 | border: 1px solid var(--grey-200); 236 | } 237 | .form-input, 238 | .form-select, 239 | .btn-block { 240 | height: 35px; 241 | } 242 | .form-row { 243 | margin-bottom: 1rem; 244 | } 245 | 246 | .form-textarea { 247 | height: 7rem; 248 | } 249 | ::placeholder { 250 | font-family: inherit; 251 | color: var(--grey-400); 252 | } 253 | .form-alert { 254 | color: var(--red-dark); 255 | letter-spacing: var(--letterSpacing); 256 | text-transform: capitalize; 257 | } 258 | /* alert */ 259 | 260 | @keyframes spinner { 261 | to { 262 | transform: rotate(360deg); 263 | } 264 | } 265 | 266 | .loading { 267 | width: 6rem; 268 | height: 6rem; 269 | border: 5px solid var(--grey-400); 270 | border-radius: 50%; 271 | border-top-color: var(--primary-500); 272 | animation: spinner 2s linear infinite; 273 | } 274 | .loading-center { 275 | margin: 0 auto; 276 | } 277 | /* title */ 278 | 279 | .title { 280 | text-align: center; 281 | } 282 | 283 | .title-underline { 284 | background: var(--primary-500); 285 | width: 7rem; 286 | height: 0.25rem; 287 | margin: 0 auto; 288 | margin-top: -1rem; 289 | } 290 | 291 | .container { 292 | width: var(--fluid-width); 293 | max-width: var(--max-width); 294 | margin: 0 auto; 295 | } 296 | .full-page { 297 | min-height: 100vh; 298 | } 299 | 300 | .coffee-info { 301 | text-align: center; 302 | text-transform: capitalize; 303 | margin-bottom: 1rem; 304 | letter-spacing: var(--letterSpacing); 305 | } 306 | .coffee-info span { 307 | display: block; 308 | } 309 | .coffee-info a { 310 | color: var(--primary-500); 311 | } 312 | 313 | @media screen and (min-width: 992px) { 314 | .coffee-info { 315 | text-align: left; 316 | } 317 | .coffee-info span { 318 | display: inline-block; 319 | margin-right: 0.5rem; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /client/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/mern-course-jobify/edd98b3f3d10ecd0a1c57e7835751e40d0a28c77/client/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/src/assets/images/main-alternative.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/images/main.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/images/not-found.svg: -------------------------------------------------------------------------------- 1 | page not found -------------------------------------------------------------------------------- /client/src/assets/wrappers/BigSidebar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.aside` 4 | display: none; 5 | @media (min-width: 992px) { 6 | display: block; 7 | box-shadow: 1px 0px 0px 0px rgba(0, 0, 0, 0.1); 8 | .sidebar-container { 9 | background: var(--white); 10 | min-height: 100vh; 11 | height: 100%; 12 | width: 250px; 13 | margin-left: -250px; 14 | transition: var(--transition); 15 | } 16 | .content { 17 | position: sticky; 18 | top: 0; 19 | } 20 | .show-sidebar { 21 | margin-left: 0; 22 | } 23 | header { 24 | height: 6rem; 25 | display: flex; 26 | align-items: center; 27 | padding-left: 2.5rem; 28 | } 29 | .nav-links { 30 | padding-top: 2rem; 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | .nav-link { 35 | display: flex; 36 | align-items: center; 37 | color: var(--grey-500); 38 | padding: 1rem 0; 39 | padding-left: 2.5rem; 40 | text-transform: capitalize; 41 | transition: var(--transition); 42 | } 43 | .nav-link:hover { 44 | background: var(--grey-50); 45 | padding-left: 3rem; 46 | color: var(--grey-900); 47 | } 48 | .nav-link:hover .icon { 49 | color: var(--primary-500); 50 | } 51 | .icon { 52 | font-size: 1.5rem; 53 | margin-right: 1rem; 54 | display: grid; 55 | place-items: center; 56 | transition: var(--transition); 57 | } 58 | .active { 59 | color: var(--grey-900); 60 | } 61 | .active .icon { 62 | color: var(--primary-500); 63 | } 64 | } 65 | ` 66 | export default Wrapper 67 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/ChartsContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | margin-top: 4rem; 5 | text-align: center; 6 | button { 7 | background: transparent; 8 | border-color: transparent; 9 | text-transform: capitalize; 10 | color: var(--primary-500); 11 | font-size: 1.25rem; 12 | cursor: pointer; 13 | } 14 | h4 { 15 | text-align: center; 16 | margin-bottom: 0.75rem; 17 | } 18 | ` 19 | 20 | export default Wrapper 21 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/DashboardFormPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | border-radius: var(--borderRadius); 5 | width: 100%; 6 | background: var(--white); 7 | padding: 3rem 2rem 4rem; 8 | box-shadow: var(--shadow-2); 9 | h3 { 10 | margin-top: 0; 11 | } 12 | .form { 13 | margin: 0; 14 | border-radius: 0; 15 | box-shadow: none; 16 | padding: 0; 17 | max-width: 100%; 18 | width: 100%; 19 | } 20 | .form-row { 21 | margin-bottom: 0; 22 | } 23 | .form-center { 24 | display: grid; 25 | row-gap: 0.5rem; 26 | } 27 | .form-center button { 28 | align-self: end; 29 | height: 35px; 30 | margin-top: 1rem; 31 | } 32 | .btn-container { 33 | display: grid; 34 | grid-template-columns: 1fr 1fr; 35 | column-gap: 1rem; 36 | align-self: flex-end; 37 | margin-top: 0.5rem; 38 | button { 39 | height: 35px; 40 | } 41 | } 42 | .clear-btn { 43 | background: var(--grey-500); 44 | } 45 | .clear-btn:hover { 46 | background: var(--black); 47 | } 48 | @media (min-width: 992px) { 49 | .form-center { 50 | grid-template-columns: 1fr 1fr; 51 | align-items: center; 52 | column-gap: 1rem; 53 | } 54 | .btn-container { 55 | margin-top: 0; 56 | } 57 | } 58 | @media (min-width: 1120px) { 59 | .form-center { 60 | grid-template-columns: 1fr 1fr 1fr; 61 | } 62 | .form-center button { 63 | margin-top: 0; 64 | } 65 | } 66 | ` 67 | 68 | export default Wrapper 69 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.main` 4 | text-align: center; 5 | img { 6 | max-width: 600px; 7 | display: block; 8 | margin-bottom: 2rem; 9 | } 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | h3 { 14 | margin-bottom: 0.5rem; 15 | } 16 | p { 17 | margin-top: 0; 18 | margin-bottom: 0.5rem; 19 | color: var(--grey-500); 20 | } 21 | a { 22 | color: var(--primary-500); 23 | text-decoration: underline; 24 | text-transform: capitalize; 25 | } 26 | ` 27 | 28 | export default Wrapper 29 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/Job.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.article` 4 | background: var(--white); 5 | border-radius: var(--borderRadius); 6 | display: grid; 7 | grid-template-rows: 1fr auto; 8 | box-shadow: var(--shadow-2); 9 | 10 | header { 11 | padding: 1rem 1.5rem; 12 | border-bottom: 1px solid var(--grey-100); 13 | display: grid; 14 | grid-template-columns: auto 1fr; 15 | align-items: center; 16 | h5 { 17 | letter-spacing: 0; 18 | } 19 | } 20 | .main-icon { 21 | width: 60px; 22 | height: 60px; 23 | display: grid; 24 | place-items: center; 25 | background: var(--primary-500); 26 | border-radius: var(--borderRadius); 27 | font-size: 1.5rem; 28 | font-weight: 700; 29 | text-transform: uppercase; 30 | color: var(--white); 31 | margin-right: 2rem; 32 | } 33 | .info { 34 | h5 { 35 | margin-bottom: 0.25rem; 36 | } 37 | p { 38 | margin: 0; 39 | text-transform: capitalize; 40 | color: var(--grey-400); 41 | letter-spacing: var(--letterSpacing); 42 | } 43 | } 44 | .pending { 45 | background: #fcefc7; 46 | color: #e9b949; 47 | } 48 | .interview { 49 | background: #e0e8f9; 50 | color: #647acb; 51 | } 52 | .declined { 53 | color: #d66a6a; 54 | background: #ffeeee; 55 | } 56 | .content { 57 | padding: 1rem 1.5rem; 58 | } 59 | .content-center { 60 | display: grid; 61 | grid-template-columns: 1fr; 62 | row-gap: 0.5rem; 63 | @media (min-width: 576px) { 64 | grid-template-columns: 1fr 1fr; 65 | } 66 | @media (min-width: 992px) { 67 | grid-template-columns: 1fr; 68 | } 69 | @media (min-width: 1120px) { 70 | grid-template-columns: 1fr 1fr; 71 | } 72 | } 73 | 74 | .status { 75 | border-radius: var(--borderRadius); 76 | text-transform: capitalize; 77 | letter-spacing: var(--letterSpacing); 78 | text-align: center; 79 | width: 100px; 80 | height: 30px; 81 | } 82 | footer { 83 | margin-top: 1rem; 84 | } 85 | .edit-btn, 86 | .delete-btn { 87 | letter-spacing: var(--letterSpacing); 88 | cursor: pointer; 89 | height: 30px; 90 | } 91 | .edit-btn { 92 | color: var(--green-dark); 93 | background: var(--green-light); 94 | margin-right: 0.5rem; 95 | } 96 | .delete-btn { 97 | color: var(--red-dark); 98 | background: var(--red-light); 99 | } 100 | &:hover .actions { 101 | visibility: visible; 102 | } 103 | ` 104 | 105 | export default Wrapper 106 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/JobInfo.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.div` 4 | margin-top: 0.5rem; 5 | display: flex; 6 | align-items: center; 7 | 8 | .icon { 9 | font-size: 1rem; 10 | margin-right: 1rem; 11 | display: flex; 12 | align-items: center; 13 | svg { 14 | color: var(--grey-400); 15 | } 16 | } 17 | .text { 18 | text-transform: capitalize; 19 | letter-spacing: var(--letterSpacing); 20 | } 21 | ` 22 | export default Wrapper 23 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/JobsContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | margin-top: 4rem; 5 | h2 { 6 | text-transform: none; 7 | } 8 | & > h5 { 9 | font-weight: 700; 10 | } 11 | .jobs { 12 | display: grid; 13 | grid-template-columns: 1fr; 14 | row-gap: 2rem; 15 | } 16 | @media (min-width: 992px) { 17 | .jobs { 18 | display: grid; 19 | grid-template-columns: 1fr 1fr; 20 | gap: 1rem; 21 | } 22 | } 23 | ` 24 | export default Wrapper 25 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/LandingPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.main` 4 | nav { 5 | width: var(--fluid-width); 6 | max-width: var(--max-width); 7 | margin: 0 auto; 8 | height: var(--nav-height); 9 | display: flex; 10 | align-items: center; 11 | } 12 | .page { 13 | min-height: calc(100vh - var(--nav-height)); 14 | display: grid; 15 | align-items: center; 16 | margin-top: -3rem; 17 | } 18 | h1 { 19 | font-weight: 700; 20 | span { 21 | color: var(--primary-500); 22 | } 23 | } 24 | p { 25 | color: var(--grey-600); 26 | } 27 | .main-img { 28 | display: none; 29 | } 30 | @media (min-width: 992px) { 31 | .page { 32 | grid-template-columns: 1fr 1fr; 33 | column-gap: 3rem; 34 | } 35 | .main-img { 36 | display: block; 37 | } 38 | } 39 | ` 40 | export default Wrapper 41 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/Navbar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.nav` 4 | height: var(--nav-height); 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | box-shadow: 0 1px 0px 0px rgba(0, 0, 0, 0.1); 9 | .logo { 10 | display: flex; 11 | align-items: center; 12 | width: 100px; 13 | } 14 | .nav-center { 15 | display: flex; 16 | width: 90vw; 17 | align-items: center; 18 | justify-content: space-between; 19 | } 20 | .toggle-btn { 21 | background: transparent; 22 | border-color: transparent; 23 | font-size: 1.75rem; 24 | color: var(--primary-500); 25 | cursor: pointer; 26 | display: flex; 27 | align-items: center; 28 | } 29 | background: var(--white); 30 | .btn-container { 31 | position: relative; 32 | } 33 | .btn { 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | gap: 0 0.5rem; 38 | position: relative; 39 | box-shadow: var(--shadow-2); 40 | } 41 | 42 | .dropdown { 43 | position: absolute; 44 | top: 40px; 45 | left: 0; 46 | width: 100%; 47 | background: var(--primary-100); 48 | box-shadow: var(--shadow-2); 49 | padding: 0.5rem; 50 | text-align: center; 51 | visibility: hidden; 52 | border-radius: var(--borderRadius); 53 | } 54 | .show-dropdown { 55 | visibility: visible; 56 | } 57 | .dropdown-btn { 58 | background: transparent; 59 | border-color: transparent; 60 | color: var(--primary-500); 61 | letter-spacing: var(--letterSpacing); 62 | text-transform: capitalize; 63 | cursor: pointer; 64 | } 65 | .logo-text { 66 | display: none; 67 | margin: 0; 68 | } 69 | @media (min-width: 992px) { 70 | position: sticky; 71 | top: 0; 72 | 73 | .nav-center { 74 | width: 90%; 75 | } 76 | .logo { 77 | display: none; 78 | } 79 | .logo-text { 80 | display: block; 81 | } 82 | } 83 | ` 84 | export default Wrapper 85 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/PageBtnContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | height: 6rem; 5 | margin-top: 2rem; 6 | display: flex; 7 | align-items: center; 8 | justify-content: end; 9 | flex-wrap: wrap; 10 | gap: 1rem; 11 | .btn-container { 12 | background: var(--primary-100); 13 | border-radius: var(--borderRadius); 14 | } 15 | .pageBtn { 16 | background: transparent; 17 | border-color: transparent; 18 | width: 50px; 19 | height: 40px; 20 | font-weight: 700; 21 | font-size: 1.25rem; 22 | color: var(--primary-500); 23 | transition: var(--transition); 24 | border-radius: var(--borderRadius); 25 | cursor: pointer; 26 | } 27 | .active { 28 | background: var(--primary-500); 29 | color: var(--white); 30 | } 31 | .prev-btn, 32 | .next-btn { 33 | width: 100px; 34 | height: 40px; 35 | background: var(--white); 36 | border-color: transparent; 37 | border-radius: var(--borderRadius); 38 | color: var(--primary-500); 39 | text-transform: capitalize; 40 | letter-spacing: var(--letterSpacing); 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | gap: 0.5rem; 45 | cursor: pointer; 46 | transition: var(--transition); 47 | } 48 | .prev-btn:hover, 49 | .next-btn:hover { 50 | background: var(--primary-500); 51 | color: var(--white); 52 | } 53 | ` 54 | export default Wrapper 55 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/RegisterPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | display: grid; 5 | align-items: center; 6 | .logo { 7 | display: block; 8 | margin: 0 auto; 9 | margin-bottom: 1.38rem; 10 | } 11 | .form { 12 | max-width: 400px; 13 | border-top: 5px solid var(--primary-500); 14 | } 15 | 16 | h3 { 17 | text-align: center; 18 | } 19 | p { 20 | margin: 0; 21 | margin-top: 1rem; 22 | text-align: center; 23 | } 24 | .btn { 25 | margin-top: 1rem; 26 | } 27 | .member-btn { 28 | background: transparent; 29 | border: transparent; 30 | color: var(--primary-500); 31 | cursor: pointer; 32 | letter-spacing: var(--letterSpacing); 33 | } 34 | ` 35 | export default Wrapper 36 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/SearchContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | .form { 5 | width: 100%; 6 | max-width: 100%; 7 | } 8 | .form-input, 9 | .form-select, 10 | .btn-block { 11 | height: 35px; 12 | } 13 | .form-row { 14 | margin-bottom: 0; 15 | } 16 | .form-center { 17 | display: grid; 18 | grid-template-columns: 1fr; 19 | column-gap: 2rem; 20 | row-gap: 0.5rem; 21 | } 22 | h5 { 23 | font-weight: 700; 24 | } 25 | .btn-block { 26 | align-self: end; 27 | margin-top: 1rem; 28 | } 29 | @media (min-width: 768px) { 30 | .form-center { 31 | grid-template-columns: 1fr 1fr; 32 | } 33 | } 34 | @media (min-width: 992px) { 35 | .form-center { 36 | grid-template-columns: 1fr 1fr 1fr; 37 | } 38 | .btn-block { 39 | margin-top: 0; 40 | } 41 | } 42 | ` 43 | 44 | export default Wrapper 45 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/SharedLayout.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | .dashboard { 5 | display: grid; 6 | grid-template-columns: 1fr; 7 | } 8 | .dashboard-page { 9 | width: 90vw; 10 | margin: 0 auto; 11 | padding: 2rem 0; 12 | } 13 | @media (min-width: 992px) { 14 | .dashboard { 15 | grid-template-columns: auto 1fr; 16 | } 17 | .dashboard-page { 18 | width: 90%; 19 | } 20 | } 21 | ` 22 | export default Wrapper 23 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/SmallSidebar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.aside` 4 | @media (min-width: 992px) { 5 | display: none; 6 | } 7 | .sidebar-container { 8 | position: fixed; 9 | inset: 0; 10 | background: rgba(0, 0, 0, 0.7); 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | z-index: -1; 15 | opacity: 0; 16 | transition: var(--transition); 17 | } 18 | .show-sidebar { 19 | z-index: 99; 20 | opacity: 1; 21 | } 22 | .content { 23 | background: var(--white); 24 | width: var(--fluid-width); 25 | height: 95vh; 26 | border-radius: var(--borderRadius); 27 | padding: 4rem 2rem; 28 | position: relative; 29 | display: flex; 30 | align-items: center; 31 | flex-direction: column; 32 | } 33 | .close-btn { 34 | position: absolute; 35 | top: 10px; 36 | left: 10px; 37 | background: transparent; 38 | border-color: transparent; 39 | font-size: 2rem; 40 | color: var(--red-dark); 41 | cursor: pointer; 42 | } 43 | .nav-links { 44 | padding-top: 2rem; 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | .nav-link { 49 | display: flex; 50 | align-items: center; 51 | color: var(--grey-500); 52 | padding: 1rem 0; 53 | text-transform: capitalize; 54 | transition: var(--transition); 55 | } 56 | .nav-link:hover { 57 | color: var(--grey-900); 58 | } 59 | .nav-link:hover .icon { 60 | color: var(--primary-500); 61 | } 62 | .icon { 63 | font-size: 1.5rem; 64 | margin-right: 1rem; 65 | display: grid; 66 | place-items: center; 67 | transition: var(--transition); 68 | } 69 | .active { 70 | color: var(--grey-900); 71 | } 72 | .active .icon { 73 | color: var(--primary-500); 74 | } 75 | ` 76 | export default Wrapper 77 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/StatItem.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.article` 4 | padding: 2rem; 5 | background: var(--white); 6 | border-radius: var(--borderRadius); 7 | border-bottom: 5px solid ${(props) => props.color}; 8 | header { 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | } 13 | .count { 14 | display: block; 15 | font-weight: 700; 16 | font-size: 50px; 17 | color: ${(props) => props.color}; 18 | } 19 | .title { 20 | margin: 0; 21 | text-transform: capitalize; 22 | letter-spacing: var(--letterSpacing); 23 | text-align: left; 24 | margin-top: 0.5rem; 25 | } 26 | .icon { 27 | width: 70px; 28 | height: 60px; 29 | background: ${(props) => props.bcg}; 30 | border-radius: var(--borderRadius); 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | svg { 35 | font-size: 2rem; 36 | color: ${(props) => props.color}; 37 | } 38 | } 39 | ` 40 | 41 | export default Wrapper 42 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/StatsContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.section` 4 | display: grid; 5 | row-gap: 2rem; 6 | @media (min-width: 768px) { 7 | grid-template-columns: 1fr 1fr; 8 | column-gap: 1rem; 9 | } 10 | @media (min-width: 1120px) { 11 | grid-template-columns: 1fr 1fr 1fr; 12 | column-gap: 1rem; 13 | } 14 | ` 15 | export default Wrapper 16 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/Testing.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.main` 4 | nav { 5 | width: var(--fluid-width); 6 | max-width: var(--max-width); 7 | margin: 0 auto; 8 | height: var(--nav-height); 9 | display: flex; 10 | align-items: center; 11 | } 12 | .page { 13 | min-height: calc(100vh - var(--nav-height)); 14 | display: grid; 15 | align-items: center; 16 | margin-top: -3rem; 17 | } 18 | h1 { 19 | font-weight: 700; 20 | span { 21 | color: var(--primary-500); 22 | } 23 | } 24 | p { 25 | color: var(--grey-600); 26 | } 27 | .main-img { 28 | display: none; 29 | } 30 | @media (min-width: 992px) { 31 | .page { 32 | grid-template-columns: 1fr 1fr; 33 | column-gap: 3rem; 34 | } 35 | .main-img { 36 | display: block; 37 | } 38 | } 39 | ` 40 | export default Wrapper 41 | -------------------------------------------------------------------------------- /client/src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../context/appContext' 2 | 3 | const Alert = () => { 4 | const { alertType, alertText } = useAppContext() 5 | return
{alertText}
6 | } 7 | 8 | export default Alert 9 | -------------------------------------------------------------------------------- /client/src/components/AreaChart.js: -------------------------------------------------------------------------------- 1 | import { 2 | ResponsiveContainer, 3 | AreaChart, 4 | Area, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | } from 'recharts' 10 | 11 | const AreaChartComponent = ({ data }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default AreaChartComponent 26 | -------------------------------------------------------------------------------- /client/src/components/BarChart.js: -------------------------------------------------------------------------------- 1 | import { 2 | BarChart, 3 | Bar, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | ResponsiveContainer, 9 | } from 'recharts' 10 | 11 | const BarChartComponent = ({ data }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default BarChartComponent 26 | -------------------------------------------------------------------------------- /client/src/components/BigSidebar.js: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../context/appContext' 2 | import NavLinks from './NavLinks' 3 | import Logo from '../components/Logo' 4 | import Wrapper from '../assets/wrappers/BigSidebar' 5 | 6 | const BigSidebar = () => { 7 | const { showSidebar } = useAppContext() 8 | return ( 9 | 10 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | ) 24 | } 25 | 26 | export default BigSidebar 27 | -------------------------------------------------------------------------------- /client/src/components/ChartsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import BarChart from './BarChart' 4 | import AreaChart from './AreaChart' 5 | import { useAppContext } from '../context/appContext' 6 | import Wrapper from '../assets/wrappers/ChartsContainer' 7 | 8 | const ChartsContainer = () => { 9 | const [barChart, setBarChart] = useState(true) 10 | const { monthlyApplications: data } = useAppContext() 11 | return ( 12 | 13 |

Monthly Applications

14 | 17 | {barChart ? : } 18 |
19 | ) 20 | } 21 | 22 | export default ChartsContainer 23 | -------------------------------------------------------------------------------- /client/src/components/FormRow.js: -------------------------------------------------------------------------------- 1 | const FormRow = ({ type, name, value, handleChange, labelText }) => { 2 | return ( 3 |
4 | 7 | 14 |
15 | ) 16 | } 17 | 18 | export default FormRow 19 | -------------------------------------------------------------------------------- /client/src/components/FormRowSelect.js: -------------------------------------------------------------------------------- 1 | const FormRowSelect = ({ labelText, name, value, handleChange, list }) => { 2 | return ( 3 |
4 | 7 | 21 |
22 | ) 23 | } 24 | 25 | export default FormRowSelect 26 | -------------------------------------------------------------------------------- /client/src/components/Job.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { FaLocationArrow, FaBriefcase, FaCalendarAlt } from 'react-icons/fa' 3 | import { Link } from 'react-router-dom' 4 | import { useAppContext } from '../context/appContext' 5 | import Wrapper from '../assets/wrappers/Job' 6 | import JobInfo from './JobInfo' 7 | 8 | const Job = ({ 9 | _id, 10 | position, 11 | company, 12 | jobLocation, 13 | jobType, 14 | createdAt, 15 | status, 16 | }) => { 17 | const { setEditJob, deleteJob } = useAppContext() 18 | 19 | let date = moment(createdAt) 20 | date = date.format('MMM Do, YYYY') 21 | return ( 22 | 23 |
24 |
{company.charAt(0)}
25 |
26 |
{position}
27 |

{company}

28 |
29 |
30 |
31 |
32 | } text={jobLocation} /> 33 | } text={date} /> 34 | } text={jobType} /> 35 |
{status}
36 |
37 | 55 |
56 |
57 | ) 58 | } 59 | 60 | export default Job 61 | -------------------------------------------------------------------------------- /client/src/components/JobInfo.js: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/JobInfo' 2 | 3 | const JobInfo = ({ icon, text }) => { 4 | return ( 5 | 6 | {icon} 7 | {text} 8 | 9 | ) 10 | } 11 | 12 | export default JobInfo 13 | -------------------------------------------------------------------------------- /client/src/components/JobsContainer.js: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../context/appContext'; 2 | import { useEffect } from 'react'; 3 | import Loading from './Loading'; 4 | import Job from './Job'; 5 | import Alert from './Alert'; 6 | import Wrapper from '../assets/wrappers/JobsContainer'; 7 | import PageBtnContainer from './PageBtnContainer'; 8 | 9 | const JobsContainer = () => { 10 | const { 11 | getJobs, 12 | jobs, 13 | isLoading, 14 | page, 15 | totalJobs, 16 | search, 17 | searchStatus, 18 | searchType, 19 | sort, 20 | numOfPages, 21 | showAlert, 22 | } = useAppContext(); 23 | useEffect(() => { 24 | getJobs(); 25 | // eslint-disable-next-line 26 | }, [page, search, searchStatus, searchType, sort]); 27 | if (isLoading) { 28 | return ; 29 | } 30 | 31 | if (jobs.length === 0) { 32 | return ( 33 | 34 |

No jobs to display...

35 |
36 | ); 37 | } 38 | 39 | return ( 40 | 41 | {showAlert && } 42 |
43 | {totalJobs} job{jobs.length > 1 && 's'} found 44 |
45 |
46 | {jobs.map((job) => { 47 | return ; 48 | })} 49 |
50 | {numOfPages > 1 && } 51 |
52 | ); 53 | }; 54 | 55 | export default JobsContainer; 56 | -------------------------------------------------------------------------------- /client/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | const Loading = ({ center }) => { 2 | return
3 | } 4 | 5 | export default Loading 6 | -------------------------------------------------------------------------------- /client/src/components/Logo.js: -------------------------------------------------------------------------------- 1 | import logo from '../assets/images/logo.svg' 2 | 3 | const Logo = () => { 4 | return jobify 5 | } 6 | 7 | export default Logo 8 | -------------------------------------------------------------------------------- /client/src/components/NavLinks.js: -------------------------------------------------------------------------------- 1 | import links from '../utils/links'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | const NavLinks = ({ toggleSidebar }) => { 5 | return ( 6 |
7 | {links.map((link) => { 8 | const { text, path, id, icon } = link; 9 | 10 | return ( 11 | 16 | isActive ? 'nav-link active' : 'nav-link' 17 | } 18 | end 19 | > 20 | {icon} 21 | {text} 22 | 23 | ); 24 | })} 25 |
26 | ); 27 | }; 28 | 29 | export default NavLinks; 30 | -------------------------------------------------------------------------------- /client/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/Navbar' 2 | import { FaAlignLeft, FaUserCircle, FaCaretDown } from 'react-icons/fa' 3 | import { useAppContext } from '../context/appContext' 4 | import Logo from './Logo' 5 | import { useState } from 'react' 6 | const Navbar = () => { 7 | const [showLogout, setShowLogout] = useState(false) 8 | const { toggleSidebar, logoutUser, user } = useAppContext() 9 | return ( 10 | 11 |
12 | 15 |
16 | 17 |

dashboard

18 |
19 |
20 | 29 |
30 | 33 |
34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | export default Navbar 41 | -------------------------------------------------------------------------------- /client/src/components/PageBtnContainer.js: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../context/appContext' 2 | import { HiChevronDoubleLeft, HiChevronDoubleRight } from 'react-icons/hi' 3 | import Wrapper from '../assets/wrappers/PageBtnContainer' 4 | 5 | const PageBtnContainer = () => { 6 | const { numOfPages, page, changePage } = useAppContext() 7 | 8 | const pages = Array.from({ length: numOfPages }, (_, index) => { 9 | return index + 1 10 | }) 11 | const nextPage = () => { 12 | let newPage = page + 1 13 | if (newPage > numOfPages) { 14 | newPage = 1 15 | } 16 | changePage(newPage) 17 | } 18 | const prevPage = () => { 19 | let newPage = page - 1 20 | if (newPage < 1) { 21 | newPage = numOfPages 22 | } 23 | changePage(newPage) 24 | } 25 | return ( 26 | 27 | 31 |
32 | {pages.map((pageNumber) => { 33 | return ( 34 | 42 | ) 43 | })} 44 |
45 | 49 |
50 | ) 51 | } 52 | 53 | export default PageBtnContainer 54 | -------------------------------------------------------------------------------- /client/src/components/SearchContainer.js: -------------------------------------------------------------------------------- 1 | import { FormRow, FormRowSelect } from '.'; 2 | import { useAppContext } from '../context/appContext'; 3 | import Wrapper from '../assets/wrappers/SearchContainer'; 4 | import { useState, useMemo } from 'react'; 5 | const SearchContainer = () => { 6 | const [localSearch, setLocalSearch] = useState(''); 7 | const { 8 | isLoading, 9 | search, 10 | searchStatus, 11 | searchType, 12 | sort, 13 | sortOptions, 14 | handleChange, 15 | clearFilters, 16 | jobTypeOptions, 17 | statusOptions, 18 | } = useAppContext(); 19 | const handleSearch = (e) => { 20 | handleChange({ name: e.target.name, value: e.target.value }); 21 | }; 22 | const handleSubmit = (e) => { 23 | e.preventDefault(); 24 | setLocalSearch(''); 25 | clearFilters(); 26 | }; 27 | const debounce = () => { 28 | let timeoutID; 29 | return (e) => { 30 | setLocalSearch(e.target.value); 31 | clearTimeout(timeoutID); 32 | timeoutID = setTimeout(() => { 33 | handleChange({ name: e.target.name, value: e.target.value }); 34 | }, 1000); 35 | }; 36 | }; 37 | const optimizedDebounce = useMemo(() => debounce(), []); 38 | return ( 39 | 40 |
41 |

search form

42 |
43 | {/* search position */} 44 | 45 | 51 | {/* search by status */} 52 | 59 | {/* search by type */} 60 | 67 | {/* sort */} 68 | 74 | 81 |
82 |
83 |
84 | ); 85 | }; 86 | 87 | export default SearchContainer; 88 | -------------------------------------------------------------------------------- /client/src/components/SmallSidebar.js: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/SmallSidebar' 2 | import { FaTimes } from 'react-icons/fa' 3 | import { useAppContext } from '../context/appContext' 4 | 5 | import Logo from './Logo' 6 | import NavLinks from './NavLinks' 7 | 8 | const SmallSidebar = () => { 9 | const { showSidebar, toggleSidebar } = useAppContext() 10 | return ( 11 | 12 |
17 |
18 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default SmallSidebar 32 | -------------------------------------------------------------------------------- /client/src/components/StatItem.js: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/StatItem' 2 | 3 | const StatsItem = ({ count, title, icon, color, bcg }) => { 4 | return ( 5 | 6 |
7 | {count} 8 | {icon} 9 |
10 |
{title}
11 |
12 | ) 13 | } 14 | 15 | export default StatsItem 16 | -------------------------------------------------------------------------------- /client/src/components/StatsContainer.js: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../context/appContext' 2 | import StatItem from './StatItem' 3 | import { FaSuitcaseRolling, FaCalendarCheck, FaBug } from 'react-icons/fa' 4 | import Wrapper from '../assets/wrappers/StatsContainer' 5 | 6 | const StatsContainer = () => { 7 | const { stats } = useAppContext() 8 | 9 | const defaultStats = [ 10 | { 11 | title: 'pending applications', 12 | count: stats.pending || 0, 13 | icon: , 14 | color: '#e9b949', 15 | bcg: '#fcefc7', 16 | }, 17 | { 18 | title: 'interviews scheduled', 19 | count: stats.interview || 0, 20 | icon: , 21 | color: '#647acb', 22 | bcg: '#e0e8f9', 23 | }, 24 | { 25 | title: 'jobs declined', 26 | count: stats.declined || 0, 27 | icon: , 28 | color: '#d66a6a', 29 | bcg: '#ffeeee', 30 | }, 31 | ] 32 | 33 | return ( 34 | 35 | {defaultStats.map((item, index) => { 36 | return 37 | })} 38 | 39 | ) 40 | } 41 | 42 | export default StatsContainer 43 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | import Alert from './Alert' 2 | import BigSidebar from './BigSidebar' 3 | import ChartsContainer from './ChartsContainer' 4 | import FormRow from './FormRow' 5 | import FormRowSelect from './FormRowSelect' 6 | import JobsContainer from './JobsContainer' 7 | import Loading from './Loading' 8 | import Logo from './Logo' 9 | import Navbar from './Navbar' 10 | import SearchContainer from './SearchContainer' 11 | import SmallSidebar from './SmallSidebar' 12 | import StatsContainer from './StatsContainer' 13 | export { 14 | Logo, 15 | FormRow, 16 | Alert, 17 | Navbar, 18 | BigSidebar, 19 | SmallSidebar, 20 | FormRowSelect, 21 | SearchContainer, 22 | JobsContainer, 23 | StatsContainer, 24 | ChartsContainer, 25 | Loading, 26 | } 27 | -------------------------------------------------------------------------------- /client/src/context/actions.js: -------------------------------------------------------------------------------- 1 | export const DISPLAY_ALERT = 'SHOW_ALERT'; 2 | export const CLEAR_ALERT = 'CLEAR_ALERT'; 3 | 4 | export const SETUP_USER_BEGIN = 'SETUP_USER_BEGIN'; 5 | export const SETUP_USER_SUCCESS = 'SETUP_USER_SUCCESS'; 6 | export const SETUP_USER_ERROR = 'SETUP_USER_ERROR'; 7 | 8 | export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR'; 9 | export const LOGOUT_USER = 'LOGOUT_USER'; 10 | 11 | export const UPDATE_USER_BEGIN = 'UPDATE_USER_BEGIN'; 12 | export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS'; 13 | export const UPDATE_USER_ERROR = 'UPDATE_USER_ERROR'; 14 | 15 | export const HANDLE_CHANGE = 'HANDLE_CHANGE'; 16 | export const CLEAR_VALUES = 'CLEAR_VALUES'; 17 | 18 | export const CREATE_JOB_BEGIN = 'CREATE_JOB_BEGIN'; 19 | export const CREATE_JOB_SUCCESS = 'CREATE_JOB_SUCCESS'; 20 | export const CREATE_JOB_ERROR = 'CREATE_JOB_ERROR'; 21 | 22 | export const GET_JOBS_BEGIN = 'GET_JOBS_BEGIN'; 23 | export const GET_JOBS_SUCCESS = 'GET_JOBS_SUCCESS'; 24 | 25 | export const SET_EDIT_JOB = 'SET_EDIT_JOB'; 26 | 27 | export const DELETE_JOB_BEGIN = 'DELETE_JOB_BEGIN'; 28 | export const DELETE_JOB_ERROR = 'DELETE_JOB_ERROR'; 29 | 30 | export const EDIT_JOB_BEGIN = 'EDIT_JOB_BEGIN'; 31 | export const EDIT_JOB_SUCCESS = 'EDIT_JOB_SUCCESS'; 32 | export const EDIT_JOB_ERROR = 'EDIT_JOB_ERROR'; 33 | 34 | export const SHOW_STATS_BEGIN = 'SHOW_STATS_BEGIN'; 35 | export const SHOW_STATS_SUCCESS = 'SHOW_STATS_SUCCESS'; 36 | export const CLEAR_FILTERS = 'CLEAR_FILTERS'; 37 | export const CHANGE_PAGE = 'CHANGE_PAGE'; 38 | 39 | export const GET_CURRENT_USER_BEGIN = 'GET_CURRENT_USER_BEGIN'; 40 | export const GET_CURRENT_USER_SUCCESS = 'GET_CURRENT_USER_SUCCESS'; 41 | -------------------------------------------------------------------------------- /client/src/context/appContext.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useContext, useEffect } from 'react'; 2 | 3 | import reducer from './reducer'; 4 | import axios from 'axios'; 5 | import { 6 | DISPLAY_ALERT, 7 | CLEAR_ALERT, 8 | SETUP_USER_BEGIN, 9 | SETUP_USER_SUCCESS, 10 | SETUP_USER_ERROR, 11 | TOGGLE_SIDEBAR, 12 | LOGOUT_USER, 13 | UPDATE_USER_BEGIN, 14 | UPDATE_USER_SUCCESS, 15 | UPDATE_USER_ERROR, 16 | HANDLE_CHANGE, 17 | CLEAR_VALUES, 18 | CREATE_JOB_BEGIN, 19 | CREATE_JOB_SUCCESS, 20 | CREATE_JOB_ERROR, 21 | GET_JOBS_BEGIN, 22 | GET_JOBS_SUCCESS, 23 | SET_EDIT_JOB, 24 | DELETE_JOB_BEGIN, 25 | DELETE_JOB_ERROR, 26 | EDIT_JOB_BEGIN, 27 | EDIT_JOB_SUCCESS, 28 | EDIT_JOB_ERROR, 29 | SHOW_STATS_BEGIN, 30 | SHOW_STATS_SUCCESS, 31 | CLEAR_FILTERS, 32 | CHANGE_PAGE, 33 | GET_CURRENT_USER_BEGIN, 34 | GET_CURRENT_USER_SUCCESS, 35 | } from './actions'; 36 | 37 | const initialState = { 38 | userLoading: true, 39 | isLoading: false, 40 | showAlert: false, 41 | alertText: '', 42 | alertType: '', 43 | user: null, 44 | userLocation: '', 45 | showSidebar: false, 46 | isEditing: false, 47 | editJobId: '', 48 | position: '', 49 | company: '', 50 | jobLocation: '', 51 | jobTypeOptions: ['full-time', 'part-time', 'remote', 'internship'], 52 | jobType: 'full-time', 53 | statusOptions: ['interview', 'declined', 'pending'], 54 | status: 'pending', 55 | jobs: [], 56 | totalJobs: 0, 57 | numOfPages: 1, 58 | page: 1, 59 | stats: {}, 60 | monthlyApplications: [], 61 | search: '', 62 | searchStatus: 'all', 63 | searchType: 'all', 64 | sort: 'latest', 65 | sortOptions: ['latest', 'oldest', 'a-z', 'z-a'], 66 | }; 67 | 68 | const AppContext = React.createContext(); 69 | 70 | const AppProvider = ({ children }) => { 71 | const [state, dispatch] = useReducer(reducer, initialState); 72 | 73 | // axios 74 | const authFetch = axios.create({ 75 | baseURL: '/api/v1', 76 | }); 77 | // request 78 | 79 | // response 80 | 81 | authFetch.interceptors.response.use( 82 | (response) => { 83 | return response; 84 | }, 85 | (error) => { 86 | // console.log(error.response) 87 | if (error.response.status === 401) { 88 | logoutUser(); 89 | } 90 | return Promise.reject(error); 91 | } 92 | ); 93 | 94 | const displayAlert = () => { 95 | dispatch({ type: DISPLAY_ALERT }); 96 | clearAlert(); 97 | }; 98 | 99 | const clearAlert = () => { 100 | setTimeout(() => { 101 | dispatch({ type: CLEAR_ALERT }); 102 | }, 3000); 103 | }; 104 | 105 | const setupUser = async ({ currentUser, endPoint, alertText }) => { 106 | dispatch({ type: SETUP_USER_BEGIN }); 107 | try { 108 | const { data } = await axios.post( 109 | `/api/v1/auth/${endPoint}`, 110 | currentUser 111 | ); 112 | 113 | const { user, location } = data; 114 | dispatch({ 115 | type: SETUP_USER_SUCCESS, 116 | payload: { user, location, alertText }, 117 | }); 118 | } catch (error) { 119 | dispatch({ 120 | type: SETUP_USER_ERROR, 121 | payload: { msg: error.response.data.msg }, 122 | }); 123 | } 124 | clearAlert(); 125 | }; 126 | const toggleSidebar = () => { 127 | dispatch({ type: TOGGLE_SIDEBAR }); 128 | }; 129 | 130 | const logoutUser = async () => { 131 | await authFetch.get('/auth/logout'); 132 | dispatch({ type: LOGOUT_USER }); 133 | }; 134 | const updateUser = async (currentUser) => { 135 | dispatch({ type: UPDATE_USER_BEGIN }); 136 | try { 137 | const { data } = await authFetch.patch('/auth/updateUser', currentUser); 138 | const { user, location } = data; 139 | 140 | dispatch({ 141 | type: UPDATE_USER_SUCCESS, 142 | payload: { user, location }, 143 | }); 144 | } catch (error) { 145 | if (error.response.status !== 401) { 146 | dispatch({ 147 | type: UPDATE_USER_ERROR, 148 | payload: { msg: error.response.data.msg }, 149 | }); 150 | } 151 | } 152 | clearAlert(); 153 | }; 154 | 155 | const handleChange = ({ name, value }) => { 156 | dispatch({ type: HANDLE_CHANGE, payload: { name, value } }); 157 | }; 158 | const clearValues = () => { 159 | dispatch({ type: CLEAR_VALUES }); 160 | }; 161 | const createJob = async () => { 162 | dispatch({ type: CREATE_JOB_BEGIN }); 163 | try { 164 | const { position, company, jobLocation, jobType, status } = state; 165 | await authFetch.post('/jobs', { 166 | position, 167 | company, 168 | jobLocation, 169 | jobType, 170 | status, 171 | }); 172 | dispatch({ type: CREATE_JOB_SUCCESS }); 173 | dispatch({ type: CLEAR_VALUES }); 174 | } catch (error) { 175 | if (error.response.status === 401) return; 176 | dispatch({ 177 | type: CREATE_JOB_ERROR, 178 | payload: { msg: error.response.data.msg }, 179 | }); 180 | } 181 | clearAlert(); 182 | }; 183 | 184 | const getJobs = async () => { 185 | const { page, search, searchStatus, searchType, sort } = state; 186 | 187 | let url = `/jobs?page=${page}&status=${searchStatus}&jobType=${searchType}&sort=${sort}`; 188 | if (search) { 189 | url = url + `&search=${search}`; 190 | } 191 | dispatch({ type: GET_JOBS_BEGIN }); 192 | try { 193 | const { data } = await authFetch(url); 194 | const { jobs, totalJobs, numOfPages } = data; 195 | dispatch({ 196 | type: GET_JOBS_SUCCESS, 197 | payload: { 198 | jobs, 199 | totalJobs, 200 | numOfPages, 201 | }, 202 | }); 203 | } catch (error) { 204 | logoutUser(); 205 | } 206 | clearAlert(); 207 | }; 208 | 209 | const setEditJob = (id) => { 210 | dispatch({ type: SET_EDIT_JOB, payload: { id } }); 211 | }; 212 | const editJob = async () => { 213 | dispatch({ type: EDIT_JOB_BEGIN }); 214 | 215 | try { 216 | const { position, company, jobLocation, jobType, status } = state; 217 | await authFetch.patch(`/jobs/${state.editJobId}`, { 218 | company, 219 | position, 220 | jobLocation, 221 | jobType, 222 | status, 223 | }); 224 | dispatch({ type: EDIT_JOB_SUCCESS }); 225 | dispatch({ type: CLEAR_VALUES }); 226 | } catch (error) { 227 | if (error.response.status === 401) return; 228 | dispatch({ 229 | type: EDIT_JOB_ERROR, 230 | payload: { msg: error.response.data.msg }, 231 | }); 232 | } 233 | clearAlert(); 234 | }; 235 | const deleteJob = async (jobId) => { 236 | dispatch({ type: DELETE_JOB_BEGIN }); 237 | try { 238 | await authFetch.delete(`/jobs/${jobId}`); 239 | getJobs(); 240 | } catch (error) { 241 | if (error.response.status === 401) return; 242 | dispatch({ 243 | type: DELETE_JOB_ERROR, 244 | payload: { msg: error.response.data.msg }, 245 | }); 246 | } 247 | clearAlert(); 248 | }; 249 | const showStats = async () => { 250 | dispatch({ type: SHOW_STATS_BEGIN }); 251 | try { 252 | const { data } = await authFetch('/jobs/stats'); 253 | dispatch({ 254 | type: SHOW_STATS_SUCCESS, 255 | payload: { 256 | stats: data.defaultStats, 257 | monthlyApplications: data.monthlyApplications, 258 | }, 259 | }); 260 | } catch (error) { 261 | logoutUser(); 262 | } 263 | clearAlert(); 264 | }; 265 | const clearFilters = () => { 266 | dispatch({ type: CLEAR_FILTERS }); 267 | }; 268 | const changePage = (page) => { 269 | dispatch({ type: CHANGE_PAGE, payload: { page } }); 270 | }; 271 | 272 | const getCurrentUser = async () => { 273 | dispatch({ type: GET_CURRENT_USER_BEGIN }); 274 | try { 275 | const { data } = await authFetch('/auth/getCurrentUser'); 276 | const { user, location } = data; 277 | 278 | dispatch({ 279 | type: GET_CURRENT_USER_SUCCESS, 280 | payload: { user, location }, 281 | }); 282 | } catch (error) { 283 | if (error.response.status === 401) return; 284 | logoutUser(); 285 | } 286 | }; 287 | useEffect(() => { 288 | getCurrentUser(); 289 | }, []); 290 | 291 | return ( 292 | 312 | {children} 313 | 314 | ); 315 | }; 316 | 317 | const useAppContext = () => { 318 | return useContext(AppContext); 319 | }; 320 | 321 | export { AppProvider, initialState, useAppContext }; 322 | -------------------------------------------------------------------------------- /client/src/context/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | DISPLAY_ALERT, 3 | CLEAR_ALERT, 4 | SETUP_USER_BEGIN, 5 | SETUP_USER_SUCCESS, 6 | SETUP_USER_ERROR, 7 | TOGGLE_SIDEBAR, 8 | LOGOUT_USER, 9 | UPDATE_USER_BEGIN, 10 | UPDATE_USER_SUCCESS, 11 | UPDATE_USER_ERROR, 12 | HANDLE_CHANGE, 13 | CLEAR_VALUES, 14 | CREATE_JOB_BEGIN, 15 | CREATE_JOB_SUCCESS, 16 | CREATE_JOB_ERROR, 17 | GET_JOBS_BEGIN, 18 | GET_JOBS_SUCCESS, 19 | SET_EDIT_JOB, 20 | DELETE_JOB_BEGIN, 21 | DELETE_JOB_ERROR, 22 | EDIT_JOB_BEGIN, 23 | EDIT_JOB_SUCCESS, 24 | EDIT_JOB_ERROR, 25 | SHOW_STATS_BEGIN, 26 | SHOW_STATS_SUCCESS, 27 | CLEAR_FILTERS, 28 | CHANGE_PAGE, 29 | GET_CURRENT_USER_BEGIN, 30 | GET_CURRENT_USER_SUCCESS, 31 | } from './actions'; 32 | 33 | import { initialState } from './appContext'; 34 | 35 | const reducer = (state, action) => { 36 | if (action.type === DISPLAY_ALERT) { 37 | return { 38 | ...state, 39 | showAlert: true, 40 | alertType: 'danger', 41 | alertText: 'Please provide all values!', 42 | }; 43 | } 44 | if (action.type === CLEAR_ALERT) { 45 | return { 46 | ...state, 47 | showAlert: false, 48 | alertType: '', 49 | alertText: '', 50 | }; 51 | } 52 | 53 | if (action.type === SETUP_USER_BEGIN) { 54 | return { ...state, isLoading: true }; 55 | } 56 | if (action.type === SETUP_USER_SUCCESS) { 57 | return { 58 | ...state, 59 | isLoading: false, 60 | user: action.payload.user, 61 | userLocation: action.payload.location, 62 | jobLocation: action.payload.location, 63 | showAlert: true, 64 | alertType: 'success', 65 | alertText: action.payload.alertText, 66 | }; 67 | } 68 | if (action.type === SETUP_USER_ERROR) { 69 | return { 70 | ...state, 71 | isLoading: false, 72 | showAlert: true, 73 | alertType: 'danger', 74 | alertText: action.payload.msg, 75 | }; 76 | } 77 | if (action.type === TOGGLE_SIDEBAR) { 78 | return { 79 | ...state, 80 | showSidebar: !state.showSidebar, 81 | }; 82 | } 83 | if (action.type === LOGOUT_USER) { 84 | return { 85 | ...initialState, 86 | userLoading: false, 87 | }; 88 | } 89 | if (action.type === UPDATE_USER_BEGIN) { 90 | return { ...state, isLoading: true }; 91 | } 92 | if (action.type === UPDATE_USER_SUCCESS) { 93 | return { 94 | ...state, 95 | isLoading: false, 96 | user: action.payload.user, 97 | userLocation: action.payload.location, 98 | jobLocation: action.payload.location, 99 | showAlert: true, 100 | alertType: 'success', 101 | alertText: 'User Profile Updated!', 102 | }; 103 | } 104 | if (action.type === UPDATE_USER_ERROR) { 105 | return { 106 | ...state, 107 | isLoading: false, 108 | showAlert: true, 109 | alertType: 'danger', 110 | alertText: action.payload.msg, 111 | }; 112 | } 113 | if (action.type === HANDLE_CHANGE) { 114 | return { 115 | ...state, 116 | page: 1, 117 | [action.payload.name]: action.payload.value, 118 | }; 119 | } 120 | if (action.type === CLEAR_VALUES) { 121 | const initialState = { 122 | isEditing: false, 123 | editJobId: '', 124 | position: '', 125 | company: '', 126 | jobLocation: state.userLocation, 127 | jobType: 'full-time', 128 | status: 'pending', 129 | }; 130 | 131 | return { 132 | ...state, 133 | ...initialState, 134 | }; 135 | } 136 | if (action.type === CREATE_JOB_BEGIN) { 137 | return { ...state, isLoading: true }; 138 | } 139 | 140 | if (action.type === CREATE_JOB_SUCCESS) { 141 | return { 142 | ...state, 143 | isLoading: false, 144 | showAlert: true, 145 | alertType: 'success', 146 | alertText: 'New Job Created!', 147 | }; 148 | } 149 | if (action.type === CREATE_JOB_ERROR) { 150 | return { 151 | ...state, 152 | isLoading: false, 153 | showAlert: true, 154 | alertType: 'danger', 155 | alertText: action.payload.msg, 156 | }; 157 | } 158 | if (action.type === GET_JOBS_BEGIN) { 159 | return { ...state, isLoading: true, showAlert: false }; 160 | } 161 | if (action.type === GET_JOBS_SUCCESS) { 162 | return { 163 | ...state, 164 | isLoading: false, 165 | jobs: action.payload.jobs, 166 | totalJobs: action.payload.totalJobs, 167 | numOfPages: action.payload.numOfPages, 168 | }; 169 | } 170 | if (action.type === SET_EDIT_JOB) { 171 | const job = state.jobs.find((job) => job._id === action.payload.id); 172 | const { _id, position, company, jobLocation, jobType, status } = job; 173 | return { 174 | ...state, 175 | isEditing: true, 176 | editJobId: _id, 177 | position, 178 | company, 179 | jobLocation, 180 | jobType, 181 | status, 182 | }; 183 | } 184 | if (action.type === DELETE_JOB_BEGIN) { 185 | return { ...state, isLoading: true }; 186 | } 187 | if (action.type === DELETE_JOB_ERROR) { 188 | return { 189 | ...state, 190 | isLoading: false, 191 | showAlert: true, 192 | alertType: 'danger', 193 | alertText: action.payload.msg, 194 | }; 195 | } 196 | if (action.type === EDIT_JOB_BEGIN) { 197 | return { 198 | ...state, 199 | isLoading: true, 200 | }; 201 | } 202 | if (action.type === EDIT_JOB_SUCCESS) { 203 | return { 204 | ...state, 205 | isLoading: false, 206 | showAlert: true, 207 | alertType: 'success', 208 | alertText: 'Job Updated!', 209 | }; 210 | } 211 | if (action.type === EDIT_JOB_ERROR) { 212 | return { 213 | ...state, 214 | isLoading: false, 215 | showAlert: true, 216 | alertType: 'danger', 217 | alertText: action.payload.msg, 218 | }; 219 | } 220 | if (action.type === SHOW_STATS_BEGIN) { 221 | return { 222 | ...state, 223 | isLoading: true, 224 | showAlert: false, 225 | }; 226 | } 227 | if (action.type === SHOW_STATS_SUCCESS) { 228 | return { 229 | ...state, 230 | isLoading: false, 231 | stats: action.payload.stats, 232 | monthlyApplications: action.payload.monthlyApplications, 233 | }; 234 | } 235 | if (action.type === CLEAR_FILTERS) { 236 | return { 237 | ...state, 238 | search: '', 239 | searchStatus: 'all', 240 | searchType: 'all', 241 | sort: 'latest', 242 | }; 243 | } 244 | if (action.type === CHANGE_PAGE) { 245 | return { ...state, page: action.payload.page }; 246 | } 247 | if (action.type === GET_CURRENT_USER_BEGIN) { 248 | return { ...state, userLoading: true, showAlert: false }; 249 | } 250 | if (action.type === GET_CURRENT_USER_SUCCESS) { 251 | return { 252 | ...state, 253 | userLoading: false, 254 | user: action.payload.user, 255 | userLocation: action.payload.location, 256 | jobLocation: action.payload.location, 257 | }; 258 | } 259 | throw new Error(`no such action : ${action.type}`); 260 | }; 261 | 262 | export default reducer; 263 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::after, 3 | ::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | /* fonts */ 8 | @import url('https://fonts.googleapis.com/css2?family=Cabin&family=Roboto+Condensed:wght@400;700&display=swap'); 9 | 10 | html { 11 | font-size: 100%; 12 | } /*16px*/ 13 | 14 | :root { 15 | /* colors */ 16 | --primary-50: #e0fcff; 17 | --primary-100: #bef8fd; 18 | --primary-200: #87eaf2; 19 | --primary-300: #54d1db; 20 | --primary-400: #38bec9; 21 | --primary-500: #2cb1bc; 22 | --primary-600: #14919b; 23 | --primary-700: #0e7c86; 24 | --primary-800: #0a6c74; 25 | --primary-900: #044e54; 26 | 27 | /* grey */ 28 | --grey-50: #f0f4f8; 29 | --grey-100: #d9e2ec; 30 | --grey-200: #bcccdc; 31 | --grey-300: #9fb3c8; 32 | --grey-400: #829ab1; 33 | --grey-500: #627d98; 34 | --grey-600: #486581; 35 | --grey-700: #334e68; 36 | --grey-800: #243b53; 37 | --grey-900: #102a43; 38 | /* rest of the colors */ 39 | --black: #222; 40 | --white: #fff; 41 | --red-light: #f8d7da; 42 | --red-dark: #842029; 43 | --green-light: #d1e7dd; 44 | --green-dark: #0f5132; 45 | 46 | /* fonts */ 47 | --headingFont: 'Roboto Condensed', Sans-Serif; 48 | --bodyFont: 'Cabin', Sans-Serif; 49 | --small-text: 0.875rem; 50 | --extra-small-text: 0.7em; 51 | /* rest of the vars */ 52 | --backgroundColor: var(--grey-50); 53 | --textColor: var(--grey-900); 54 | --borderRadius: 0.25rem; 55 | --letterSpacing: 1px; 56 | --transition: 0.3s ease-in-out all; 57 | --max-width: 1120px; 58 | --fixed-width: 500px; 59 | --fluid-width: 90vw; 60 | --breakpoint-lg: 992px; 61 | --nav-height: 6rem; 62 | /* box shadow*/ 63 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 64 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 65 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 66 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 67 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 68 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 69 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 70 | } 71 | 72 | body { 73 | background: var(--backgroundColor); 74 | font-family: var(--bodyFont); 75 | font-weight: 400; 76 | line-height: 1.75; 77 | color: var(--textColor); 78 | } 79 | 80 | p { 81 | margin-bottom: 1.5rem; 82 | max-width: 40em; 83 | } 84 | 85 | h1, 86 | h2, 87 | h3, 88 | h4, 89 | h5 { 90 | margin: 0; 91 | margin-bottom: 1.38rem; 92 | font-family: var(--headingFont); 93 | font-weight: 400; 94 | line-height: 1.3; 95 | text-transform: capitalize; 96 | letter-spacing: var(--letterSpacing); 97 | } 98 | 99 | h1 { 100 | margin-top: 0; 101 | font-size: 3.052rem; 102 | } 103 | 104 | h2 { 105 | font-size: 2.441rem; 106 | } 107 | 108 | h3 { 109 | font-size: 1.953rem; 110 | } 111 | 112 | h4 { 113 | font-size: 1.563rem; 114 | } 115 | 116 | h5 { 117 | font-size: 1.25rem; 118 | } 119 | 120 | small, 121 | .text-small { 122 | font-size: var(--small-text); 123 | } 124 | 125 | a { 126 | text-decoration: none; 127 | letter-spacing: var(--letterSpacing); 128 | } 129 | a, 130 | button { 131 | line-height: 1.15; 132 | } 133 | button:disabled { 134 | cursor: not-allowed; 135 | } 136 | ul { 137 | list-style-type: none; 138 | padding: 0; 139 | } 140 | 141 | .img { 142 | width: 100%; 143 | display: block; 144 | object-fit: cover; 145 | } 146 | /* buttons */ 147 | 148 | .btn { 149 | cursor: pointer; 150 | color: var(--white); 151 | background: var(--primary-500); 152 | border: transparent; 153 | border-radius: var(--borderRadius); 154 | letter-spacing: var(--letterSpacing); 155 | padding: 0.375rem 0.75rem; 156 | box-shadow: var(--shadow-2); 157 | transition: var(--transition); 158 | text-transform: capitalize; 159 | display: inline-block; 160 | } 161 | .btn:hover { 162 | background: var(--primary-700); 163 | box-shadow: var(--shadow-3); 164 | } 165 | .btn-hipster { 166 | color: var(--primary-500); 167 | background: var(--primary-200); 168 | } 169 | .btn-hipster:hover { 170 | color: var(--primary-200); 171 | background: var(--primary-700); 172 | } 173 | .btn-block { 174 | width: 100%; 175 | } 176 | .btn-hero { 177 | font-size: 1.25rem; 178 | padding: 0.5rem 1.25rem; 179 | } 180 | .btn-danger { 181 | background: var(--red-light); 182 | color: var(--red-dark); 183 | } 184 | .btn-danger:hover { 185 | background: var(--red-dark); 186 | color: var(--white); 187 | } 188 | /* alerts */ 189 | .alert { 190 | padding: 0.375rem 0.75rem; 191 | margin-bottom: 1rem; 192 | border-color: transparent; 193 | border-radius: var(--borderRadius); 194 | text-align: center; 195 | letter-spacing: var(--letterSpacing); 196 | } 197 | 198 | .alert-danger { 199 | color: var(--red-dark); 200 | background: var(--red-light); 201 | } 202 | .alert-success { 203 | color: var(--green-dark); 204 | background: var(--green-light); 205 | } 206 | /* form */ 207 | 208 | .form { 209 | width: 90vw; 210 | max-width: var(--fixed-width); 211 | background: var(--white); 212 | border-radius: var(--borderRadius); 213 | box-shadow: var(--shadow-2); 214 | padding: 2rem 2.5rem; 215 | margin: 3rem auto; 216 | transition: var(--transition); 217 | } 218 | .form:hover { 219 | box-shadow: var(--shadow-4); 220 | } 221 | .form-label { 222 | display: block; 223 | font-size: var(--smallText); 224 | margin-bottom: 0.5rem; 225 | text-transform: capitalize; 226 | letter-spacing: var(--letterSpacing); 227 | } 228 | .form-input, 229 | .form-textarea, 230 | .form-select { 231 | width: 100%; 232 | padding: 0.375rem 0.75rem; 233 | border-radius: var(--borderRadius); 234 | background: var(--backgroundColor); 235 | border: 1px solid var(--grey-200); 236 | } 237 | .form-input, 238 | .form-select, 239 | .btn-block { 240 | height: 35px; 241 | } 242 | .form-row { 243 | margin-bottom: 1rem; 244 | } 245 | 246 | .form-textarea { 247 | height: 7rem; 248 | } 249 | ::placeholder { 250 | font-family: inherit; 251 | color: var(--grey-400); 252 | } 253 | .form-alert { 254 | color: var(--red-dark); 255 | letter-spacing: var(--letterSpacing); 256 | text-transform: capitalize; 257 | } 258 | /* alert */ 259 | 260 | @keyframes spinner { 261 | to { 262 | transform: rotate(360deg); 263 | } 264 | } 265 | 266 | .loading { 267 | width: 6rem; 268 | height: 6rem; 269 | border: 5px solid var(--grey-400); 270 | border-radius: 50%; 271 | border-top-color: var(--primary-500); 272 | animation: spinner 2s linear infinite; 273 | } 274 | .loading-center { 275 | margin: 0 auto; 276 | } 277 | /* title */ 278 | 279 | .title { 280 | text-align: center; 281 | } 282 | 283 | .title-underline { 284 | background: var(--primary-500); 285 | width: 7rem; 286 | height: 0.25rem; 287 | margin: 0 auto; 288 | margin-top: -1rem; 289 | } 290 | 291 | .container { 292 | width: var(--fluid-width); 293 | max-width: var(--max-width); 294 | margin: 0 auto; 295 | } 296 | .full-page { 297 | min-height: 100vh; 298 | } 299 | 300 | .coffee-info { 301 | text-align: center; 302 | text-transform: capitalize; 303 | margin-bottom: 1rem; 304 | letter-spacing: var(--letterSpacing); 305 | } 306 | .coffee-info span { 307 | display: block; 308 | } 309 | .coffee-info a { 310 | color: var(--primary-500); 311 | } 312 | 313 | @media screen and (min-width: 992px) { 314 | .coffee-info { 315 | text-align: left; 316 | } 317 | .coffee-info span { 318 | display: inline-block; 319 | margin-right: 0.5rem; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import 'normalize.css'; 4 | import './index.css'; 5 | import App from './App'; 6 | import { AppProvider } from './context/appContext'; 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /client/src/pages/Error.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import img from '../assets/images/not-found.svg' 3 | import Wrapper from '../assets/wrappers/ErrorPage' 4 | 5 | const Error = () => { 6 | return ( 7 | 8 |
9 | not found 10 |

Ohh! page not found

11 |

We can't seem to find the page you're looking for

12 | back home 13 |
14 |
15 | ) 16 | } 17 | 18 | export default Error 19 | -------------------------------------------------------------------------------- /client/src/pages/Landing.js: -------------------------------------------------------------------------------- 1 | import main from '../assets/images/main.svg'; 2 | import Wrapper from '../assets/wrappers/LandingPage'; 3 | import { Logo } from '../components'; 4 | import { Link } from 'react-router-dom'; 5 | import { Navigate } from 'react-router-dom'; 6 | import { useAppContext } from '../context/appContext'; 7 | import React from 'react'; 8 | 9 | const Landing = () => { 10 | const { user } = useAppContext(); 11 | return ( 12 | 13 | {user && } 14 | 15 | 18 |
19 | {/* info */} 20 |
21 |

22 | job tracking app 23 |

24 |

25 | I'm baby wayfarers hoodie next level taiyaki brooklyn cliche blue 26 | bottle single-origin coffee chia. Aesthetic post-ironic venmo, 27 | quinoa lo-fi tote bag adaptogen everyday carry meggings +1 brunch 28 | narwhal. 29 |

30 | 31 | Login/Register 32 | 33 |
34 | job hunt 35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Landing; 42 | -------------------------------------------------------------------------------- /client/src/pages/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../context/appContext'; 2 | import { Navigate } from 'react-router-dom'; 3 | import Loading from '../components/Loading'; 4 | const ProtectedRoute = ({ children }) => { 5 | const { user, userLoading } = useAppContext(); 6 | 7 | if (userLoading) return ; 8 | 9 | if (!user) { 10 | return ; 11 | } 12 | return children; 13 | }; 14 | 15 | export default ProtectedRoute; 16 | -------------------------------------------------------------------------------- /client/src/pages/Register.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Logo, FormRow, Alert } from '../components'; 3 | import Wrapper from '../assets/wrappers/RegisterPage'; 4 | import { useAppContext } from '../context/appContext'; 5 | import { useNavigate } from 'react-router-dom'; 6 | const initialState = { 7 | name: '', 8 | email: '', 9 | password: '', 10 | isMember: true, 11 | }; 12 | 13 | const Register = () => { 14 | const navigate = useNavigate(); 15 | const [values, setValues] = useState(initialState); 16 | const { user, isLoading, showAlert, displayAlert, setupUser } = 17 | useAppContext(); 18 | 19 | const toggleMember = () => { 20 | setValues({ ...values, isMember: !values.isMember }); 21 | }; 22 | 23 | const handleChange = (e) => { 24 | setValues({ ...values, [e.target.name]: e.target.value }); 25 | }; 26 | const onSubmit = (e) => { 27 | e.preventDefault(); 28 | const { name, email, password, isMember } = values; 29 | if (!email || !password || (!isMember && !name)) { 30 | displayAlert(); 31 | return; 32 | } 33 | const currentUser = { name, email, password }; 34 | if (isMember) { 35 | setupUser({ 36 | currentUser, 37 | endPoint: 'login', 38 | alertText: 'Login Successful! Redirecting...', 39 | }); 40 | } else { 41 | setupUser({ 42 | currentUser, 43 | endPoint: 'register', 44 | alertText: 'User Created! Redirecting...', 45 | }); 46 | } 47 | }; 48 | 49 | useEffect(() => { 50 | if (user) { 51 | setTimeout(() => { 52 | navigate('/'); 53 | }, 3000); 54 | } 55 | }, [user, navigate]); 56 | 57 | return ( 58 | 59 |
60 | 61 |

{values.isMember ? 'Login' : 'Register'}

62 | {showAlert && } 63 | {/* name input */} 64 | {!values.isMember && ( 65 | 71 | )} 72 | 73 | {/* email input */} 74 | 80 | {/* password input */} 81 | 87 | 90 | 104 |

105 | {values.isMember ? 'Not a member yet?' : 'Already a member?'} 106 | 109 |

110 | 111 |
112 | ); 113 | }; 114 | export default Register; 115 | -------------------------------------------------------------------------------- /client/src/pages/dashboard/AddJob.js: -------------------------------------------------------------------------------- 1 | import { FormRow, FormRowSelect, Alert } from '../../components' 2 | import { useAppContext } from '../../context/appContext' 3 | import Wrapper from '../../assets/wrappers/DashboardFormPage' 4 | 5 | const AddJob = () => { 6 | const { 7 | isLoading, 8 | isEditing, 9 | showAlert, 10 | displayAlert, 11 | position, 12 | company, 13 | jobLocation, 14 | jobType, 15 | jobTypeOptions, 16 | status, 17 | statusOptions, 18 | handleChange, 19 | clearValues, 20 | createJob, 21 | editJob, 22 | } = useAppContext() 23 | 24 | const handleSubmit = (e) => { 25 | e.preventDefault() 26 | 27 | if (!position || !company || !jobLocation) { 28 | displayAlert() 29 | return 30 | } 31 | if (isEditing) { 32 | editJob() 33 | return 34 | } 35 | createJob() 36 | } 37 | const handleJobInput = (e) => { 38 | const name = e.target.name 39 | const value = e.target.value 40 | handleChange({ name, value }) 41 | } 42 | 43 | return ( 44 | 45 |
46 |

{isEditing ? 'edit job' : 'add job'}

47 | {showAlert && } 48 |
49 | {/* position */} 50 | 56 | {/* company */} 57 | 63 | {/* location */} 64 | 71 | {/* job status */} 72 | 78 | {/* job type */} 79 | 86 | {/* btn container */} 87 |
88 | 96 | 105 |
106 |
107 | 108 |
109 | ) 110 | } 111 | 112 | export default AddJob 113 | -------------------------------------------------------------------------------- /client/src/pages/dashboard/AllJobs.js: -------------------------------------------------------------------------------- 1 | import { JobsContainer, SearchContainer } from '../../components' 2 | 3 | const AllJobs = () => { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default AllJobs 13 | -------------------------------------------------------------------------------- /client/src/pages/dashboard/Profile.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { FormRow, Alert } from '../../components' 3 | import { useAppContext } from '../../context/appContext' 4 | import Wrapper from '../../assets/wrappers/DashboardFormPage' 5 | const Profile = () => { 6 | const { user, showAlert, displayAlert, updateUser, isLoading } = 7 | useAppContext() 8 | 9 | const [name, setName] = useState(user?.name) 10 | const [email, setEmail] = useState(user?.email) 11 | const [lastName, setLastName] = useState(user?.lastName) 12 | const [location, setLocation] = useState(user?.location) 13 | 14 | const handleSubmit = (e) => { 15 | e.preventDefault() 16 | if (!name || !email || !lastName || !location) { 17 | displayAlert() 18 | return 19 | } 20 | updateUser({ name, email, lastName, location }) 21 | } 22 | 23 | return ( 24 | 25 |
26 |

profile

27 | {showAlert && } 28 |
29 | setName(e.target.value)} 34 | /> 35 | setLastName(e.target.value)} 41 | /> 42 | setEmail(e.target.value)} 47 | /> 48 | setLocation(e.target.value)} 53 | /> 54 | 57 |
58 | 59 |
60 | ) 61 | } 62 | 63 | export default Profile 64 | -------------------------------------------------------------------------------- /client/src/pages/dashboard/SharedLayout.js: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom' 2 | import Wrapper from '../../assets/wrappers/SharedLayout' 3 | import { Navbar, BigSidebar, SmallSidebar } from '../../components' 4 | const SharedLayout = () => { 5 | return ( 6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 | ) 19 | } 20 | 21 | export default SharedLayout 22 | -------------------------------------------------------------------------------- /client/src/pages/dashboard/Stats.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useAppContext } from '../../context/appContext' 3 | import { StatsContainer, Loading, ChartsContainer } from '../../components' 4 | 5 | const Stats = () => { 6 | const { showStats, isLoading, monthlyApplications } = useAppContext() 7 | 8 | useEffect(() => { 9 | showStats() 10 | // eslint-disable-next-line 11 | }, []) 12 | if (isLoading) { 13 | return 14 | } 15 | return ( 16 | <> 17 | 18 | {monthlyApplications.length > 0 && } 19 | 20 | ) 21 | } 22 | 23 | export default Stats 24 | -------------------------------------------------------------------------------- /client/src/pages/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import AddJob from './AddJob' 2 | import AllJobs from './AllJobs' 3 | import Profile from './Profile' 4 | import SharedLayout from './SharedLayout' 5 | import Stats from './Stats' 6 | export { AllJobs, Profile, SharedLayout, Stats, AddJob } 7 | -------------------------------------------------------------------------------- /client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Landing from './Landing' 2 | import Error from './Error' 3 | import Register from './Register' 4 | import ProtectedRoute from './ProtectedRoute' 5 | export { Landing, Error, Register, ProtectedRoute } 6 | -------------------------------------------------------------------------------- /client/src/utils/links.js: -------------------------------------------------------------------------------- 1 | import { IoBarChartSharp } from 'react-icons/io5' 2 | import { MdQueryStats } from 'react-icons/md' 3 | import { FaWpforms } from 'react-icons/fa' 4 | import { ImProfile } from 'react-icons/im' 5 | 6 | const links = [ 7 | { id: 1, text: 'stats', path: '/', icon: }, 8 | { id: 2, text: 'all jobs', path: 'all-jobs', icon: }, 9 | { id: 3, text: 'add job', path: 'add-job', icon: }, 10 | { id: 4, text: 'profile', path: 'profile', icon: }, 11 | ] 12 | 13 | export default links 14 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User.js'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import { BadRequestError, UnAuthenticatedError } from '../errors/index.js'; 4 | import attachCookie from '../utils/attachCookie.js'; 5 | const register = async (req, res) => { 6 | const { name, email, password } = req.body; 7 | 8 | if (!name || !email || !password) { 9 | throw new BadRequestError('please provide all values'); 10 | } 11 | const userAlreadyExists = await User.findOne({ email }); 12 | if (userAlreadyExists) { 13 | throw new BadRequestError('Email already in use'); 14 | } 15 | const user = await User.create({ name, email, password }); 16 | 17 | const token = user.createJWT(); 18 | attachCookie({ res, token }); 19 | res.status(StatusCodes.CREATED).json({ 20 | user: { 21 | email: user.email, 22 | lastName: user.lastName, 23 | location: user.location, 24 | name: user.name, 25 | }, 26 | 27 | location: user.location, 28 | }); 29 | }; 30 | const login = async (req, res) => { 31 | const { email, password } = req.body; 32 | if (!email || !password) { 33 | throw new BadRequestError('Please provide all values'); 34 | } 35 | const user = await User.findOne({ email }).select('+password'); 36 | if (!user) { 37 | throw new UnAuthenticatedError('Invalid Credentials'); 38 | } 39 | 40 | const isPasswordCorrect = await user.comparePassword(password); 41 | if (!isPasswordCorrect) { 42 | throw new UnAuthenticatedError('Invalid Credentials'); 43 | } 44 | const token = user.createJWT(); 45 | attachCookie({ res, token }); 46 | user.password = undefined; 47 | 48 | res.status(StatusCodes.OK).json({ user, location: user.location }); 49 | }; 50 | const updateUser = async (req, res) => { 51 | const { email, name, lastName, location } = req.body; 52 | if (!email || !name || !lastName || !location) { 53 | throw new BadRequestError('Please provide all values'); 54 | } 55 | const user = await User.findOne({ _id: req.user.userId }); 56 | 57 | user.email = email; 58 | user.name = name; 59 | user.lastName = lastName; 60 | user.location = location; 61 | 62 | await user.save(); 63 | 64 | const token = user.createJWT(); 65 | attachCookie({ res, token }); 66 | 67 | res.status(StatusCodes.OK).json({ user, location: user.location }); 68 | }; 69 | 70 | const getCurrentUser = async (req, res) => { 71 | const user = await User.findOne({ _id: req.user.userId }); 72 | res.status(StatusCodes.OK).json({ user, location: user.location }); 73 | }; 74 | 75 | const logout = async (req, res) => { 76 | res.cookie('token', 'logout', { 77 | httpOnly: true, 78 | expires: new Date(Date.now() + 1000), 79 | }); 80 | res.status(StatusCodes.OK).json({ msg: 'user logged out!' }); 81 | }; 82 | 83 | export { register, login, updateUser, getCurrentUser, logout }; 84 | -------------------------------------------------------------------------------- /controllers/jobsController.js: -------------------------------------------------------------------------------- 1 | import Job from '../models/Job.js'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import { 4 | BadRequestError, 5 | NotFoundError, 6 | UnAuthenticatedError, 7 | } from '../errors/index.js'; 8 | import checkPermissions from '../utils/checkPermissions.js'; 9 | import mongoose from 'mongoose'; 10 | import moment from 'moment'; 11 | const createJob = async (req, res) => { 12 | const { position, company } = req.body; 13 | 14 | if (!position || !company) { 15 | throw new BadRequestError('Please provide all values'); 16 | } 17 | req.body.createdBy = req.user.userId; 18 | const job = await Job.create(req.body); 19 | res.status(StatusCodes.CREATED).json({ job }); 20 | }; 21 | const getAllJobs = async (req, res) => { 22 | const { status, jobType, sort, search } = req.query; 23 | 24 | const queryObject = { 25 | createdBy: req.user.userId, 26 | }; 27 | // add stuff based on condition 28 | 29 | if (status && status !== 'all') { 30 | queryObject.status = status; 31 | } 32 | if (jobType && jobType !== 'all') { 33 | queryObject.jobType = jobType; 34 | } 35 | if (search) { 36 | queryObject.position = { $regex: search, $options: 'i' }; 37 | } 38 | // NO AWAIT 39 | 40 | let result = Job.find(queryObject); 41 | 42 | // chain sort conditions 43 | 44 | if (sort === 'latest') { 45 | result = result.sort('-createdAt'); 46 | } 47 | if (sort === 'oldest') { 48 | result = result.sort('createdAt'); 49 | } 50 | if (sort === 'a-z') { 51 | result = result.sort('position'); 52 | } 53 | if (sort === 'z-a') { 54 | result = result.sort('-position'); 55 | } 56 | 57 | // 58 | 59 | // setup pagination 60 | const page = Number(req.query.page) || 1; 61 | const limit = Number(req.query.limit) || 10; 62 | const skip = (page - 1) * limit; 63 | 64 | result = result.skip(skip).limit(limit); 65 | 66 | const jobs = await result; 67 | 68 | const totalJobs = await Job.countDocuments(queryObject); 69 | const numOfPages = Math.ceil(totalJobs / limit); 70 | 71 | res.status(StatusCodes.OK).json({ jobs, totalJobs, numOfPages }); 72 | }; 73 | const updateJob = async (req, res) => { 74 | const { id: jobId } = req.params; 75 | const { company, position } = req.body; 76 | 77 | if (!position || !company) { 78 | throw new BadRequestError('Please provide all values'); 79 | } 80 | const job = await Job.findOne({ _id: jobId }); 81 | 82 | if (!job) { 83 | throw new NotFoundError(`No job with id :${jobId}`); 84 | } 85 | // check permissions 86 | 87 | checkPermissions(req.user, job.createdBy); 88 | 89 | const updatedJob = await Job.findOneAndUpdate({ _id: jobId }, req.body, { 90 | new: true, 91 | runValidators: true, 92 | }); 93 | 94 | res.status(StatusCodes.OK).json({ updatedJob }); 95 | }; 96 | const deleteJob = async (req, res) => { 97 | const { id: jobId } = req.params; 98 | 99 | const job = await Job.findOne({ _id: jobId }); 100 | 101 | if (!job) { 102 | throw new NotFoundError(`No job with id :${jobId}`); 103 | } 104 | 105 | checkPermissions(req.user, job.createdBy); 106 | 107 | await job.remove(); 108 | 109 | res.status(StatusCodes.OK).json({ msg: 'Success! Job removed' }); 110 | }; 111 | const showStats = async (req, res) => { 112 | let stats = await Job.aggregate([ 113 | { $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } }, 114 | { $group: { _id: '$status', count: { $sum: 1 } } }, 115 | ]); 116 | stats = stats.reduce((acc, curr) => { 117 | const { _id: title, count } = curr; 118 | acc[title] = count; 119 | return acc; 120 | }, {}); 121 | 122 | const defaultStats = { 123 | pending: stats.pending || 0, 124 | interview: stats.interview || 0, 125 | declined: stats.declined || 0, 126 | }; 127 | 128 | let monthlyApplications = await Job.aggregate([ 129 | { $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } }, 130 | { 131 | $group: { 132 | _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } }, 133 | count: { $sum: 1 }, 134 | }, 135 | }, 136 | { $sort: { '_id.year': -1, '_id.month': -1 } }, 137 | { $limit: 6 }, 138 | ]); 139 | monthlyApplications = monthlyApplications 140 | .map((item) => { 141 | const { 142 | _id: { year, month }, 143 | count, 144 | } = item; 145 | const date = moment() 146 | .month(month - 1) 147 | .year(year) 148 | .format('MMM Y'); 149 | return { date, count }; 150 | }) 151 | .reverse(); 152 | 153 | res.status(StatusCodes.OK).json({ defaultStats, monthlyApplications }); 154 | }; 155 | 156 | export { createJob, deleteJob, getAllJobs, updateJob, showStats }; 157 | -------------------------------------------------------------------------------- /db/connect.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const connectDB = (url) => { 4 | return mongoose.connect(url) 5 | } 6 | export default connectDB 7 | -------------------------------------------------------------------------------- /errors/bad-request.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes' 2 | import CustomAPIError from './custom-api.js' 3 | 4 | class BadRequestError extends CustomAPIError { 5 | constructor(message) { 6 | super(message) 7 | this.statusCode = StatusCodes.BAD_REQUEST 8 | } 9 | } 10 | 11 | export default BadRequestError 12 | -------------------------------------------------------------------------------- /errors/custom-api.js: -------------------------------------------------------------------------------- 1 | class CustomAPIError extends Error { 2 | constructor(message) { 3 | super(message) 4 | } 5 | } 6 | 7 | export default CustomAPIError 8 | -------------------------------------------------------------------------------- /errors/index.js: -------------------------------------------------------------------------------- 1 | import BadRequestError from './bad-request.js' 2 | import NotFoundError from './not-found.js' 3 | import UnAuthenticatedError from './unauthenticated.js' 4 | export { BadRequestError, NotFoundError, UnAuthenticatedError } 5 | -------------------------------------------------------------------------------- /errors/not-found.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes' 2 | import CustomAPIError from './custom-api.js' 3 | 4 | class NotFoundError extends CustomAPIError { 5 | constructor(message) { 6 | super(message) 7 | this.statusCode = StatusCodes.NOT_FOUND 8 | } 9 | } 10 | 11 | export default NotFoundError 12 | -------------------------------------------------------------------------------- /errors/unauthenticated.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes' 2 | import CustomAPIError from './custom-api.js' 3 | 4 | class UnAuthenticatedError extends CustomAPIError { 5 | constructor(message) { 6 | super(message) 7 | this.statusCode = StatusCodes.UNAUTHORIZED 8 | } 9 | } 10 | 11 | export default UnAuthenticatedError 12 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { UnAuthenticatedError } from '../errors/index.js'; 3 | 4 | const auth = async (req, res, next) => { 5 | const token = req.cookies.token; 6 | if (!token) { 7 | throw new UnAuthenticatedError('Authentication Invalid'); 8 | } 9 | try { 10 | const payload = jwt.verify(token, process.env.JWT_SECRET); 11 | const testUser = payload.userId === '63628d5d178e918562ef9ce8'; 12 | req.user = { userId: payload.userId, testUser }; 13 | next(); 14 | } catch (error) { 15 | throw new UnAuthenticatedError('Authentication Invalid'); 16 | } 17 | }; 18 | 19 | export default auth; 20 | -------------------------------------------------------------------------------- /middleware/error-handler.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes' 2 | 3 | const errorHandlerMiddleware = (err, req, res, next) => { 4 | const defaultError = { 5 | statusCode: err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR, 6 | msg: err.message || 'Something went wrong, try again later', 7 | } 8 | if (err.name === 'ValidationError') { 9 | defaultError.statusCode = StatusCodes.BAD_REQUEST 10 | // defaultError.msg = err.message 11 | defaultError.msg = Object.values(err.errors) 12 | .map((item) => item.message) 13 | .join(',') 14 | } 15 | if (err.code && err.code === 11000) { 16 | defaultError.statusCode = StatusCodes.BAD_REQUEST 17 | defaultError.msg = `${Object.keys(err.keyValue)} field has to be unique` 18 | } 19 | 20 | res.status(defaultError.statusCode).json({ msg: defaultError.msg }) 21 | } 22 | 23 | export default errorHandlerMiddleware 24 | -------------------------------------------------------------------------------- /middleware/not-found.js: -------------------------------------------------------------------------------- 1 | const notFoundMiddleware = (req, res) => 2 | res.status(404).send('Route does not exist') 3 | 4 | export default notFoundMiddleware 5 | -------------------------------------------------------------------------------- /middleware/testUser.js: -------------------------------------------------------------------------------- 1 | import { BadRequestError } from '../errors/index.js'; 2 | 3 | const testUser = (req, res, next) => { 4 | if (req.user.testUser) { 5 | throw new BadRequestError('Test User. Read Only!'); 6 | } 7 | next(); 8 | }; 9 | 10 | export default testUser; 11 | -------------------------------------------------------------------------------- /mock-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "company": "Kihn-Roberts", 4 | "position": "Software Test Engineer III", 5 | "jobLocation": "Karangtengah", 6 | "jobType": "full-time", 7 | "status": "pending", 8 | "createdBy": "63628d5d178e918562ef9ce8", 9 | "createdAt": "2020-01-06T12:17:11Z" 10 | }, 11 | { 12 | "company": "Strosin and Sons", 13 | "position": "Staff Accountant II", 14 | "jobLocation": "Gnosjö", 15 | "jobType": "remote", 16 | "status": "interview", 17 | "createdBy": "63628d5d178e918562ef9ce8", 18 | "createdAt": "2020-08-11T04:52:58Z" 19 | }, 20 | { 21 | "company": "Marvin-Wisoky", 22 | "position": "Nuclear Power Engineer", 23 | "jobLocation": "Sulkava", 24 | "jobType": "part-time", 25 | "status": "interview", 26 | "createdBy": "63628d5d178e918562ef9ce8", 27 | "createdAt": "2021-09-22T20:17:03Z" 28 | }, 29 | { 30 | "company": "Weber, Glover and Quitzon", 31 | "position": "Electrical Engineer", 32 | "jobLocation": "København", 33 | "jobType": "internship", 34 | "status": "declined", 35 | "createdBy": "63628d5d178e918562ef9ce8", 36 | "createdAt": "2020-11-29T01:32:17Z" 37 | }, 38 | { 39 | "company": "Kirlin LLC", 40 | "position": "Developer IV", 41 | "jobLocation": "Zheyuan", 42 | "jobType": "internship", 43 | "status": "declined", 44 | "createdBy": "63628d5d178e918562ef9ce8", 45 | "createdAt": "2020-04-19T09:50:29Z" 46 | }, 47 | { 48 | "company": "Denesik Group", 49 | "position": "Administrative Officer", 50 | "jobLocation": "Pergamos", 51 | "jobType": "part-time", 52 | "status": "interview", 53 | "createdBy": "63628d5d178e918562ef9ce8", 54 | "createdAt": "2020-01-10T09:00:17Z" 55 | }, 56 | { 57 | "company": "Batz, Bruen and Miller", 58 | "position": "Human Resources Manager", 59 | "jobLocation": "Pandangan Kulon", 60 | "jobType": "remote", 61 | "status": "interview", 62 | "createdBy": "63628d5d178e918562ef9ce8", 63 | "createdAt": "2020-01-15T01:12:55Z" 64 | }, 65 | { 66 | "company": "Turcotte and Sons", 67 | "position": "Research Assistant IV", 68 | "jobLocation": "Būrabay", 69 | "jobType": "full-time", 70 | "status": "pending", 71 | "createdBy": "63628d5d178e918562ef9ce8", 72 | "createdAt": "2020-06-20T05:16:03Z" 73 | }, 74 | { 75 | "company": "Wiza, Sawayn and Hayes", 76 | "position": "Legal Assistant", 77 | "jobLocation": "Rio Pardo", 78 | "jobType": "part-time", 79 | "status": "declined", 80 | "createdBy": "63628d5d178e918562ef9ce8", 81 | "createdAt": "2021-03-05T09:15:30Z" 82 | }, 83 | { 84 | "company": "Hills, Ward and Gleichner", 85 | "position": "Senior Editor", 86 | "jobLocation": "Giồng Riềng", 87 | "jobType": "part-time", 88 | "status": "pending", 89 | "createdBy": "63628d5d178e918562ef9ce8", 90 | "createdAt": "2021-08-23T02:08:39Z" 91 | }, 92 | { 93 | "company": "Bins-Ullrich", 94 | "position": "Dental Hygienist", 95 | "jobLocation": "Methven", 96 | "jobType": "internship", 97 | "status": "interview", 98 | "createdBy": "63628d5d178e918562ef9ce8", 99 | "createdAt": "2020-10-23T03:37:19Z" 100 | }, 101 | { 102 | "company": "Hahn, Sanford and Goodwin", 103 | "position": "Product Engineer", 104 | "jobLocation": "Liushutun", 105 | "jobType": "internship", 106 | "status": "interview", 107 | "createdBy": "63628d5d178e918562ef9ce8", 108 | "createdAt": "2020-04-14T23:21:45Z" 109 | }, 110 | { 111 | "company": "Koch Group", 112 | "position": "Web Designer III", 113 | "jobLocation": "San Miguel", 114 | "jobType": "internship", 115 | "status": "pending", 116 | "createdBy": "63628d5d178e918562ef9ce8", 117 | "createdAt": "2020-11-30T16:02:13Z" 118 | }, 119 | { 120 | "company": "Cremin, Durgan and Schmitt", 121 | "position": "Project Manager", 122 | "jobLocation": "Weton", 123 | "jobType": "internship", 124 | "status": "pending", 125 | "createdBy": "63628d5d178e918562ef9ce8", 126 | "createdAt": "2020-01-31T02:39:30Z" 127 | }, 128 | { 129 | "company": "Jaskolski-Senger", 130 | "position": "VP Sales", 131 | "jobLocation": "Svyetlahorsk", 132 | "jobType": "remote", 133 | "status": "pending", 134 | "createdBy": "63628d5d178e918562ef9ce8", 135 | "createdAt": "2020-01-07T18:04:05Z" 136 | }, 137 | { 138 | "company": "Borer, Dibbert and Larson", 139 | "position": "Community Outreach Specialist", 140 | "jobLocation": "Svislach", 141 | "jobType": "remote", 142 | "status": "interview", 143 | "createdBy": "63628d5d178e918562ef9ce8", 144 | "createdAt": "2020-09-24T05:03:18Z" 145 | }, 146 | { 147 | "company": "Littel, Cummerata and Wisoky", 148 | "position": "Accounting Assistant III", 149 | "jobLocation": "Simitli", 150 | "jobType": "remote", 151 | "status": "pending", 152 | "createdBy": "63628d5d178e918562ef9ce8", 153 | "createdAt": "2021-08-12T03:47:47Z" 154 | }, 155 | { 156 | "company": "Schimmel, Beahan and Baumbach", 157 | "position": "Pharmacist", 158 | "jobLocation": "Jakhaly", 159 | "jobType": "internship", 160 | "status": "interview", 161 | "createdBy": "63628d5d178e918562ef9ce8", 162 | "createdAt": "2021-10-21T07:03:27Z" 163 | }, 164 | { 165 | "company": "Predovic and Sons", 166 | "position": "Senior Financial Analyst", 167 | "jobLocation": "Estoi", 168 | "jobType": "part-time", 169 | "status": "pending", 170 | "createdBy": "63628d5d178e918562ef9ce8", 171 | "createdAt": "2020-12-26T13:14:04Z" 172 | }, 173 | { 174 | "company": "Little LLC", 175 | "position": "Dental Hygienist", 176 | "jobLocation": "Rebrikha", 177 | "jobType": "remote", 178 | "status": "declined", 179 | "createdBy": "63628d5d178e918562ef9ce8", 180 | "createdAt": "2021-11-23T06:27:19Z" 181 | }, 182 | { 183 | "company": "Hahn-Koss", 184 | "position": "Clinical Specialist", 185 | "jobLocation": "Tiancheng", 186 | "jobType": "part-time", 187 | "status": "pending", 188 | "createdBy": "63628d5d178e918562ef9ce8", 189 | "createdAt": "2021-06-25T06:40:23Z" 190 | }, 191 | { 192 | "company": "Satterfield Group", 193 | "position": "Nuclear Power Engineer", 194 | "jobLocation": "Si Bun Rueang", 195 | "jobType": "remote", 196 | "status": "pending", 197 | "createdBy": "63628d5d178e918562ef9ce8", 198 | "createdAt": "2020-09-07T19:38:06Z" 199 | }, 200 | { 201 | "company": "Runte-O'Reilly", 202 | "position": "Tax Accountant", 203 | "jobLocation": "Chok Chai", 204 | "jobType": "part-time", 205 | "status": "interview", 206 | "createdBy": "63628d5d178e918562ef9ce8", 207 | "createdAt": "2020-10-14T23:25:12Z" 208 | }, 209 | { 210 | "company": "Jaskolski LLC", 211 | "position": "Physical Therapy Assistant", 212 | "jobLocation": "Paoua", 213 | "jobType": "part-time", 214 | "status": "interview", 215 | "createdBy": "63628d5d178e918562ef9ce8", 216 | "createdAt": "2020-10-31T19:20:18Z" 217 | }, 218 | { 219 | "company": "Fritsch-Muller", 220 | "position": "Desktop Support Technician", 221 | "jobLocation": "Kosai-shi", 222 | "jobType": "remote", 223 | "status": "declined", 224 | "createdBy": "63628d5d178e918562ef9ce8", 225 | "createdAt": "2021-09-24T11:01:03Z" 226 | }, 227 | { 228 | "company": "Schumm-Cormier", 229 | "position": "Programmer Analyst I", 230 | "jobLocation": "New Agutaya", 231 | "jobType": "remote", 232 | "status": "pending", 233 | "createdBy": "63628d5d178e918562ef9ce8", 234 | "createdAt": "2020-08-19T18:23:46Z" 235 | }, 236 | { 237 | "company": "Toy Group", 238 | "position": "Software Consultant", 239 | "jobLocation": "Belfast", 240 | "jobType": "full-time", 241 | "status": "interview", 242 | "createdBy": "63628d5d178e918562ef9ce8", 243 | "createdAt": "2020-08-24T08:56:52Z" 244 | }, 245 | { 246 | "company": "Kreiger, Thiel and McDermott", 247 | "position": "Geologist IV", 248 | "jobLocation": "Cerenti", 249 | "jobType": "remote", 250 | "status": "declined", 251 | "createdBy": "63628d5d178e918562ef9ce8", 252 | "createdAt": "2021-03-09T08:41:18Z" 253 | }, 254 | { 255 | "company": "Pacocha and Sons", 256 | "position": "Tax Accountant", 257 | "jobLocation": "An Lão", 258 | "jobType": "part-time", 259 | "status": "interview", 260 | "createdBy": "63628d5d178e918562ef9ce8", 261 | "createdAt": "2020-11-29T04:38:32Z" 262 | }, 263 | { 264 | "company": "Veum-Kulas", 265 | "position": "Marketing Assistant", 266 | "jobLocation": "Guohe", 267 | "jobType": "internship", 268 | "status": "pending", 269 | "createdBy": "63628d5d178e918562ef9ce8", 270 | "createdAt": "2020-11-28T02:07:37Z" 271 | }, 272 | { 273 | "company": "Wilkinson-Kub", 274 | "position": "Automation Specialist II", 275 | "jobLocation": "Kuznetsovs’k", 276 | "jobType": "remote", 277 | "status": "declined", 278 | "createdBy": "63628d5d178e918562ef9ce8", 279 | "createdAt": "2021-06-20T11:21:38Z" 280 | }, 281 | { 282 | "company": "Kunde and Sons", 283 | "position": "Sales Associate", 284 | "jobLocation": "La Mesa", 285 | "jobType": "internship", 286 | "status": "declined", 287 | "createdBy": "63628d5d178e918562ef9ce8", 288 | "createdAt": "2020-09-24T17:22:21Z" 289 | }, 290 | { 291 | "company": "Mraz, Weimann and Parisian", 292 | "position": "Information Systems Manager", 293 | "jobLocation": "Tshela", 294 | "jobType": "full-time", 295 | "status": "declined", 296 | "createdBy": "63628d5d178e918562ef9ce8", 297 | "createdAt": "2020-02-24T22:56:42Z" 298 | }, 299 | { 300 | "company": "Raynor, Harber and Boyer", 301 | "position": "Programmer I", 302 | "jobLocation": "Cartagena", 303 | "jobType": "remote", 304 | "status": "declined", 305 | "createdBy": "63628d5d178e918562ef9ce8", 306 | "createdAt": "2020-06-20T03:03:36Z" 307 | }, 308 | { 309 | "company": "Senger-Greenfelder", 310 | "position": "Payment Adjustment Coordinator", 311 | "jobLocation": "Verkhozim", 312 | "jobType": "internship", 313 | "status": "declined", 314 | "createdBy": "63628d5d178e918562ef9ce8", 315 | "createdAt": "2020-07-01T23:49:49Z" 316 | }, 317 | { 318 | "company": "Kunze and Sons", 319 | "position": "Accounting Assistant III", 320 | "jobLocation": "Kafr Mandā", 321 | "jobType": "remote", 322 | "status": "interview", 323 | "createdBy": "63628d5d178e918562ef9ce8", 324 | "createdAt": "2021-12-22T11:20:44Z" 325 | }, 326 | { 327 | "company": "Bode Group", 328 | "position": "Quality Control Specialist", 329 | "jobLocation": "Kosaya Gora", 330 | "jobType": "full-time", 331 | "status": "declined", 332 | "createdBy": "63628d5d178e918562ef9ce8", 333 | "createdAt": "2020-12-26T20:23:21Z" 334 | }, 335 | { 336 | "company": "Bednar-Osinski", 337 | "position": "Chief Design Engineer", 338 | "jobLocation": "Langley", 339 | "jobType": "part-time", 340 | "status": "pending", 341 | "createdBy": "63628d5d178e918562ef9ce8", 342 | "createdAt": "2021-05-20T05:34:50Z" 343 | }, 344 | { 345 | "company": "Waters-Koch", 346 | "position": "Help Desk Operator", 347 | "jobLocation": "Orsha", 348 | "jobType": "full-time", 349 | "status": "pending", 350 | "createdBy": "63628d5d178e918562ef9ce8", 351 | "createdAt": "2020-12-21T03:23:00Z" 352 | }, 353 | { 354 | "company": "Stehr, Homenick and Turcotte", 355 | "position": "Assistant Professor", 356 | "jobLocation": "Shizi", 357 | "jobType": "full-time", 358 | "status": "interview", 359 | "createdBy": "63628d5d178e918562ef9ce8", 360 | "createdAt": "2021-10-05T17:37:24Z" 361 | }, 362 | { 363 | "company": "Kuhlman, Balistreri and Romaguera", 364 | "position": "Automation Specialist I", 365 | "jobLocation": "Utmānzai", 366 | "jobType": "remote", 367 | "status": "declined", 368 | "createdBy": "63628d5d178e918562ef9ce8", 369 | "createdAt": "2020-08-05T06:53:47Z" 370 | }, 371 | { 372 | "company": "Rutherford-Considine", 373 | "position": "Chemical Engineer", 374 | "jobLocation": "Sukosari", 375 | "jobType": "remote", 376 | "status": "declined", 377 | "createdBy": "63628d5d178e918562ef9ce8", 378 | "createdAt": "2021-08-09T21:24:39Z" 379 | }, 380 | { 381 | "company": "Klein, Wunsch and Gutmann", 382 | "position": "Clinical Specialist", 383 | "jobLocation": "Neiguan", 384 | "jobType": "full-time", 385 | "status": "interview", 386 | "createdBy": "63628d5d178e918562ef9ce8", 387 | "createdAt": "2021-06-18T08:15:10Z" 388 | }, 389 | { 390 | "company": "Windler-Turner", 391 | "position": "Research Nurse", 392 | "jobLocation": "Tilburg", 393 | "jobType": "part-time", 394 | "status": "interview", 395 | "createdBy": "63628d5d178e918562ef9ce8", 396 | "createdAt": "2020-10-15T20:09:06Z" 397 | }, 398 | { 399 | "company": "Crooks-Dare", 400 | "position": "Librarian", 401 | "jobLocation": "Lévis", 402 | "jobType": "internship", 403 | "status": "interview", 404 | "createdBy": "63628d5d178e918562ef9ce8", 405 | "createdAt": "2021-05-29T04:55:22Z" 406 | }, 407 | { 408 | "company": "Labadie LLC", 409 | "position": "Account Representative II", 410 | "jobLocation": "Paripiranga", 411 | "jobType": "remote", 412 | "status": "pending", 413 | "createdBy": "63628d5d178e918562ef9ce8", 414 | "createdAt": "2020-08-14T09:32:28Z" 415 | }, 416 | { 417 | "company": "Heathcote, Herzog and Konopelski", 418 | "position": "Biostatistician IV", 419 | "jobLocation": "Dingzhai", 420 | "jobType": "full-time", 421 | "status": "interview", 422 | "createdBy": "63628d5d178e918562ef9ce8", 423 | "createdAt": "2020-09-15T06:43:57Z" 424 | }, 425 | { 426 | "company": "Price Inc", 427 | "position": "Cost Accountant", 428 | "jobLocation": "Severnyy", 429 | "jobType": "remote", 430 | "status": "interview", 431 | "createdBy": "63628d5d178e918562ef9ce8", 432 | "createdAt": "2020-10-20T19:59:44Z" 433 | }, 434 | { 435 | "company": "Cartwright LLC", 436 | "position": "Engineer IV", 437 | "jobLocation": "Pétionville", 438 | "jobType": "internship", 439 | "status": "declined", 440 | "createdBy": "63628d5d178e918562ef9ce8", 441 | "createdAt": "2020-06-22T12:27:14Z" 442 | }, 443 | { 444 | "company": "Bode, Bernier and Trantow", 445 | "position": "Database Administrator II", 446 | "jobLocation": "San Agustin", 447 | "jobType": "full-time", 448 | "status": "pending", 449 | "createdBy": "63628d5d178e918562ef9ce8", 450 | "createdAt": "2021-05-05T01:43:43Z" 451 | }, 452 | { 453 | "company": "Kirlin LLC", 454 | "position": "Software Test Engineer IV", 455 | "jobLocation": "Isulan", 456 | "jobType": "remote", 457 | "status": "interview", 458 | "createdBy": "63628d5d178e918562ef9ce8", 459 | "createdAt": "2020-12-03T09:15:40Z" 460 | }, 461 | { 462 | "company": "Bechtelar-Bednar", 463 | "position": "Civil Engineer", 464 | "jobLocation": "Kiamba", 465 | "jobType": "internship", 466 | "status": "declined", 467 | "createdBy": "63628d5d178e918562ef9ce8", 468 | "createdAt": "2021-12-27T03:20:35Z" 469 | }, 470 | { 471 | "company": "Barrows Inc", 472 | "position": "Graphic Designer", 473 | "jobLocation": "Baihua", 474 | "jobType": "internship", 475 | "status": "interview", 476 | "createdBy": "63628d5d178e918562ef9ce8", 477 | "createdAt": "2021-11-22T08:24:08Z" 478 | }, 479 | { 480 | "company": "Boehm, Bashirian and Ledner", 481 | "position": "Internal Auditor", 482 | "jobLocation": "Princeton", 483 | "jobType": "part-time", 484 | "status": "interview", 485 | "createdBy": "63628d5d178e918562ef9ce8", 486 | "createdAt": "2021-02-01T05:24:13Z" 487 | }, 488 | { 489 | "company": "Swaniawski-Murphy", 490 | "position": "Web Designer IV", 491 | "jobLocation": "Liutan", 492 | "jobType": "remote", 493 | "status": "interview", 494 | "createdBy": "63628d5d178e918562ef9ce8", 495 | "createdAt": "2020-08-05T15:12:41Z" 496 | }, 497 | { 498 | "company": "Luettgen, Swaniawski and Altenwerth", 499 | "position": "Human Resources Manager", 500 | "jobLocation": "Fengmen", 501 | "jobType": "part-time", 502 | "status": "interview", 503 | "createdBy": "63628d5d178e918562ef9ce8", 504 | "createdAt": "2020-11-03T23:31:16Z" 505 | }, 506 | { 507 | "company": "Rutherford-Brown", 508 | "position": "Chief Design Engineer", 509 | "jobLocation": "Fulong", 510 | "jobType": "remote", 511 | "status": "declined", 512 | "createdBy": "63628d5d178e918562ef9ce8", 513 | "createdAt": "2020-06-29T07:04:40Z" 514 | }, 515 | { 516 | "company": "Roob and Sons", 517 | "position": "Assistant Professor", 518 | "jobLocation": "Kebonsari Kidul", 519 | "jobType": "full-time", 520 | "status": "declined", 521 | "createdBy": "63628d5d178e918562ef9ce8", 522 | "createdAt": "2021-05-01T11:04:20Z" 523 | }, 524 | { 525 | "company": "Orn Inc", 526 | "position": "Senior Sales Associate", 527 | "jobLocation": "Dmitriyevka", 528 | "jobType": "full-time", 529 | "status": "declined", 530 | "createdBy": "63628d5d178e918562ef9ce8", 531 | "createdAt": "2020-02-29T06:16:40Z" 532 | }, 533 | { 534 | "company": "Weissnat LLC", 535 | "position": "Data Coordiator", 536 | "jobLocation": "Néa Kíos", 537 | "jobType": "part-time", 538 | "status": "declined", 539 | "createdBy": "63628d5d178e918562ef9ce8", 540 | "createdAt": "2020-04-03T14:02:29Z" 541 | }, 542 | { 543 | "company": "Macejkovic Group", 544 | "position": "Executive Secretary", 545 | "jobLocation": "Piracuruca", 546 | "jobType": "full-time", 547 | "status": "interview", 548 | "createdBy": "63628d5d178e918562ef9ce8", 549 | "createdAt": "2021-08-18T13:07:09Z" 550 | }, 551 | { 552 | "company": "Cremin LLC", 553 | "position": "Environmental Tech", 554 | "jobLocation": "Meixian", 555 | "jobType": "full-time", 556 | "status": "declined", 557 | "createdBy": "63628d5d178e918562ef9ce8", 558 | "createdAt": "2021-12-10T00:41:19Z" 559 | }, 560 | { 561 | "company": "West-Cruickshank", 562 | "position": "Assistant Media Planner", 563 | "jobLocation": "San Ramón de la Nueva Orán", 564 | "jobType": "full-time", 565 | "status": "pending", 566 | "createdBy": "63628d5d178e918562ef9ce8", 567 | "createdAt": "2021-01-02T14:35:26Z" 568 | }, 569 | { 570 | "company": "Schmitt, Murphy and Simonis", 571 | "position": "Staff Scientist", 572 | "jobLocation": "Puntaru", 573 | "jobType": "internship", 574 | "status": "declined", 575 | "createdBy": "63628d5d178e918562ef9ce8", 576 | "createdAt": "2021-09-14T23:56:30Z" 577 | }, 578 | { 579 | "company": "Smitham Inc", 580 | "position": "Programmer Analyst I", 581 | "jobLocation": "Shanghu", 582 | "jobType": "internship", 583 | "status": "declined", 584 | "createdBy": "63628d5d178e918562ef9ce8", 585 | "createdAt": "2021-05-03T05:37:59Z" 586 | }, 587 | { 588 | "company": "Gottlieb LLC", 589 | "position": "Safety Technician III", 590 | "jobLocation": "Jingmen", 591 | "jobType": "part-time", 592 | "status": "pending", 593 | "createdBy": "63628d5d178e918562ef9ce8", 594 | "createdAt": "2020-04-09T13:05:35Z" 595 | }, 596 | { 597 | "company": "Erdman and Sons", 598 | "position": "Civil Engineer", 599 | "jobLocation": "Arrah", 600 | "jobType": "remote", 601 | "status": "pending", 602 | "createdBy": "63628d5d178e918562ef9ce8", 603 | "createdAt": "2020-07-25T22:53:24Z" 604 | }, 605 | { 606 | "company": "Rohan-Feil", 607 | "position": "Programmer II", 608 | "jobLocation": "Lidköping", 609 | "jobType": "full-time", 610 | "status": "pending", 611 | "createdBy": "63628d5d178e918562ef9ce8", 612 | "createdAt": "2020-06-10T07:35:43Z" 613 | }, 614 | { 615 | "company": "Mraz-Hilpert", 616 | "position": "Account Executive", 617 | "jobLocation": "Kota Kinabalu", 618 | "jobType": "full-time", 619 | "status": "pending", 620 | "createdBy": "63628d5d178e918562ef9ce8", 621 | "createdAt": "2021-12-07T20:53:45Z" 622 | }, 623 | { 624 | "company": "Runolfsdottir and Sons", 625 | "position": "Associate Professor", 626 | "jobLocation": "Hewan", 627 | "jobType": "full-time", 628 | "status": "interview", 629 | "createdBy": "63628d5d178e918562ef9ce8", 630 | "createdAt": "2021-07-24T07:35:08Z" 631 | }, 632 | { 633 | "company": "Schneider and Sons", 634 | "position": "Staff Scientist", 635 | "jobLocation": "Wadusari", 636 | "jobType": "full-time", 637 | "status": "declined", 638 | "createdBy": "63628d5d178e918562ef9ce8", 639 | "createdAt": "2020-04-01T09:32:34Z" 640 | }, 641 | { 642 | "company": "Gislason, Rice and Rosenbaum", 643 | "position": "Help Desk Operator", 644 | "jobLocation": "As Suwaydā", 645 | "jobType": "part-time", 646 | "status": "pending", 647 | "createdBy": "63628d5d178e918562ef9ce8", 648 | "createdAt": "2021-05-18T19:10:23Z" 649 | }, 650 | { 651 | "company": "Klocko and Sons", 652 | "position": "Actuary", 653 | "jobLocation": "Dianfang", 654 | "jobType": "full-time", 655 | "status": "pending", 656 | "createdBy": "63628d5d178e918562ef9ce8", 657 | "createdAt": "2021-12-08T18:33:34Z" 658 | }, 659 | { 660 | "company": "Shanahan-Runte", 661 | "position": "Developer III", 662 | "jobLocation": "Liuhe", 663 | "jobType": "remote", 664 | "status": "pending", 665 | "createdBy": "63628d5d178e918562ef9ce8", 666 | "createdAt": "2021-06-05T00:16:36Z" 667 | }, 668 | { 669 | "company": "Grimes, Cormier and Funk", 670 | "position": "Systems Administrator II", 671 | "jobLocation": "Levashi", 672 | "jobType": "full-time", 673 | "status": "interview", 674 | "createdBy": "63628d5d178e918562ef9ce8", 675 | "createdAt": "2021-05-07T06:49:04Z" 676 | } 677 | ] 678 | -------------------------------------------------------------------------------- /models/Job.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const JobSchema = new mongoose.Schema( 4 | { 5 | company: { 6 | type: String, 7 | required: [true, 'Please provide company'], 8 | maxlength: 50, 9 | }, 10 | position: { 11 | type: String, 12 | required: [true, 'Please provide position'], 13 | maxlength: 100, 14 | }, 15 | status: { 16 | type: String, 17 | enum: ['interview', 'declined', 'pending'], 18 | default: 'pending', 19 | }, 20 | jobType: { 21 | type: String, 22 | enum: ['full-time', 'part-time', 'remote', 'internship'], 23 | default: 'full-time', 24 | }, 25 | jobLocation: { 26 | type: String, 27 | default: 'my city', 28 | required: true, 29 | }, 30 | createdBy: { 31 | type: mongoose.Types.ObjectId, 32 | ref: 'User', 33 | required: [true, 'Please provide user'], 34 | }, 35 | }, 36 | { timestamps: true } 37 | ) 38 | 39 | export default mongoose.model('Job', JobSchema) 40 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import validator from 'validator' 3 | import bcrypt from 'bcryptjs' 4 | import jwt from 'jsonwebtoken' 5 | const UserSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | required: [true, 'Please provide name'], 9 | minlength: 3, 10 | maxlength: 20, 11 | trim: true, 12 | }, 13 | email: { 14 | type: String, 15 | required: [true, 'Please provide email'], 16 | validate: { 17 | validator: validator.isEmail, 18 | message: 'Please provide a valid email', 19 | }, 20 | unique: true, 21 | }, 22 | password: { 23 | type: String, 24 | required: [true, 'Please provide password'], 25 | minlength: 6, 26 | select: false, 27 | }, 28 | lastName: { 29 | type: String, 30 | trim: true, 31 | maxlength: 20, 32 | default: 'lastName', 33 | }, 34 | location: { 35 | type: String, 36 | trim: true, 37 | maxlength: 20, 38 | default: 'my city', 39 | }, 40 | }) 41 | 42 | UserSchema.pre('save', async function () { 43 | // console.log(this.modifiedPaths()) 44 | if (!this.isModified('password')) return 45 | const salt = await bcrypt.genSalt(10) 46 | this.password = await bcrypt.hash(this.password, salt) 47 | }) 48 | 49 | UserSchema.methods.createJWT = function () { 50 | return jwt.sign({ userId: this._id }, process.env.JWT_SECRET, { 51 | expiresIn: process.env.JWT_LIFETIME, 52 | }) 53 | } 54 | 55 | UserSchema.methods.comparePassword = async function (candidatePassword) { 56 | const isMatch = await bcrypt.compare(candidatePassword, this.password) 57 | return isMatch 58 | } 59 | 60 | export default mongoose.model('User', UserSchema) 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jobify", 3 | "version": "1.0.0", 4 | "description": "#### Track Your Job Search", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "install-dependencies": "npm run install-client && npm install", 9 | "setup-production": "npm run install-client && npm run build-client && npm install", 10 | "install-client": "cd client && npm install", 11 | "build-client": "cd client && npm run build", 12 | "server": "nodemon server --ignore client", 13 | "client": "npm start --prefix client", 14 | "start": "concurrently --kill-others-on-fail \" npm run server\" \" npm run client\"" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "concurrently": "^6.4.0", 21 | "nodemon": "^2.0.15" 22 | }, 23 | "dependencies": { 24 | "bcryptjs": "^2.4.3", 25 | "cookie-parser": "^1.4.6", 26 | "cors": "^2.8.5", 27 | "dotenv": "^10.0.0", 28 | "express": "^4.17.1", 29 | "express-async-errors": "^3.1.1", 30 | "express-mongo-sanitize": "^2.1.0", 31 | "express-rate-limit": "^6.0.4", 32 | "helmet": "^5.0.1", 33 | "http-status-codes": "^2.1.4", 34 | "jsonwebtoken": "^8.5.1", 35 | "moment": "^2.29.1", 36 | "mongoose": "^6.0.13", 37 | "morgan": "^1.10.0", 38 | "validator": "^13.7.0", 39 | "xss-clean": "^0.1.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /populate.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | import connectDB from './db/connect.js'; 7 | import Job from './models/Job.js'; 8 | 9 | const start = async () => { 10 | try { 11 | await connectDB(process.env.MONGO_URL); 12 | 13 | const jsonProducts = JSON.parse( 14 | await readFile(new URL('./mock-data.json', import.meta.url)) 15 | ); 16 | await Job.create(jsonProducts); 17 | console.log('Success!!!'); 18 | process.exit(0); 19 | } catch (error) { 20 | console.log(error); 21 | process.exit(1); 22 | } 23 | }; 24 | 25 | start(); 26 | -------------------------------------------------------------------------------- /routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | 4 | import rateLimiter from 'express-rate-limit'; 5 | const apiLimiter = rateLimiter({ 6 | windowMs: 15 * 60 * 1000, // 15 minutes 7 | max: 10, 8 | message: 'Too many requests from this IP, please try again after 15 minutes', 9 | }); 10 | 11 | import { 12 | register, 13 | login, 14 | updateUser, 15 | getCurrentUser, 16 | logout, 17 | } from '../controllers/authController.js'; 18 | import authenticateUser from '../middleware/auth.js'; 19 | import testUser from '../middleware/testUser.js'; 20 | router.route('/register').post(apiLimiter, register); 21 | router.route('/login').post(apiLimiter, login); 22 | router.get('/logout', logout); 23 | 24 | router.route('/updateUser').patch(authenticateUser, testUser, updateUser); 25 | router.route('/getCurrentUser').get(authenticateUser, getCurrentUser); 26 | 27 | export default router; 28 | -------------------------------------------------------------------------------- /routes/jobsRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | 4 | import { 5 | createJob, 6 | deleteJob, 7 | getAllJobs, 8 | updateJob, 9 | showStats, 10 | } from '../controllers/jobsController.js'; 11 | 12 | import testUser from '../middleware/testUser.js'; 13 | 14 | router.route('/').post(testUser, createJob).get(getAllJobs); 15 | // remember about :id 16 | router.route('/stats').get(showStats); 17 | router.route('/:id').delete(testUser, deleteJob).patch(testUser, updateJob); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const app = express(); 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | import 'express-async-errors'; 6 | import morgan from 'morgan'; 7 | 8 | import { dirname } from 'path'; 9 | import { fileURLToPath } from 'url'; 10 | import path from 'path'; 11 | 12 | import helmet from 'helmet'; 13 | import xss from 'xss-clean'; 14 | import mongoSanitize from 'express-mongo-sanitize'; 15 | import cookieParser from 'cookie-parser'; 16 | // hello 17 | // db and authenticateUser 18 | import connectDB from './db/connect.js'; 19 | 20 | // routers 21 | import authRouter from './routes/authRoutes.js'; 22 | import jobsRouter from './routes/jobsRoutes.js'; 23 | 24 | // middleware 25 | import notFoundMiddleware from './middleware/not-found.js'; 26 | import errorHandlerMiddleware from './middleware/error-handler.js'; 27 | import authenticateUser from './middleware/auth.js'; 28 | 29 | if (process.env.NODE_ENV !== 'production') { 30 | app.use(morgan('dev')); 31 | } 32 | 33 | const __dirname = dirname(fileURLToPath(import.meta.url)); 34 | 35 | // only when ready to deploy 36 | app.use(express.static(path.resolve(__dirname, './client/build'))); 37 | 38 | app.use(express.json()); 39 | app.use(helmet()); 40 | app.use(xss()); 41 | app.use(mongoSanitize()); 42 | app.use(cookieParser()); 43 | 44 | app.use('/api/v1/auth', authRouter); 45 | app.use('/api/v1/jobs', authenticateUser, jobsRouter); 46 | 47 | // only when ready to deploy 48 | app.get('*', (req, res) => { 49 | res.sendFile(path.resolve(__dirname, './client/build', 'index.html')); 50 | }); 51 | 52 | app.use(notFoundMiddleware); 53 | app.use(errorHandlerMiddleware); 54 | 55 | const port = process.env.PORT || 5000; 56 | 57 | const start = async () => { 58 | try { 59 | await connectDB(process.env.MONGO_URL); 60 | app.listen(port, () => { 61 | console.log(`Server is listening on port ${port}...`); 62 | }); 63 | } catch (error) { 64 | console.log(error); 65 | } 66 | }; 67 | 68 | start(); 69 | -------------------------------------------------------------------------------- /utils/attachCookie.js: -------------------------------------------------------------------------------- 1 | const attachCookie = ({ res, token }) => { 2 | const oneDay = 1000 * 60 * 60 * 24; 3 | 4 | res.cookie('token', token, { 5 | httpOnly: true, 6 | expires: new Date(Date.now() + oneDay), 7 | secure: process.env.NODE_ENV === 'production', 8 | }); 9 | }; 10 | 11 | export default attachCookie; 12 | -------------------------------------------------------------------------------- /utils/checkPermissions.js: -------------------------------------------------------------------------------- 1 | import { UnAuthenticatedError } from '../errors/index.js' 2 | 3 | const checkPermissions = (requestUser, resourceUserId) => { 4 | if (requestUser.userId === resourceUserId.toString()) return 5 | 6 | throw new UnAuthenticatedError('Not authorized to access this route') 7 | } 8 | 9 | export default checkPermissions 10 | --------------------------------------------------------------------------------