├── .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 ├── components ├── FormRow.js └── Navbar.js ├── context.js ├── index.css ├── index.js ├── pages ├── Dashboard.js ├── Error.js ├── ForgotPassword.js ├── Home.js ├── Login.js ├── ProtectedRoute.js ├── Register.js ├── ResetPassword.js ├── Verify.js └── index.js └── utils ├── localState.js └── url.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 | #### Proxy 2 | 3 | "proxy": "https://user-workflow-11.herokuapp.com" 4 | "proxy": "http://localhost:5000" 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email-workflow-front-end", 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 | "normalize.css": "^8.0.1", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-icons": "^4.2.0", 14 | "react-router-dom": "^5.2.1", 15 | "react-scripts": "4.0.3", 16 | "styled-components": "^5.3.1", 17 | "web-vitals": "^1.1.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "CI= react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "proxy": "http://localhost:5000" 44 | } 45 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /api/* https://user-workflow-11.herokuapp.com/api/:splat 200 2 | 3 | /* /index.html 200 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-node-user-workflow-front-end/6052147b23c0d98401b89cc77c080f8741353791/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Auth Workflow 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-node-user-workflow-front-end/6052147b23c0d98401b89cc77c080f8741353791/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-node-user-workflow-front-end/6052147b23c0d98401b89cc77c080f8741353791/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 { 3 | Home, 4 | Error, 5 | Register, 6 | Login, 7 | Verify, 8 | Dashboard, 9 | ProtectedRoute, 10 | ForgotPassword, 11 | ResetPassword, 12 | } from './pages'; 13 | import Navbar from './components/Navbar'; 14 | import { useGlobalContext } from './context'; 15 | function App() { 16 | const { isLoading } = useGlobalContext(); 17 | if (isLoading) { 18 | return ( 19 |
20 |
21 |
22 | ); 23 | } 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | export default App; 58 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/main.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/FormRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormRow = ({ type, name, value, handleChange }) => { 4 | return ( 5 |
6 | 9 | 16 |
17 | ); 18 | }; 19 | 20 | export default FormRow; 21 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import logo from '../assets/logo.svg'; 4 | import { Link } from 'react-router-dom'; 5 | import { useGlobalContext } from '../context'; 6 | 7 | const Navbar = () => { 8 | const { user, logoutUser } = useGlobalContext(); 9 | return ( 10 | 11 |
12 | 13 | jobs app 14 | 15 | {user && ( 16 |
17 |

hello, {user.name}

18 | 26 |
27 | )} 28 |
29 |
30 | ); 31 | }; 32 | 33 | const Wrapper = styled.nav` 34 | background: var(--white); 35 | height: 6rem; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 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 | flex-wrap: wrap; 46 | } 47 | .nav-links { 48 | display: flex; 49 | flex-direction: column; 50 | } 51 | .nav-links p { 52 | margin: 0; 53 | text-transform: capitalize; 54 | margin-bottom: 0.25rem; 55 | } 56 | .home-link { 57 | display: flex; 58 | align-items: flex-end; 59 | } 60 | @media (min-width: 776px) { 61 | .nav-links { 62 | flex-direction: row; 63 | align-items: center; 64 | } 65 | .nav-links p { 66 | margin: 0; 67 | margin-right: 1.5rem; 68 | } 69 | } ; 70 | `; 71 | 72 | export default Navbar; 73 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useContext, useState, useEffect } from 'react'; 3 | import url from './utils/url'; 4 | const AppContext = React.createContext(); 5 | 6 | const AppProvider = ({ children }) => { 7 | const [isLoading, setIsLoading] = useState(true); 8 | const [user, setUser] = useState(null); 9 | const saveUser = (user) => { 10 | setUser(user); 11 | }; 12 | 13 | const removeUser = () => { 14 | setUser(null); 15 | }; 16 | 17 | const fetchUser = async () => { 18 | try { 19 | const { data } = await axios.get(`/api/v1/users/showMe`); 20 | saveUser(data.user); 21 | } catch (error) { 22 | removeUser(); 23 | } 24 | setIsLoading(false); 25 | }; 26 | 27 | const logoutUser = async () => { 28 | try { 29 | await axios.delete('/api/v1/auth/logout'); 30 | removeUser(); 31 | } catch (error) { 32 | console.log(error); 33 | } 34 | }; 35 | 36 | useEffect(() => { 37 | fetchUser(); 38 | }, []); 39 | 40 | return ( 41 | 49 | {children} 50 | 51 | ); 52 | }; 53 | // make sure use 54 | export const useGlobalContext = () => { 55 | return useContext(AppContext); 56 | }; 57 | 58 | export { AppProvider }; 59 | -------------------------------------------------------------------------------- /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 | /* rest of the vars */ 49 | --backgroundColor: var(--grey-50); 50 | --textColor: var(--grey-900); 51 | --borderRadius: 0.25rem; 52 | --letterSpacing: 1px; 53 | --transition: 0.3s ease-in-out all; 54 | --max-width: 1120px; 55 | --fixed-width: 500px; 56 | --fluid-width: 90vw; 57 | 58 | /* box shadow*/ 59 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 60 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 61 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 62 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 63 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 64 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 65 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 66 | } 67 | 68 | body { 69 | background: var(--backgroundColor); 70 | font-family: var(--bodyFont); 71 | font-weight: 400; 72 | line-height: 1.75; 73 | color: var(--textColor); 74 | } 75 | 76 | p { 77 | margin-bottom: 1.5rem; 78 | max-width: 40em; 79 | } 80 | 81 | h1, 82 | h2, 83 | h3, 84 | h4, 85 | h5 { 86 | margin: 0; 87 | margin-bottom: 1.38rem; 88 | font-family: var(--headingFont); 89 | font-weight: 400; 90 | line-height: 1.3; 91 | text-transform: capitalize; 92 | letter-spacing: var(--letterSpacing); 93 | } 94 | 95 | h1 { 96 | margin-top: 0; 97 | font-size: 3.052rem; 98 | } 99 | 100 | h2 { 101 | font-size: 2.441rem; 102 | } 103 | 104 | h3 { 105 | font-size: 1.953rem; 106 | } 107 | 108 | h4 { 109 | font-size: 1.563rem; 110 | } 111 | 112 | h5 { 113 | font-size: 1.25rem; 114 | } 115 | 116 | small, 117 | .text-small { 118 | font-size: var(--smallText); 119 | } 120 | 121 | a { 122 | text-decoration: none; 123 | } 124 | ul { 125 | list-style-type: none; 126 | padding: 0; 127 | } 128 | 129 | .img { 130 | width: 100%; 131 | display: block; 132 | object-fit: cover; 133 | } 134 | /* buttons */ 135 | 136 | .btn { 137 | cursor: pointer; 138 | color: var(--white); 139 | background: var(--primary-500); 140 | border: transparent; 141 | border-radius: var(--borderRadius); 142 | letter-spacing: var(--letterSpacing); 143 | padding: 0.375rem 0.75rem; 144 | box-shadow: var(--shadow-1); 145 | transition: var(--transition); 146 | text-transform: capitalize; 147 | display: inline-block; 148 | } 149 | .btn:hover { 150 | background: var(--primary-700); 151 | box-shadow: var(--shadow-3); 152 | } 153 | .btn-hipster { 154 | color: var(--primary-500); 155 | background: var(--primary-200); 156 | } 157 | .btn-hipster:hover { 158 | color: var(--primary-200); 159 | background: var(--primary-700); 160 | } 161 | .btn-block { 162 | width: 100%; 163 | } 164 | .btn-small { 165 | padding: 0.25rem 0.5rem; 166 | font-size: 0.75rem; 167 | } 168 | .btn:disabled { 169 | cursor: not-allowed; 170 | } 171 | /* alerts */ 172 | .alert { 173 | padding: 0.375rem 0.75rem; 174 | margin: 0 auto; 175 | border-color: transparent; 176 | border-radius: var(--borderRadius); 177 | width: var(--fluid-width); 178 | max-width: var(--fixed-width); 179 | text-align: center; 180 | text-transform: capitalize; 181 | } 182 | 183 | .alert-danger { 184 | color: var(--red-dark); 185 | background: var(--red-light); 186 | } 187 | .alert-success { 188 | color: var(--green-dark); 189 | background: var(--green-light); 190 | } 191 | /* form */ 192 | 193 | .form { 194 | width: var(--fluid-width); 195 | max-width: var(--fixed-width); 196 | background: var(--white); 197 | border-radius: var(--borderRadius); 198 | box-shadow: var(--shadow-2); 199 | padding: 2rem 2.5rem; 200 | margin: 3rem auto; 201 | position: relative; 202 | } 203 | .form-label { 204 | display: block; 205 | font-size: var(--smallText); 206 | margin-bottom: 0.5rem; 207 | text-transform: capitalize; 208 | letter-spacing: var(--letterSpacing); 209 | } 210 | .form-input, 211 | .form-textarea { 212 | width: 100%; 213 | padding: 0.375rem 0.75rem; 214 | border-radius: var(--borderRadius); 215 | background: var(--backgroundColor); 216 | border: 1px solid var(--grey-200); 217 | } 218 | 219 | .form-row { 220 | margin-bottom: 1rem; 221 | } 222 | 223 | .form-textarea { 224 | height: 7rem; 225 | } 226 | ::placeholder { 227 | font-family: inherit; 228 | color: var(--grey-400); 229 | } 230 | .form-alert { 231 | color: var(--red-dark); 232 | letter-spacing: var(--letterSpacing); 233 | text-transform: capitalize; 234 | } 235 | /* alert */ 236 | 237 | @keyframes spinner { 238 | to { 239 | transform: rotate(360deg); 240 | } 241 | } 242 | 243 | .loading { 244 | width: 6rem; 245 | height: 6rem; 246 | border: 5px solid var(--grey-400); 247 | border-radius: 50%; 248 | border-top-color: var(--primary-500); 249 | animation: spinner 0.6s linear infinite; 250 | margin: 0 auto; 251 | } 252 | .loading { 253 | margin: 0 auto; 254 | } 255 | /* title */ 256 | 257 | .title { 258 | text-align: center; 259 | } 260 | 261 | .title-underline { 262 | background: var(--primary-500); 263 | width: 7rem; 264 | height: 0.25rem; 265 | margin: 0 auto; 266 | margin-top: -1rem; 267 | } 268 | .page { 269 | min-height: calc(100vh - 6rem); 270 | width: var(--fluid-width); 271 | max-width: var(--max-width); 272 | margin: 0 auto; 273 | padding-top: 3rem; 274 | } 275 | .page-center { 276 | min-height: 100vh; 277 | display: grid; 278 | place-items: center; 279 | } 280 | .form-loading::after { 281 | content: ''; 282 | position: absolute; 283 | top: 0; 284 | left: 0; 285 | border-radius: var(--borderRadius); 286 | width: 100%; 287 | height: 100%; 288 | background: rgba(255, 255, 255, 0.6); 289 | cursor: not-allowed; 290 | } 291 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import 'normalize.css'; 5 | import App from './App'; 6 | import { AppProvider } from './context'; 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /src/pages/Dashboard.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import styled from 'styled-components'; 3 | import main from '../assets/main.svg'; 4 | import { Redirect } from 'react-router-dom'; 5 | import { useGlobalContext } from '../context'; 6 | function Dashboard() { 7 | const { user } = useGlobalContext(); 8 | const { name, userId, role } = user; 9 | return ( 10 | <> 11 | 12 |

