├── .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 | 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 | 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 | 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 | jobs app 14 | {user && ( 15 |
16 | 21 |
22 | 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 |
39 | {/* position */} 40 | 48 | {/* company */} 49 | 57 | 60 | 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 |
75 |

{editComplete && 'Success! Edit Complete'}

76 |

Update Job

77 | {/* company */} 78 |
79 | 85 | 91 |
92 | 95 | 105 |
106 | 113 |
114 |
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 | not found 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 | 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 | job hunt 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 |
45 | jobio 46 |

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

47 | {/* name field */} 48 | {!values.isMember && ( 49 | 55 | )} 56 | 57 | {/* single form row */} 58 | 64 | {/* end of single form row */} 65 | {/* single form row */} 66 | 72 | {/* end of single form row */} 73 | 80 |

81 | {values.isMember ? 'Not a member yet?' : 'Already a member?'} 82 | 83 | 90 |

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