├── .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 |
6 |
--------------------------------------------------------------------------------
/client/src/assets/images/main-alternative.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/images/main.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/images/not-found.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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
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 |
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 |
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 |

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 |

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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------