Hello there, {user.name}

13 |

14 | Your ID : {userId} 15 |

16 |

17 | Your Role : {role} 18 |

19 |
20 | 21 | ); 22 | } 23 | 24 | const Wrapper = styled.div` 25 | p span { 26 | background: var(--primary-500); 27 | padding: 0.15rem 0.25rem; 28 | color: var(--white); 29 | border-radius: var(--borderRadius); 30 | letter-spacing: var(--letterSpacing); 31 | } 32 | `; 33 | 34 | export default Dashboard; 35 | -------------------------------------------------------------------------------- /src/pages/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | const Error = () => { 5 | return ( 6 | 7 |
8 |

404

9 |

page not found

10 | 11 | Back Home 12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | const Wrapper = styled.main` 19 | text-align: center; 20 | padding-top: 5rem; 21 | h1 { 22 | font-size: 9rem; 23 | } 24 | `; 25 | 26 | export default Error; 27 | -------------------------------------------------------------------------------- /src/pages/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import FormRow from '../components/FormRow'; 5 | import axios from 'axios'; 6 | import useLocalState from '../utils/localState'; 7 | 8 | const ForgotPassword = () => { 9 | const [email, setEmail] = useState(''); 10 | const { 11 | alert, 12 | showAlert, 13 | loading, 14 | setLoading, 15 | success, 16 | setSuccess, 17 | hideAlert, 18 | } = useLocalState(); 19 | 20 | const handleChange = (e) => { 21 | setEmail(e.target.value); 22 | }; 23 | 24 | const handleSubmit = async (e) => { 25 | e.preventDefault(); 26 | setLoading(true); 27 | hideAlert(); 28 | if (!email) { 29 | showAlert({ 30 | text: 'Please provide email', 31 | }); 32 | setLoading(false); 33 | return; 34 | } 35 | try { 36 | const { data } = await axios.post('/api/v1/auth/forgot-password', { 37 | email, 38 | }); 39 | showAlert({ text: data.msg, type: 'success' }); 40 | setSuccess(true); 41 | } catch (error) { 42 | showAlert({ 43 | text: 'Something went wrong, please try again', 44 | }); 45 | setSuccess(true); 46 | } 47 | setLoading(false); 48 | }; 49 | return ( 50 | 51 | {alert.show && ( 52 |
{alert.text}
53 | )} 54 | {!success && ( 55 |
59 |

Forgot password

60 | {/* single form row */} 61 | 67 | {/* end of single form row */} 68 | 71 |

72 | Already a have an account? 73 | 74 | Log In 75 | 76 |

77 | 78 | )} 79 |
80 | ); 81 | }; 82 | 83 | const Wrapper = styled.main` 84 | h4, 85 | p { 86 | text-align: center; 87 | } 88 | p { 89 | margin: 0; 90 | margin-top: 1rem; 91 | } 92 | .login-link { 93 | display: inline-block; 94 | margin-left: 0.25rem; 95 | text-transform: capitalize; 96 | color: var(--primary-500); 97 | cursor: pointer; 98 | } 99 | `; 100 | 101 | export default ForgotPassword; 102 | -------------------------------------------------------------------------------- /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 { Redirect } from 'react-router-dom'; 5 | import { useGlobalContext } from '../context'; 6 | function Home() { 7 | const { user } = useGlobalContext(); 8 | return ( 9 | <> 10 | {user && } 11 | 12 |
13 |

14 | Auth 15 | Workflow 16 |

17 |

18 | I'm baby viral enamel pin chartreuse cliche retro af selfies kinfolk 19 | photo booth plaid jianbing actually squid 3 wolf moon lumbersexual. 20 | Hell of humblebrag gluten-free lo-fi man braid leggings. 21 |

22 |

23 | Cloud bread kale chips wayfarers deep v chicharrones leggings 24 | fingerstache actually blog cliche four dollar toast. Sriracha ugh 25 | kickstarter, next level la croix butcher lomo. 26 |

27 | 28 | 29 | Login 30 | 31 | 32 | Register 33 | 34 |
35 | job hunt 36 |
37 | 38 | ); 39 | } 40 | 41 | const Wrapper = styled.div` 42 | display: grid; 43 | align-items: center; 44 | h2 { 45 | font-weight: 700; 46 | } 47 | h2 span { 48 | color: var(--primary-500); 49 | } 50 | .main-img { 51 | display: none; 52 | } 53 | @media (min-width: 992px) { 54 | grid-template-columns: 1fr 1fr; 55 | column-gap: 6rem; 56 | .main-img { 57 | display: block; 58 | } 59 | } 60 | .btn { 61 | margin-left: 0.25rem; 62 | margin-right: 0.25rem; 63 | } 64 | `; 65 | 66 | export default Home; 67 | -------------------------------------------------------------------------------- /src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link, useHistory, Redirect } from 'react-router-dom'; 4 | import FormRow from '../components/FormRow'; 5 | import { useGlobalContext } from '../context'; 6 | import useLocalState from '../utils/localState'; 7 | 8 | import axios from 'axios'; 9 | 10 | function Login() { 11 | const { saveUser } = useGlobalContext(); 12 | const history = useHistory(); 13 | const [values, setValues] = useState({ 14 | email: '', 15 | password: '', 16 | }); 17 | const { alert, showAlert, loading, setLoading, hideAlert } = useLocalState(); 18 | 19 | const handleChange = (e) => { 20 | setValues({ ...values, [e.target.name]: e.target.value }); 21 | }; 22 | const onSubmit = async (e) => { 23 | e.preventDefault(); 24 | hideAlert(); 25 | setLoading(true); 26 | const { email, password } = values; 27 | const loginUser = { email, password }; 28 | try { 29 | const { data } = await axios.post(`/api/v1/auth/login`, loginUser); 30 | setValues({ name: '', email: '', password: '' }); 31 | showAlert({ 32 | text: `Welcome, ${data.user.name}. Redirecting to dashboard...`, 33 | type: 'success', 34 | }); 35 | setLoading(false); 36 | saveUser(data.user); 37 | history.push('/dashboard'); 38 | } catch (error) { 39 | showAlert({ text: error.response.data.msg }); 40 | setLoading(false); 41 | } 42 | }; 43 | 44 | return ( 45 | <> 46 | 47 | {alert.show && ( 48 |
{alert.text}
49 | )} 50 |
54 | {/* single form row */} 55 | 61 | {/* end of single form row */} 62 | {/* single form row */} 63 | 69 | {/* end of single form row */} 70 | 73 |

74 | Don't have an account? 75 | 76 | Register 77 | 78 |

79 |

80 | Forgot your password?{' '} 81 | 82 | Reset Password 83 | 84 |

85 | 86 |
87 | 88 | ); 89 | } 90 | 91 | const Wrapper = styled.section` 92 | .alert { 93 | margin-top: 3rem; 94 | } 95 | h4 { 96 | text-align: center; 97 | } 98 | p { 99 | margin: 0; 100 | text-align: center; 101 | } 102 | .btn { 103 | margin-bottom: 1.5rem; 104 | } 105 | .register-link, 106 | .reset-link { 107 | display: inline-block; 108 | margin-left: 0.25rem; 109 | text-transform: capitalize; 110 | color: var(--primary-500); 111 | cursor: pointer; 112 | } 113 | .reset-link { 114 | margin-top: 0.25rem; 115 | } 116 | .btn:disabled { 117 | cursor: not-allowed; 118 | } 119 | `; 120 | 121 | export default Login; 122 | -------------------------------------------------------------------------------- /src/pages/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | // import { useUserContext } from '../context/user_context' 4 | import { useGlobalContext } from '../context'; 5 | 6 | const PrivateRoute = ({ children, ...rest }) => { 7 | const { user } = useGlobalContext(); 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 { Link } from 'react-router-dom'; 4 | import FormRow from '../components/FormRow'; 5 | import axios from 'axios'; 6 | import useLocalState from '../utils/localState'; 7 | 8 | function Register() { 9 | const [values, setValues] = useState({ 10 | name: '', 11 | email: '', 12 | password: '', 13 | }); 14 | 15 | const { 16 | alert, 17 | showAlert, 18 | loading, 19 | setLoading, 20 | success, 21 | setSuccess, 22 | hideAlert, 23 | } = useLocalState(); 24 | 25 | const handleChange = (e) => { 26 | setValues({ ...values, [e.target.name]: e.target.value }); 27 | }; 28 | const onSubmit = async (e) => { 29 | e.preventDefault(); 30 | hideAlert(); 31 | setLoading(true); 32 | const { name, email, password } = values; 33 | const registerNewUser = { name, email, password }; 34 | 35 | try { 36 | const { data } = await axios.post( 37 | `/api/v1/auth/register`, 38 | registerNewUser 39 | ); 40 | 41 | setSuccess(true); 42 | setValues({ name: '', email: '', password: '' }); 43 | showAlert({ text: data.msg, type: 'success' }); 44 | } catch (error) { 45 | const { msg } = error.response.data; 46 | showAlert({ text: msg }); 47 | } 48 | setLoading(false); 49 | }; 50 | 51 | return ( 52 | <> 53 | 54 | {alert.show && ( 55 |
{alert.text}
56 | )} 57 | {!success && ( 58 |
62 | {/* single form row */} 63 | 64 | 70 | 71 | {/* single form row */} 72 | 78 | {/* end of single form row */} 79 | {/* single form row */} 80 | 86 | {/* end of single form row */} 87 | 90 |

91 | Already a have an account? 92 | 93 | Log In 94 | 95 |

96 | 97 | )} 98 |
99 | 100 | ); 101 | } 102 | 103 | const Wrapper = styled.section` 104 | .alert { 105 | margin-top: 3rem; 106 | margin-bottom: -1.5rem; 107 | } 108 | h4 { 109 | text-align: center; 110 | } 111 | p { 112 | margin: 0; 113 | margin-top: 1rem; 114 | text-align: center; 115 | } 116 | .login-link { 117 | display: inline-block; 118 | margin-left: 0.25rem; 119 | text-transform: capitalize; 120 | color: var(--primary-500); 121 | cursor: pointer; 122 | } 123 | .btn:disabled { 124 | cursor: not-allowed; 125 | } 126 | `; 127 | 128 | export default Register; 129 | -------------------------------------------------------------------------------- /src/pages/ResetPassword.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useLocation, useHistory, Link } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import axios from 'axios'; 5 | import FormRow from '../components/FormRow'; 6 | import useLocalState from '../utils/localState'; 7 | 8 | function useQuery() { 9 | return new URLSearchParams(useLocation().search); 10 | } 11 | 12 | const ResetPasswordForm = () => { 13 | const history = useHistory(); 14 | const [password, setPassword] = useState(''); 15 | const { alert, showAlert, loading, setLoading, success, setSuccess } = 16 | useLocalState(); 17 | 18 | const query = useQuery(); 19 | 20 | const handleChange = async (e) => { 21 | setPassword(e.target.value); 22 | }; 23 | const handleSubmit = async (e) => { 24 | e.preventDefault(); 25 | setLoading(true); 26 | if (!password) { 27 | showAlert({ text: 'please enter password' }); 28 | setLoading(false); 29 | return; 30 | } 31 | try { 32 | const { data } = await axios.post('/api/v1/auth/reset-password', { 33 | password, 34 | token: query.get('token'), 35 | email: query.get('email'), 36 | }); 37 | setLoading(false); 38 | setSuccess(true); 39 | showAlert({ 40 | text: `Success, redirecting to login page shortly`, 41 | type: 'success', 42 | }); 43 | setTimeout(() => { 44 | history.push('/login'); 45 | }, 3000); 46 | } catch (error) { 47 | showAlert({ text: error.response.data.msg }); 48 | setLoading(false); 49 | } 50 | }; 51 | return ( 52 | 53 | {alert.show && ( 54 |
{alert.text}
55 | )} 56 | {!success && ( 57 |
61 |

reset password

62 | {/* single form row */} 63 | 69 | {/* end of single form row */} 70 | 73 | 74 | )} 75 |
76 | ); 77 | }; 78 | 79 | const Wrapper = styled.section` 80 | h4, 81 | p { 82 | text-align: center; 83 | } 84 | p { 85 | margin: 0; 86 | margin-top: 1rem; 87 | } 88 | `; 89 | 90 | export default ResetPasswordForm; 91 | -------------------------------------------------------------------------------- /src/pages/Verify.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useLocation, Link } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import { useGlobalContext } from '../context'; 5 | import axios from 'axios'; 6 | function useQuery() { 7 | return new URLSearchParams(useLocation().search); 8 | } 9 | 10 | const VerifyPage = () => { 11 | const [error, setError] = useState(false); 12 | const [loading, setLoading] = useState(false); 13 | const { isLoading } = useGlobalContext(); 14 | const query = useQuery(); 15 | 16 | const verifyToken = async () => { 17 | setLoading(true); 18 | try { 19 | const { data } = await axios.post('/api/v1/auth/verify-email', { 20 | verificationToken: query.get('token'), 21 | email: query.get('email'), 22 | }); 23 | } catch (error) { 24 | // console.log(error.response); 25 | setError(true); 26 | } 27 | setLoading(false); 28 | }; 29 | 30 | useEffect(() => { 31 | if (!isLoading) { 32 | verifyToken(); 33 | } 34 | }, []); 35 | 36 | if (loading) { 37 | return ( 38 | 39 |

Loading...

40 |
41 | ); 42 | } 43 | 44 | if (error) { 45 | return ( 46 | 47 |

There was an error, please double check your verification link

48 |
49 | ); 50 | } 51 | 52 | return ( 53 | 54 |

Account Confirmed

55 | 56 | Please login 57 | 58 |
59 | ); 60 | }; 61 | 62 | const Wrapper = styled.section``; 63 | 64 | export default VerifyPage; 65 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | import Error from './Error'; 3 | import Register from './Register'; 4 | import Login from './Login'; 5 | import Verify from './Verify'; 6 | import Dashboard from './Dashboard'; 7 | import ProtectedRoute from './ProtectedRoute'; 8 | import ForgotPassword from './ForgotPassword'; 9 | import ResetPassword from './ResetPassword'; 10 | export { 11 | Home, 12 | Error, 13 | Register, 14 | Login, 15 | Verify, 16 | Dashboard, 17 | ProtectedRoute, 18 | ForgotPassword, 19 | ResetPassword, 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/localState.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useLocalState = () => { 4 | const [alert, setAlert] = useState({ 5 | show: false, 6 | text: '', 7 | type: 'danger', 8 | }); 9 | const [loading, setLoading] = useState(false); 10 | const [success, setSuccess] = useState(false); 11 | 12 | const showAlert = ({ text, type = 'danger' }) => { 13 | setAlert({ show: true, text, type }); 14 | }; 15 | const hideAlert = () => { 16 | setAlert({ show: false, text: '', type: 'danger' }); 17 | }; 18 | return { 19 | alert, 20 | showAlert, 21 | loading, 22 | setLoading, 23 | success, 24 | setSuccess, 25 | hideAlert, 26 | }; 27 | }; 28 | 29 | export default useLocalState; 30 | -------------------------------------------------------------------------------- /src/utils/url.js: -------------------------------------------------------------------------------- 1 | const url = 'http://localhost:5000'; 2 | 3 | export default url; 4 | --------------------------------------------------------------------------------