├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── _redirects
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── assets
├── logo.svg
├── main.svg
└── not-found.svg
├── axios.js
├── components
├── FormRow.js
├── JobColumns.js
├── Jobs.js
└── Navbar.js
├── context
├── actions.js
├── appContext.js
└── reducer.js
├── index.css
├── index.js
└── pages
├── Dashboard.js
├── Edit.js
├── Error.js
├── Home.js
├── PrivateRoute.js
├── Register.js
└── index.js
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jobs-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^11.2.7",
8 | "@testing-library/user-event": "^12.8.3",
9 | "axios": "^0.21.1",
10 | "moment": "^2.29.1",
11 | "normalize.css": "^8.0.1",
12 | "react": "^17.0.2",
13 | "react-dom": "^17.0.2",
14 | "react-icons": "^4.3.1",
15 | "react-router-dom": "^5.2.0",
16 | "react-scripts": "4.0.3",
17 | "styled-components": "^5.3.0",
18 | "web-vitals": "^1.1.2"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "CI= react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/john-smilga/react-jobs-app/5ca8d21c2a093e99b277ca0eda86314d83a7515a/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Jobio
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/john-smilga/react-jobs-app/5ca8d21c2a093e99b277ca0eda86314d83a7515a/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/john-smilga/react-jobs-app/5ca8d21c2a093e99b277ca0eda86314d83a7515a/public/logo512.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
2 | import { Home, Dashboard, Register, Edit, Error, PrivateRoute } from './pages';
3 | import Navbar from './components/Navbar';
4 | function App() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default App;
30 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/main.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/not-found.svg:
--------------------------------------------------------------------------------
1 | page not found
--------------------------------------------------------------------------------
/src/axios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | axios.defaults.baseURL = 'https://node-course-jobs-api.onrender.com/api/v1';
3 |
4 | axios.interceptors.request.use(function (req) {
5 | const user = localStorage.getItem('user');
6 |
7 | if (user) {
8 | const { token } = JSON.parse(localStorage.getItem('user'));
9 | req.headers.authorization = `Bearer ${token}`;
10 | return req;
11 | }
12 | return req;
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/FormRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const FormRow = ({
4 | type,
5 | name,
6 | value,
7 | handleChange,
8 | horizontal,
9 | placeholder,
10 | }) => {
11 | return (
12 |
13 | {!horizontal && (
14 |
15 | {name}
16 |
17 | )}
18 |
26 |
27 | );
28 | };
29 |
30 | export default FormRow;
31 |
--------------------------------------------------------------------------------
/src/components/JobColumns.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const JobColumns = () => {
5 | return (
6 |
7 | position
8 | company
9 | date
10 | status
11 | action
12 |
13 | );
14 | };
15 |
16 | const Wrapper = styled.section`
17 | display: none;
18 | @media (min-width: 992px) {
19 | display: block;
20 | background: var(--grey-200);
21 | color: var(--grey-500);
22 | border-top-left-radius: var(--borderRadius);
23 | border-top-right-radius: var(--borderRadius);
24 | display: grid;
25 | grid-template-columns: 1fr 1fr 150px 100px 100px;
26 | align-items: center;
27 | padding: 1rem 1.5rem;
28 | column-gap: 1rem;
29 | text-transform: capitalize;
30 | letter-spacing: var(--letterSpacing);
31 | font-size: var(--small-text);
32 | font-weight: 600;
33 | .action {
34 | margin-left: 1rem;
35 | }
36 | }
37 | `;
38 |
39 | export default JobColumns;
40 |
--------------------------------------------------------------------------------
/src/components/Jobs.js:
--------------------------------------------------------------------------------
1 | import { useGlobalContext } from '../context/appContext';
2 | import React from 'react';
3 | import styled from 'styled-components';
4 | import { Link } from 'react-router-dom';
5 | import { FaEdit, FaTrash } from 'react-icons/fa';
6 | import moment from 'moment';
7 | import JobColumns from './JobColumns';
8 |
9 | const Jobs = () => {
10 | const { jobs, isLoading, deleteJob } = useGlobalContext();
11 |
12 | if (isLoading) {
13 | return
;
14 | }
15 |
16 | if (jobs.length < 1) {
17 | return (
18 |
19 |
20 | Currently, you have no JOBS
21 | to display
22 |
23 |
24 | );
25 | }
26 |
27 | return (
28 | <>
29 |
30 |
31 | {jobs.map((item) => {
32 | const { _id: id, company, position, status, createdAt } = item;
33 | let date = moment(createdAt);
34 | date = date.format('MMMM Do, YYYY');
35 | return (
36 |
37 | {company.charAt(0)}
38 | {position.toLowerCase()}
39 | {company}
40 | {date}
41 |
42 | {status}
43 |
44 |
45 |
46 |
47 |
48 | deleteJob(id)}
52 | >
53 |
54 |
55 |
56 |
57 | );
58 | })}
59 |
60 | >
61 | );
62 | };
63 | const EmptyContainer = styled.section`
64 | text-align: center;
65 | h5 {
66 | text-transform: none;
67 | }
68 | span {
69 | color: var(--primary-500);
70 | }
71 | `;
72 | const Container = styled.section`
73 | .job {
74 | background: var(--white);
75 | border-radius: var(--borderRadius);
76 | margin-bottom: 2rem;
77 | display: grid;
78 | padding: 2rem 0;
79 | justify-content: center;
80 | text-align: center;
81 | }
82 | .icon {
83 | background: var(--primary-500);
84 | display: block;
85 | border-radius: var(--borderRadius);
86 | color: var(--white);
87 | font-size: 2rem;
88 | width: 40px;
89 | height: 40px;
90 | display: flex;
91 | align-items: center;
92 | justify-content: center;
93 | margin: 0 auto;
94 | margin-bottom: 1rem;
95 | }
96 | span {
97 | text-transform: capitalize;
98 | letter-spacing: var(--letterSpacing);
99 | }
100 | .position {
101 | font-weight: 600;
102 | }
103 | .date {
104 | color: var(--grey-500);
105 | }
106 | .status {
107 | border-radius: var(--borderRadius);
108 | text-transform: capitalize;
109 | letter-spacing: var(--letterSpacing);
110 | text-align: center;
111 | margin: 0.75rem auto;
112 | width: 100px;
113 | }
114 | .edit-btn {
115 | color: var(--green-dark);
116 | border-color: transparent;
117 | background: transparent !important;
118 | outline: transparent;
119 | border-radius: var(--borderRadius);
120 | cursor: pointer;
121 | display: inline-block;
122 | appearance: none;
123 | }
124 | .delete-btn {
125 | color: var(--red-dark);
126 | border-color: transparent;
127 | border-radius: var(--borderRadius);
128 | cursor: pointer;
129 | background: transparent;
130 | }
131 | .edit-btn,
132 | .delete-btn {
133 | font-size: 1rem;
134 | line-height: 1.15;
135 | margin-bottom: -3px;
136 | }
137 |
138 | .action-div {
139 | display: flex;
140 | align-items: center;
141 | justify-content: center;
142 | gap: 0 0.5rem;
143 | }
144 | @media (min-width: 768px) {
145 | display: grid;
146 | grid-template-columns: 1fr 1fr;
147 | column-gap: 1rem;
148 | }
149 | @media (min-width: 992px) {
150 | grid-template-columns: 1fr;
151 | .icon {
152 | display: none;
153 | }
154 | background: var(--white);
155 | border-bottom-left-radius: var(--borderRadius);
156 | border-bottom-right-radius: var(--borderRadius);
157 |
158 | .job {
159 | border-radius: 0;
160 | justify-content: left;
161 | text-align: left;
162 | border-bottom: 1px solid var(--grey-200);
163 | grid-template-columns: 1fr 1fr 150px 100px 100px;
164 | align-items: center;
165 | padding: 1rem 1.5rem;
166 | column-gap: 1rem;
167 | margin-bottom: 0;
168 | }
169 | .job:last-child {
170 | border-bottom: none;
171 | }
172 | span {
173 | font-size: var(--small-text);
174 | }
175 | .company,
176 | .position {
177 | font-weight: 400;
178 | text-transform: capitalize;
179 | }
180 | .date {
181 | font-weight: 400;
182 | color: var(--grey-500);
183 | }
184 |
185 | .status {
186 | font-size: var(--smallText);
187 | }
188 |
189 | .action-div {
190 | margin-left: 1rem;
191 | justify-content: left;
192 | }
193 | }
194 | `;
195 | const setStatusColor = (status) => {
196 | if (status === 'interview') return '#0f5132';
197 | if (status === 'declined') return '#842029';
198 | return '#927238';
199 | };
200 | const setStatusBackground = (status) => {
201 | if (status === 'interview') return '#d1e7dd';
202 | if (status === 'declined') return '#f8d7da';
203 | return '#f7f3d7';
204 | };
205 |
206 | const StatusContainer = styled.span`
207 | border-radius: var(--borderRadius);
208 | text-transform: capitalize;
209 | letter-spacing: var(--letterSpacing);
210 | text-align: center;
211 | color: ${(props) => setStatusColor(props.status)};
212 | background: ${(props) => setStatusBackground(props.status)};
213 | `;
214 | export default Jobs;
215 |
--------------------------------------------------------------------------------
/src/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import logo from '../assets/logo.svg';
4 | import { FaUserCircle, FaCaretDown } from 'react-icons/fa';
5 | import { useGlobalContext } from '../context/appContext';
6 |
7 | const Navbar = () => {
8 | const { user, logout } = useGlobalContext();
9 | const [showLogout, setShowLogout] = useState(false);
10 | return (
11 |
12 |
13 |
14 | {user && (
15 |
16 |
setShowLogout(!showLogout)}>
17 |
18 | {user}
19 |
20 |
21 |
22 | logout()} className='dropdown-btn'>
23 | logout
24 |
25 |
26 |
27 | )}
28 |
29 |
30 | );
31 | };
32 |
33 | const Wrapper = styled.nav`
34 | height: 6rem;
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 |
39 | .nav-center {
40 | width: var(--fluid-width);
41 | max-width: var(--max-width);
42 | display: flex;
43 | justify-content: space-between;
44 | align-items: center;
45 | }
46 | .btn-container {
47 | position: relative;
48 | }
49 | .btn {
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | gap: 0 0.5rem;
54 | position: relative;
55 | }
56 |
57 | .dropdown {
58 | position: absolute;
59 | top: 40px;
60 | left: 0;
61 | width: 100%;
62 | background: var(--white);
63 | padding: 0.5rem;
64 | text-align: center;
65 | visibility: hidden;
66 | transition: var(--transition);
67 | border-radius: var(--borderRadius);
68 | }
69 | .show-dropdown {
70 | visibility: visible;
71 | }
72 | .dropdown-btn {
73 | background: transparent;
74 | border-color: transparent;
75 | color: var(--primary-500);
76 | letter-spacing: var(--letterSpacing);
77 | text-transform: capitalize;
78 | cursor: pointer;
79 | }
80 | `;
81 |
82 | export default Navbar;
83 |
--------------------------------------------------------------------------------
/src/context/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_LOADING = 'SET_LOADING'
2 |
3 | export const REGISTER_USER_SUCCESS = 'REGISTER_USER_SUCCESS'
4 | export const REGISTER_USER_ERROR = 'REGISTER_USER_ERROR'
5 |
6 | export const SET_USER = 'SET_USER'
7 | export const FETCH_JOBS_SUCCESS = 'FETCH_JOBS_SUCCESS'
8 | export const FETCH_JOBS_ERROR = 'FETCH_JOBS_ERROR'
9 | export const LOGOUT_USER = 'LOGOUT_USER'
10 | export const CREATE_JOB_SUCCESS = 'CREATE_JOB_SUCCESS'
11 | export const CREATE_JOB_ERROR = 'CREATE_JOB_ERROR'
12 | export const DELETE_JOB_ERROR = 'DELETE_JOB_ERROR'
13 | export const FETCH_SINGLE_JOB_SUCCESS = 'FETCH_SINGLE_JOB_SUCCESS'
14 | export const FETCH_SINGLE_JOB_ERROR = 'FETCH_SINGLE_JOB_ERROR'
15 |
16 | export const EDIT_JOB_SUCCESS = 'EDIT_JOB_SUCCESS'
17 | export const EDIT_JOB_ERROR = 'EDIT_JOB_ERROR'
18 |
--------------------------------------------------------------------------------
/src/context/appContext.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import '../axios'
3 | import React, { useContext, useEffect, useReducer } from 'react'
4 | import {
5 | SET_LOADING,
6 | REGISTER_USER_SUCCESS,
7 | REGISTER_USER_ERROR,
8 | LOGOUT_USER,
9 | SET_USER,
10 | FETCH_JOBS_SUCCESS,
11 | FETCH_JOBS_ERROR,
12 | CREATE_JOB_SUCCESS,
13 | CREATE_JOB_ERROR,
14 | DELETE_JOB_ERROR,
15 | FETCH_SINGLE_JOB_SUCCESS,
16 | FETCH_SINGLE_JOB_ERROR,
17 | EDIT_JOB_SUCCESS,
18 | EDIT_JOB_ERROR,
19 | } from './actions'
20 | import reducer from './reducer'
21 |
22 | const initialState = {
23 | user: null,
24 | isLoading: false,
25 | jobs: [],
26 | showAlert: false,
27 | editItem: null,
28 | singleJobError: false,
29 | editComplete: false,
30 | }
31 | const AppContext = React.createContext()
32 |
33 | const AppProvider = ({ children }) => {
34 | const [state, dispatch] = useReducer(reducer, initialState)
35 |
36 | const setLoading = () => {
37 | dispatch({ type: SET_LOADING })
38 | }
39 |
40 | // register
41 | const register = async (userInput) => {
42 | setLoading()
43 | try {
44 | const { data } = await axios.post(`/auth/register`, {
45 | ...userInput,
46 | })
47 |
48 | dispatch({ type: REGISTER_USER_SUCCESS, payload: data.user.name })
49 | localStorage.setItem(
50 | 'user',
51 | JSON.stringify({ name: data.user.name, token: data.token })
52 | )
53 | } catch (error) {
54 | dispatch({ type: REGISTER_USER_ERROR })
55 | }
56 | }
57 |
58 | // login
59 | const login = async (userInput) => {
60 | setLoading()
61 | try {
62 | const { data } = await axios.post(`/auth/login`, {
63 | ...userInput,
64 | })
65 | dispatch({ type: REGISTER_USER_SUCCESS, payload: data.user.name })
66 | localStorage.setItem(
67 | 'user',
68 | JSON.stringify({ name: data.user.name, token: data.token })
69 | )
70 | } catch (error) {
71 | dispatch({ type: REGISTER_USER_ERROR })
72 | }
73 | }
74 |
75 | // logout
76 | const logout = () => {
77 | localStorage.removeItem('user')
78 | dispatch({ type: LOGOUT_USER })
79 | }
80 |
81 | // fetch jobs
82 | const fetchJobs = async () => {
83 | setLoading()
84 | try {
85 | const { data } = await axios.get(`/jobs`)
86 | dispatch({ type: FETCH_JOBS_SUCCESS, payload: data.jobs })
87 | } catch (error) {
88 | dispatch({ type: FETCH_JOBS_ERROR })
89 | logout()
90 | }
91 | }
92 |
93 | // create job
94 | const createJob = async (userInput) => {
95 | setLoading()
96 | try {
97 | const { data } = await axios.post(`/jobs`, {
98 | ...userInput,
99 | })
100 |
101 | dispatch({ type: CREATE_JOB_SUCCESS, payload: data.job })
102 | } catch (error) {
103 | dispatch({ type: CREATE_JOB_ERROR })
104 | }
105 | }
106 | const deleteJob = async (jobId) => {
107 | setLoading()
108 | try {
109 | await axios.delete(`/jobs/${jobId}`)
110 |
111 | fetchJobs()
112 | } catch (error) {
113 | dispatch({ type: DELETE_JOB_ERROR })
114 | }
115 | }
116 |
117 | const fetchSingleJob = async (jobId) => {
118 | setLoading()
119 | try {
120 | const { data } = await axios.get(`/jobs/${jobId}`)
121 | dispatch({ type: FETCH_SINGLE_JOB_SUCCESS, payload: data.job })
122 | } catch (error) {
123 | dispatch({ type: FETCH_SINGLE_JOB_ERROR })
124 | }
125 | }
126 | const editJob = async (jobId, userInput) => {
127 | setLoading()
128 | try {
129 | const { data } = await axios.patch(`/jobs/${jobId}`, {
130 | ...userInput,
131 | })
132 | dispatch({ type: EDIT_JOB_SUCCESS, payload: data.job })
133 | } catch (error) {
134 | dispatch({ type: EDIT_JOB_ERROR })
135 | }
136 | }
137 |
138 | useEffect(() => {
139 | const user = localStorage.getItem('user')
140 | if (user) {
141 | const newUser = JSON.parse(user)
142 | dispatch({ type: SET_USER, payload: newUser.name })
143 | }
144 | }, [])
145 | return (
146 |
160 | {children}
161 |
162 | )
163 | }
164 | // make sure use
165 | export const useGlobalContext = () => {
166 | return useContext(AppContext)
167 | }
168 |
169 | export { AppProvider }
170 |
--------------------------------------------------------------------------------
/src/context/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | REGISTER_USER_SUCCESS,
3 | REGISTER_USER_ERROR,
4 | SET_USER,
5 | FETCH_JOBS_SUCCESS,
6 | FETCH_JOBS_ERROR,
7 | LOGOUT_USER,
8 | SET_LOADING,
9 | CREATE_JOB_SUCCESS,
10 | CREATE_JOB_ERROR,
11 | DELETE_JOB_ERROR,
12 | FETCH_SINGLE_JOB_SUCCESS,
13 | FETCH_SINGLE_JOB_ERROR,
14 | EDIT_JOB_ERROR,
15 | EDIT_JOB_SUCCESS,
16 | } from './actions'
17 |
18 | const reducer = (state, action) => {
19 | if (action.type === SET_LOADING) {
20 | return { ...state, isLoading: true, showAlert: false, editComplete: false }
21 | }
22 |
23 | if (action.type === REGISTER_USER_SUCCESS) {
24 | return {
25 | ...state,
26 | isLoading: false,
27 | user: action.payload,
28 | }
29 | }
30 | if (action.type === REGISTER_USER_ERROR) {
31 | return {
32 | ...state,
33 | isLoading: false,
34 | user: null,
35 | showAlert: true,
36 | }
37 | }
38 |
39 | if (action.type === SET_USER) {
40 | return { ...state, user: action.payload }
41 | }
42 | if (action.type === LOGOUT_USER) {
43 | return {
44 | ...state,
45 | user: null,
46 | showAlert: false,
47 | jobs: [],
48 | isEditing: false,
49 | editItem: null,
50 | }
51 | }
52 |
53 | if (action.type === FETCH_JOBS_SUCCESS) {
54 | return {
55 | ...state,
56 | isLoading: false,
57 | editItem: null,
58 | singleJobError: false,
59 | editComplete: false,
60 | jobs: action.payload,
61 | }
62 | }
63 | if (action.type === FETCH_JOBS_ERROR) {
64 | return { ...state, isLoading: false }
65 | }
66 | if (action.type === CREATE_JOB_SUCCESS) {
67 | return {
68 | ...state,
69 | isLoading: false,
70 | jobs: [...state.jobs, action.payload],
71 | }
72 | }
73 | if (action.type === CREATE_JOB_ERROR) {
74 | return {
75 | ...state,
76 | isLoading: false,
77 | showAlert: true,
78 | }
79 | }
80 |
81 | if (action.type === DELETE_JOB_ERROR) {
82 | return {
83 | ...state,
84 | isLoading: false,
85 | showAlert: true,
86 | }
87 | }
88 |
89 | if (action.type === FETCH_SINGLE_JOB_SUCCESS) {
90 | return { ...state, isLoading: false, editItem: action.payload }
91 | }
92 | if (action.type === FETCH_SINGLE_JOB_ERROR) {
93 | return { ...state, isLoading: false, editItem: '', singleJobError: true }
94 | }
95 |
96 | if (action.type === EDIT_JOB_SUCCESS) {
97 | return {
98 | ...state,
99 | isLoading: false,
100 | editComplete: true,
101 | editItem: action.payload,
102 | }
103 | }
104 | if (action.type === EDIT_JOB_ERROR) {
105 | return {
106 | ...state,
107 | isLoading: false,
108 | editComplete: true,
109 | showAlert: true,
110 | }
111 | }
112 | throw new Error(`no such action : ${action}`)
113 | }
114 |
115 | export default reducer
116 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | *,
2 | ::after,
3 | ::before {
4 | box-sizing: border-box;
5 | }
6 | /* fonts */
7 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&family=Montserrat&display=swap');
8 |
9 | html {
10 | font-size: 100%;
11 | } /*16px*/
12 |
13 | :root {
14 | /* colors */
15 | --primary-100: #e2e0ff;
16 | --primary-200: #c1beff;
17 | --primary-300: #a29dff;
18 | --primary-400: #837dff;
19 | --primary-500: #645cff;
20 | --primary-600: #504acc;
21 | --primary-700: #3c3799;
22 | --primary-800: #282566;
23 | --primary-900: #141233;
24 |
25 | /* grey */
26 | --grey-50: #f8fafc;
27 | --grey-100: #f1f5f9;
28 | --grey-200: #e2e8f0;
29 | --grey-300: #cbd5e1;
30 | --grey-400: #94a3b8;
31 | --grey-500: #64748b;
32 | --grey-600: #475569;
33 | --grey-700: #334155;
34 | --grey-800: #1e293b;
35 | --grey-900: #0f172a;
36 | /* rest of the colors */
37 | --black: #222;
38 | --white: #fff;
39 | --red-light: #f8d7da;
40 | --red-dark: #842029;
41 | --green-light: #d1e7dd;
42 | --green-dark: #0f5132;
43 |
44 | /* fonts */
45 | --headingFont: 'Roboto', sans-serif;
46 | --bodyFont: 'Nunito', sans-serif;
47 | --smallText: 0.7em;
48 | --small-text: 0.875rem;
49 | /* rest of the vars */
50 | --backgroundColor: var(--grey-50);
51 | --textColor: var(--grey-900);
52 | --borderRadius: 0.25rem;
53 | --letterSpacing: 1px;
54 | --transition: 0.3s ease-in-out all;
55 | --max-width: 1120px;
56 | --fixed-width: 500px;
57 | --fluid-width: 90vw;
58 |
59 | /* box shadow*/
60 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
61 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
62 | 0 2px 4px -1px rgba(0, 0, 0, 0.06);
63 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
64 | 0 4px 6px -2px rgba(0, 0, 0, 0.05);
65 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
66 | 0 10px 10px -5px rgba(0, 0, 0, 0.04);
67 | }
68 |
69 | body {
70 | background: var(--backgroundColor);
71 | font-family: var(--bodyFont);
72 | font-weight: 400;
73 | line-height: 1.75;
74 | color: var(--textColor);
75 | }
76 |
77 | p {
78 | margin-bottom: 1.5rem;
79 | max-width: 40em;
80 | }
81 |
82 | h1,
83 | h2,
84 | h3,
85 | h4,
86 | h5 {
87 | margin: 0;
88 | margin-bottom: 1.38rem;
89 | font-family: var(--headingFont);
90 | font-weight: 400;
91 | line-height: 1.3;
92 | text-transform: capitalize;
93 | letter-spacing: var(--letterSpacing);
94 | }
95 |
96 | h1 {
97 | margin-top: 0;
98 | font-size: 3.052rem;
99 | }
100 |
101 | h2 {
102 | font-size: 2.441rem;
103 | }
104 |
105 | h3 {
106 | font-size: 1.953rem;
107 | }
108 |
109 | h4 {
110 | font-size: 1.563rem;
111 | }
112 |
113 | h5 {
114 | font-size: 1.25rem;
115 | }
116 |
117 | small,
118 | .text-small {
119 | font-size: var(--smallText);
120 | }
121 |
122 | a {
123 | text-decoration: none;
124 | }
125 | ul {
126 | list-style-type: none;
127 | padding: 0;
128 | }
129 |
130 | .img {
131 | width: 100%;
132 | display: block;
133 | object-fit: cover;
134 | }
135 | /* buttons */
136 |
137 | .btn {
138 | cursor: pointer;
139 | color: var(--white);
140 | background: var(--primary-500);
141 | border: transparent;
142 | border-radius: var(--borderRadius);
143 | letter-spacing: var(--letterSpacing);
144 | padding: 0.375rem 0.75rem;
145 | box-shadow: var(--shadow-1);
146 | transition: var(--transition);
147 | text-transform: capitalize;
148 | display: inline-block;
149 | }
150 | .btn:hover {
151 | background: var(--primary-700);
152 | box-shadow: var(--shadow-3);
153 | }
154 | .btn-hipster {
155 | color: var(--primary-500);
156 | background: var(--primary-200);
157 | }
158 | .btn-hipster:hover {
159 | color: var(--primary-200);
160 | background: var(--primary-700);
161 | }
162 | .btn-block {
163 | width: 100%;
164 | }
165 | .hero-btn {
166 | font-size: 1.25rem;
167 | padding: 0.5rem 1.25rem;
168 | }
169 | /* alerts */
170 | .alert {
171 | padding: 0.375rem 0.75rem;
172 | margin: 0 auto;
173 | border-color: transparent;
174 | border-radius: var(--borderRadius);
175 | width: var(--fluid-width);
176 | max-width: var(--fixed-width);
177 | text-align: center;
178 | text-transform: capitalize;
179 | }
180 |
181 | .alert-danger {
182 | color: var(--red-dark);
183 | background: var(--red-light);
184 | }
185 | .alert-success {
186 | color: var(--green-dark);
187 | background: var(--green-light);
188 | }
189 | /* form */
190 |
191 | .form {
192 | width: var(--fluid-width);
193 | max-width: var(--fixed-width);
194 | background: var(--white);
195 | border-radius: var(--borderRadius);
196 | box-shadow: var(--shadow-2);
197 | padding: 2rem 2.5rem;
198 | margin: 3rem auto;
199 | }
200 | .form-label {
201 | display: block;
202 | font-size: var(--smallText);
203 | margin-bottom: 0.5rem;
204 | text-transform: capitalize;
205 | letter-spacing: var(--letterSpacing);
206 | }
207 | .form-input,
208 | .form-textarea {
209 | width: 100%;
210 | padding: 0.375rem 0.75rem;
211 | border-radius: var(--borderRadius);
212 | background: var(--backgroundColor);
213 | border: 1px solid var(--grey-200);
214 | }
215 |
216 | .form-row {
217 | margin-bottom: 1rem;
218 | }
219 |
220 | .form-textarea {
221 | height: 7rem;
222 | }
223 | ::placeholder {
224 | font-family: inherit;
225 | /* color: var(--grey-400) !important; */
226 | letter-spacing: var(--letterSpacing);
227 | }
228 | .form-alert {
229 | color: var(--red-dark);
230 | letter-spacing: var(--letterSpacing);
231 | text-transform: capitalize;
232 | }
233 | /* alert */
234 |
235 | @keyframes spinner {
236 | to {
237 | transform: rotate(360deg);
238 | }
239 | }
240 |
241 | .loading {
242 | width: 6rem;
243 | height: 6rem;
244 | border: 5px solid var(--grey-400);
245 | border-radius: 50%;
246 | border-top-color: var(--primary-500);
247 | animation: spinner 0.6s linear infinite;
248 | margin: 0 auto;
249 | }
250 | .loading {
251 | margin: 0 auto;
252 | }
253 | /* title */
254 |
255 | .title {
256 | text-align: center;
257 | }
258 |
259 | .title-underline {
260 | background: var(--primary-500);
261 | width: 7rem;
262 | height: 0.25rem;
263 | margin: 0 auto;
264 | margin-top: -1rem;
265 | }
266 | .page {
267 | width: var(--fluid-width);
268 | max-width: var(--max-width);
269 | margin: 0 auto;
270 | }
271 | .full-page {
272 | min-height: 100vh;
273 | }
274 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import 'normalize.css'
4 | import './index.css'
5 | import App from './App'
6 | import { AppProvider } from './context/appContext'
7 | ReactDOM.render(
8 |
9 |
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | )
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 |
--------------------------------------------------------------------------------
/src/pages/Dashboard.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useState, useEffect } from 'react';
3 | import { useGlobalContext } from '../context/appContext';
4 | import FormRow from '../components/FormRow';
5 | import Navbar from '../components/Navbar';
6 | import Jobs from '../components/Jobs';
7 |
8 | function Dashboard() {
9 | const [values, setValues] = useState({ company: '', position: '' });
10 |
11 | const handleChange = (e) => {
12 | setValues({ ...values, [e.target.name]: e.target.value });
13 | };
14 |
15 | const { isLoading, showAlert, fetchJobs, createJob } = useGlobalContext();
16 |
17 | const handleSubmit = (e) => {
18 | e.preventDefault();
19 | const { company, position } = values;
20 | if (company && position) {
21 | createJob(values);
22 | setValues({ company: '', position: '' });
23 | }
24 | };
25 | useEffect(() => {
26 | fetchJobs();
27 | }, []);
28 | return (
29 | <>
30 |
31 |
32 |
33 | {showAlert && (
34 |
35 | there was an error, please try again
36 |
37 | )}
38 |
61 |
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | const Wrapper = styled.section`
69 | padding: 3rem 0;
70 |
71 | .job-form {
72 | background: var(--white);
73 | display: grid;
74 | row-gap: 1rem;
75 | column-gap: 0.5rem;
76 | align-items: center;
77 | margin-bottom: 3rem;
78 | border-radius: var(--borderRadius);
79 | padding: 1.5rem;
80 | .form-input {
81 | padding: 0.75rem;
82 | }
83 |
84 | .form-input:focus {
85 | outline: 1px solid var(--primary-500);
86 | }
87 | .form-row {
88 | margin-bottom: 0;
89 | }
90 | .btn {
91 | padding: 0.75rem;
92 | }
93 | @media (min-width: 776px) {
94 | grid-template-columns: 1fr 1fr auto;
95 | .btn {
96 | height: 100%;
97 | padding: 0 2rem;
98 | }
99 | column-gap: 2rem;
100 | }
101 | }
102 | .alert {
103 | max-width: var(--max-width);
104 | margin-bottom: 1rem;
105 | }
106 | `;
107 |
108 | export default Dashboard;
109 |
--------------------------------------------------------------------------------
/src/pages/Edit.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useParams, Redirect, Link } from 'react-router-dom';
3 | import styled from 'styled-components';
4 | import { useGlobalContext } from '../context/appContext';
5 | import FormRow from '../components/FormRow';
6 | import Navbar from '../components/Navbar';
7 | function Update() {
8 | const { id } = useParams();
9 | const {
10 | isLoading,
11 | editItem,
12 | fetchSingleJob,
13 | singleJobError: error,
14 | user,
15 | editJob,
16 | editComplete,
17 | } = useGlobalContext();
18 |
19 | const [values, setValues] = useState({
20 | company: '',
21 | position: '',
22 | status: '',
23 | });
24 |
25 | useEffect(() => {
26 | fetchSingleJob(id);
27 | }, [id]);
28 |
29 | useEffect(() => {
30 | if (editItem) {
31 | const { company, position, status } = editItem;
32 | setValues({ company, position, status });
33 | }
34 | }, [editItem]);
35 |
36 | const handleChange = (e) => {
37 | setValues({ ...values, [e.target.name]: e.target.value });
38 | };
39 | const handleSubmit = (e) => {
40 | e.preventDefault();
41 | const { company, position, status } = values;
42 | if (company && position) {
43 | editJob(id, { company, position, status });
44 | }
45 | };
46 | if (isLoading && !editItem) {
47 | return
;
48 | }
49 |
50 | if (!editItem || error) {
51 | return (
52 | <>
53 | {!user && }
54 |
55 | There was an error, please double check your job ID
56 |
57 |
58 | dasboard
59 |
60 |
61 | >
62 | );
63 | }
64 | return (
65 | <>
66 | {!user && }
67 |
68 |
69 |
70 |
71 | back home
72 |
73 |
74 |
115 |
116 | >
117 | );
118 | }
119 | const ErrorContainer = styled.section`
120 | text-align: center;
121 | padding-top: 6rem; ;
122 | `;
123 |
124 | const Container = styled.section`
125 | header {
126 | margin-top: 4rem;
127 | }
128 | .form {
129 | max-width: var(--max-width);
130 | margin-top: 2rem;
131 | }
132 | .form h4 {
133 | text-align: center;
134 | }
135 | .form > p {
136 | text-align: center;
137 | color: var(--green-dark);
138 | letter-spacing: var(--letterSpacing);
139 | margin-top: 0;
140 | }
141 | .status {
142 | background: var(--grey-100);
143 | border-radius: var(--borderRadius);
144 | border-color: transparent;
145 | padding: 0.25rem;
146 | }
147 | .back-home {
148 | text-align: center;
149 | display: block;
150 | width: 100%;
151 | font-size: 1rem;
152 | line-height: 1.15;
153 | background: var(--black);
154 | }
155 | .back-home:hover {
156 | background: var(--grey-500);
157 | }
158 | @media (min-width: 768px) {
159 | .back-home {
160 | width: 200px;
161 | }
162 | .form h4 {
163 | text-align: left;
164 | }
165 | .form-container {
166 | display: grid;
167 | grid-template-columns: 1fr 1fr 100px auto;
168 | column-gap: 0.5rem;
169 | align-items: center;
170 | }
171 |
172 | .form > p {
173 | text-align: left;
174 | }
175 | .form-row {
176 | margin-bottom: 0;
177 | }
178 | .submit-btn {
179 | align-self: end;
180 | }
181 | }
182 | `;
183 | export default Update;
184 |
--------------------------------------------------------------------------------
/src/pages/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Link } from 'react-router-dom';
4 | import img from '../assets/not-found.svg';
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 to home
13 |
14 |
15 | );
16 | };
17 |
18 | const Wrapper = styled.main`
19 | text-align: center;
20 | img {
21 | max-width: 600px;
22 | display: block;
23 | margin-bottom: 2rem;
24 | }
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | h3 {
29 | margin-bottom: 0.5rem;
30 | }
31 | p {
32 | margin-top: 0;
33 | margin-bottom: 0.5rem;
34 | color: var(--grey-500);
35 | }
36 | a {
37 | color: var(--primary-500);
38 | text-decoration: underline;
39 | }
40 | `;
41 |
42 | export default Error;
43 |
--------------------------------------------------------------------------------
/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import styled from 'styled-components';
3 | import main from '../assets/main.svg';
4 | import { useGlobalContext } from '../context/appContext';
5 | import { Redirect } from 'react-router-dom';
6 | import logo from '../assets/logo.svg';
7 | function Home() {
8 | const { user } = useGlobalContext();
9 |
10 | return (
11 | <>
12 | {user && }
13 |
14 |
15 |
16 |
17 |
18 |
19 |
job tracking app
20 |
21 | I'm baby viral enamel pin chartreuse cliche retro af selfies
22 | kinfolk photo booth plaid jianbing actually squid 3 wolf moon
23 | lumbersexual. Hell of humblebrag gluten-free lo-fi man braid
24 | leggings.
25 |
26 |
27 |
28 | Login / Register
29 |
30 |
31 |
32 |
33 |
34 | >
35 | );
36 | }
37 |
38 | const Wrapper = styled.div`
39 | .container {
40 | min-height: calc(100vh - 6rem);
41 | display: grid;
42 | align-items: center;
43 | margin-top: -3rem;
44 | }
45 | nav {
46 | width: var(--fluid-width);
47 | max-width: var(--max-width);
48 | margin: 0 auto;
49 | height: 6rem;
50 | display: flex;
51 | align-items: center;
52 | }
53 | h1 {
54 | font-weight: 700;
55 | }
56 | .main-img {
57 | display: none;
58 | }
59 | @media (min-width: 992px) {
60 | .container {
61 | grid-template-columns: 1fr 1fr;
62 | column-gap: 6rem;
63 | }
64 | .main-img {
65 | display: block;
66 | }
67 | }
68 | `;
69 |
70 | export default Home;
71 |
--------------------------------------------------------------------------------
/src/pages/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, Redirect } from 'react-router-dom'
3 | import { useGlobalContext } from '../context/appContext'
4 |
5 | const PrivateRoute = ({ children, ...rest }) => {
6 | const { user } = useGlobalContext()
7 |
8 | return (
9 | {
12 | return user ? children :
13 | }}
14 | >
15 | )
16 | }
17 | export default PrivateRoute
18 |
--------------------------------------------------------------------------------
/src/pages/Register.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import { useGlobalContext } from '../context/appContext';
4 | import { Redirect } from 'react-router-dom';
5 | import FormRow from '../components/FormRow';
6 | import logo from '../assets/logo.svg';
7 |
8 | function Register() {
9 | const [values, setValues] = useState({
10 | name: '',
11 | email: '',
12 | password: '',
13 | isMember: true,
14 | });
15 |
16 | const { user, register, login, isLoading, showAlert } = useGlobalContext();
17 | const toggleMember = () => {
18 | setValues({ ...values, isMember: !values.isMember });
19 | };
20 | const handleChange = (e) => {
21 | setValues({ ...values, [e.target.name]: e.target.value });
22 | };
23 | const onSubmit = (e) => {
24 | e.preventDefault();
25 | const { name, email, password, isMember } = values;
26 |
27 | if (isMember) {
28 | login({ email, password });
29 | } else {
30 | register({ name, email, password });
31 | }
32 | };
33 |
34 | return (
35 | <>
36 | {user && }
37 |
38 |
39 | {showAlert && (
40 |
41 | there was an error, please try again
42 |
43 | )}
44 |
92 |
93 |
94 | >
95 | );
96 | }
97 |
98 | const Wrapper = styled.section`
99 | display: grid;
100 | align-items: center;
101 | .logo {
102 | display: block;
103 | margin: 0 auto;
104 | margin-bottom: 1.38rem;
105 | }
106 | .form {
107 | max-width: 400;
108 | border-top: 5px solid var(--primary-500);
109 | }
110 |
111 | h4 {
112 | text-align: center;
113 | }
114 | p {
115 | margin: 0;
116 | margin-top: 1rem;
117 | text-align: center;
118 | }
119 | .btn {
120 | margin-top: 1rem;
121 | }
122 | .member-btn {
123 | background: transparent;
124 | border: transparent;
125 | color: var(--primary-500);
126 | cursor: pointer;
127 | }
128 | `;
129 |
130 | export default Register;
131 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import Register from './Register'
2 | import Home from './Home'
3 | import Dashboard from './Dashboard'
4 | import Edit from './Edit'
5 | import Error from './Error'
6 | import PrivateRoute from './PrivateRoute'
7 | export { Home, Register, Dashboard, Edit, Error, PrivateRoute }
8 |
--------------------------------------------------------------------------------