├── .gitignore ├── README.md ├── client ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── robots.txt │ └── static │ │ ├── css │ │ ├── main.773b908a.css │ │ └── main.773b908a.css.map │ │ └── js │ │ ├── main.1b0e90fa.js │ │ ├── main.1b0e90fa.js.LICENSE.txt │ │ └── main.1b0e90fa.js.map ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── components │ ├── AddTaskForm.js │ ├── Dashboard.js │ ├── EditTaskForm.js │ ├── Footer.js │ ├── Login.js │ ├── Logout.js │ ├── Navbar.js │ ├── Signup.js │ ├── Task.js │ ├── TaskList.js │ └── css │ │ ├── AddTaskForm.css │ │ ├── Dashboard.css │ │ ├── EditTaskForm.css │ │ ├── Footer.css │ │ ├── Login.css │ │ ├── Signup.css │ │ ├── Task.css │ │ └── TaskList.css │ ├── contexts │ └── TaskContext.js │ ├── hooks │ ├── useFormInput.js │ └── useToggle.js │ ├── index.js │ └── services │ └── service.js ├── package-lock.json ├── package.json └── server ├── db └── mongoose.js ├── emails └── email.js ├── index.js ├── middleware └── authToken.js ├── models ├── taskModel.js └── userModel.js └── routers ├── taskRouter.js └── userRouter.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task manager 2 | An online task manager app that you can access from anywhere as long as you remember your email address. 3 | 4 | ### Signup form 5 | A user provides their details and upon verification they are saved in the database and a welcome email notification is sent to them. 6 | 7 | ![signup](https://user-images.githubusercontent.com/72663882/181628729-3c128178-e350-4033-8f4c-a61c6980e88f.png) 8 | 9 | ### Login form 10 | If the user already has an account they can login and track their tasks (delete or add them) 11 | 12 | ![login](https://user-images.githubusercontent.com/72663882/181627696-15f6076c-7ab6-4f59-82a7-2b54df63b1e3.png) 13 | 14 | ### Dashboard 15 | ![dashboard](https://user-images.githubusercontent.com/72663882/181628312-813d71d1-de54-4bb3-813d-7a6df968e3df.png) 16 | ![dashboard](https://user-images.githubusercontent.com/72663882/181628910-d88117b6-70b8-4795-9bd9-5ce194095fee.png) 17 | 18 | -------------------------------------------------------------------------------- /client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.773b908a.css", 4 | "main.js": "/static/js/main.1b0e90fa.js", 5 | "index.html": "/index.html", 6 | "main.773b908a.css.map": "/static/css/main.773b908a.css.map", 7 | "main.1b0e90fa.js.map": "/static/js/main.1b0e90fa.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.773b908a.css", 11 | "static/js/main.1b0e90fa.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/node-react-task-manager/14ad231a294dbd4d2b61eab6d52caa98a114a91d/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /client/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/node-react-task-manager/14ad231a294dbd4d2b61eab6d52caa98a114a91d/client/build/logo192.png -------------------------------------------------------------------------------- /client/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/node-react-task-manager/14ad231a294dbd4d2b61eab6d52caa98a114a91d/client/build/logo512.png -------------------------------------------------------------------------------- /client/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/build/static/css/main.773b908a.css: -------------------------------------------------------------------------------- 1 | .Signup__wrapper{background:#ecf0f1;border-radius:5px;box-shadow:3px 3px 10px #333;max-width:550px;min-width:330px;padding:15px}.Signup__wrapper h2{color:#34495e;font-size:2em;font-weight:200;margin-top:10px;text-align:center}.Signup__link{color:#34495e;text-align:center;text-decoration:none}.Signup__link a{font-weight:600;padding-left:3px;text-decoration:none}.Signup__form{padding-top:20px}.Signup__button,.Signup__email,.Signup__password,.Signup__username{border-radius:5px;height:40px;margin-bottom:25px;margin-left:10%;outline:none;width:80%}.Signup__email,.Signup__password,.Signup__username{border:1px solid #bbb;font-size:14px;padding:0 0 0 10px}.Signup__email:focus,.Signup__password:focus,.Signup__username:focus{border:1px solid #3498db}.Signup__button{background:#e74c3c;border:none;color:#fff;cursor:pointer;font-size:18px;font-weight:200;transition:box-shadow .4s ease;width:83%}.Signup__button:active,.Signup__button:hover{box-shadow:1px 1px 5px #555}.Signup__error{border:1px solid red;border-radius:5px;color:red;margin-bottom:-10px;margin-left:10%;padding:5px;text-align:center;width:80%}body{align-items:center;background:#333;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='64' height='64' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm33.414-6 5.95-5.95L45.95.636 40 6.586 34.05.636 32.636 2.05 38.586 8l-5.95 5.95 1.414 1.414L40 9.414l5.95 5.95 1.414-1.414L41.414 8zM40 48a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-2a6 6 0 1 0 0-12 6 6 0 0 0 0 12zM9.414 40l5.95-5.95-1.414-1.414L8 38.586l-5.95-5.95L.636 34.05 6.586 40l-5.95 5.95 1.414 1.414L8 41.414l5.95 5.95 1.414-1.414L9.414 40z' fill='%239C92AC' fill-opacity='.4' fill-rule='evenodd'/%3E%3C/svg%3E");display:flex;flex-direction:column;font-family:Helvetica Neue,Arial,Sans-Serif;justify-content:center;margin:0;min-height:100vh}.Login__wrap{background:#ecf0f1;border-radius:5px;box-shadow:3px 3px 10px #333;max-width:550px;min-width:330px;padding:15px}.Login__wrap h2{font-size:2em;font-weight:200;margin-top:10px}.Login__link,.Login__wrap h2{color:#34495e;text-align:center}.Login__link,.Login__link a{text-decoration:none}.Login__link a{font-weight:600;padding-left:3px}.Login__form{padding-top:20px}.Login__button,.Login__email,.Login__password{border-radius:5px;height:40px;margin-bottom:25px;margin-left:10%;outline:none;width:80%}.Login__email,.Login__password{border:1px solid #bbb;font-size:14px;padding:0 0 0 10px}.Login__email:focus,.Login__password:focus{border:1px solid #3498db}.Login__button{background:#e74c3c;border:none;color:#fff;cursor:pointer;font-size:18px;font-weight:200;transition:box-shadow .4s ease;width:83%}.Login__button:active,.Login__button:hover{box-shadow:1px 1px 5px #555}.Login__error{border:1px solid red;border-radius:5px;color:red;margin-bottom:-10px;margin-left:10%;padding:5px;text-align:center;width:80%}.Footer{background:#151414;color:#fff;display:flex;flex-direction:column;margin-top:auto}.Footer__information{border-bottom:1px solid #373636;height:100px}.Footer__copyright{align-items:center;background:#202020;display:flex;flex-wrap:wrap;justify-content:space-between;padding:20px 30px}.Footer__copyright-text{font-size:14px}.Footer__copyright-text a{color:#ff5e14;padding-left:3px;text-decoration:none}.Footer__menu ul{display:flex}.Footer__menu li{list-style:none}.Footer__menu li>*{margin-right:20px}.Footer__menu li:hover a{color:#ff5e14}.Footer__menu li a{color:#878787;font-size:14px;text-decoration:none}@media (max-width:440px){.Footer__copyright{flex-direction:column}.Footer__menu ul{justify-content:center;padding-left:20px}}.Dashboard{color:#fff;display:flex;flex-direction:column;min-height:100vh;width:100vw}.Dashboard__header{background-color:#e74c3c;padding:5px 20px}.Dashboard__nav{display:flex;justify-content:space-between}.Dashboard__img{background-color:#333;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49'%3E%3Cpath d='m13.99 9.25 13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z' fill='%239C92AC' fill-opacity='.4' fill-rule='nonzero'/%3E%3C/svg%3E");border:1px solid #fff;border-radius:50%;height:80px;width:80px}.Dashboard__img img{border-radius:50%;height:auto;width:100%}.Dashboard__details{align-items:flex-end;display:flex;flex-direction:column;padding:10px}.Dashboard__email,.Dashboard__username{margin-top:3px}.Dashboard__title{font-size:48px;margin-bottom:0;text-align:center}.Dashboard__list{list-style:none}.AddTaskForm{display:flex;flex-wrap:wrap;justify-content:center;margin-bottom:10px;margin-top:10px}.AddTaskForm__input{background-color:#f7f1f1;font-size:medium}.AddTaskForm__btn,.AddTaskForm__input{border:none;outline:none;padding:0 1em}.AddTaskForm__btn{background-color:#8e0e00;background-color:#a01809;border:1px solid hsla(0,0%,100%,.3);color:#fff;cursor:pointer;font-weight:700;margin-left:5px;text-transform:uppercase;transition:background .2s ease-out}.AddTaskForm__btn,.AddTaskForm__input{font-family:Quicksand,sans-serif;padding:1rem 1em}.EditTaskForm{align-items:center;background:hsla(0,0%,100%,.2);display:flex;justify-content:space-between;margin:0 -3rem 4px;overflow:hidden;padding:1rem 3rem;position:relative;transition:opacity .5s ease-in-out}.EditTaskForm__input{background-color:#f7f1f1;border:none;flex-grow:1;font-size:medium;outline:none;padding:0 1em}.EditTaskForm__btn{background-color:#8e0e00;background-color:#a01809;border:none;border:1px solid hsla(0,0%,100%,.3);color:#fff;cursor:pointer;font-weight:700;margin-left:5px;padding:0 1em;text-transform:uppercase;transition:background .2s ease-out}.EditTaskForm__btn,.EditTaskForm__input{font-family:Quicksand,sans-serif;padding:1rem 1em}.Task{align-items:center;background:hsla(0,0%,100%,.2);display:flex;justify-content:space-between;margin:3px -3px 4px;overflow:hidden;padding:1rem 3rem;position:relative;transition:opacity .5s ease-in-out}.Task__item{font-size:1.25rem;list-style:none}.Task__btns{flex-shrink:0;margin-left:auto;padding-left:.7em}.Task__btn{-webkit-appearance:none;background-color:#8e0e00;border:none;color:#fff;cursor:pointer;font-family:Quicksand,sans-serif;font-size:1em;margin:.4em;padding:.7rem 1.3rem}.TaskList{display:flex;justify-content:center}.TaskList__lists{margin:0;padding:0} 2 | /*# sourceMappingURL=main.773b908a.css.map*/ -------------------------------------------------------------------------------- /client/build/static/css/main.773b908a.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.773b908a.css","mappings":"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","sources":["components/css/Signup.css","components/css/Login.css","components/css/Footer.css","components/css/Dashboard.css","components/css/AddTaskForm.css","components/css/EditTaskForm.css","components/css/Task.css","components/css/TaskList.css"],"sourcesContent":["\r\n.Signup__wrapper{\r\n background: #ecf0f1;\r\n max-width: 550px;\r\n min-width: 330px;\r\n border-radius: 5px;\r\n box-shadow: 3px 3px 10px #333;\r\n padding: 15px;\r\n}\r\n.Signup__wrapper h2{\r\n text-align: center;\r\n font-weight: 200;\r\n font-size: 2em;\r\n margin-top: 10px;\r\n color: #34495e;\r\n}\r\n.Signup__link{\r\n text-align: center;\r\n color: #34495e;\r\n text-decoration: none;\r\n}\r\n.Signup__link a{\r\n text-decoration: none;\r\n font-weight: 600;\r\n padding-left: 3px;\r\n}\r\n.Signup__form{\r\n padding-top: 20px;\r\n}\r\n.Signup__username,.Signup__email,.Signup__password,.Signup__button{\r\n width: 80%;\r\n margin-left: 10%;\r\n margin-bottom: 25px;\r\n height: 40px;\r\n border-radius: 5px;\r\n outline: none; \r\n}\r\n\r\n.Signup__username,.Signup__email,.Signup__password{\r\n border: 1px solid #bbb;\r\n padding: 0 0 0 10px;\r\n font-size: 14px;\r\n}\r\n\r\n.Signup__username:focus,.Signup__email:focus,.Signup__password:focus{\r\n border: 1px solid #3498db;\r\n}\r\n\r\n.Signup__button{\r\n background: #e74c3c;\r\n border:none;\r\n color: white;\r\n font-size: 18px;\r\n font-weight: 200;\r\n cursor: pointer;\r\n width: 83%;\r\n transition: box-shadow .4s ease;\r\n}\r\n\r\n.Signup__button:hover,.Signup__button:active{\r\n box-shadow: 1px 1px 5px #555;\r\n}\r\n\r\n.Signup__error{\r\n padding: 5px;\r\n text-align: center;\r\n border: 1px solid red;\r\n border-radius: 5px;\r\n color: red;\r\n width: 80%;\r\n margin-left: 10%;\r\n margin-bottom: -10px;\r\n}\r\n\r\n\r\n","body{\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: center;\r\n align-items: center;\r\n min-height: 100vh;\r\n margin: 0;\r\n background: #333;\r\n background-image: url('data:image/svg+xml,%3Csvg width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cpath d=\"M8 16c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm33.414-6l5.95-5.95L45.95.636 40 6.586 34.05.636 32.636 2.05 38.586 8l-5.95 5.95 1.414 1.414L40 9.414l5.95 5.95 1.414-1.414L41.414 8zM40 48c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zM9.414 40l5.95-5.95-1.414-1.414L8 38.586l-5.95-5.95L.636 34.05 6.586 40l-5.95 5.95 1.414 1.414L8 41.414l5.95 5.95 1.414-1.414L9.414 40z\" fill=\"%239C92AC\" fill-opacity=\"0.4\" fill-rule=\"evenodd\"/%3E%3C/svg%3E');\r\n font-family: 'Helvetica Neue', Arial, Sans-Serif;\r\n}\r\n\r\n.Login__wrap{\r\n max-width: 550px;\r\n min-width: 330px;\r\n background: #ecf0f1;\r\n border-radius: 5px;\r\n box-shadow: 3px 3px 10px #333;\r\n padding: 15px;\r\n}\r\n.Login__wrap h2{\r\n text-align: center;\r\n font-weight: 200;\r\n font-size: 2em;\r\n margin-top: 10px;\r\n color: #34495e;\r\n}\r\n.Login__link{\r\n text-align: center;\r\n color: #34495e;\r\n text-decoration: none;\r\n}\r\n.Login__link a{\r\n text-decoration: none;\r\n font-weight: 600;\r\n padding-left: 3px;\r\n}\r\n.Login__form{\r\n padding-top: 20px;\r\n \r\n}\r\n.Login__email,.Login__password,.Login__button{\r\n width: 80%;\r\n margin-left: 10%;\r\n margin-bottom: 25px;\r\n height: 40px;\r\n border-radius: 5px;\r\n outline: none;\r\n \r\n}\r\n.Login__email,.Login__password{\r\n border: 1px solid #bbb;\r\n padding: 0 0 0 10px;\r\n font-size: 14px;\r\n}\r\n.Login__email:focus,.Login__password:focus{\r\n border: 1px solid #3498db;\r\n}\r\n.Login__button{\r\n background: #e74c3c;\r\n border:none;\r\n color: white;\r\n font-size: 18px;\r\n font-weight: 200;\r\n cursor: pointer;\r\n transition: box-shadow .4s ease;\r\n width: 83%;\r\n}\r\n.Login__button:hover,.Login__button:active{\r\n box-shadow: 1px 1px 5px #555;\r\n}\r\n\r\n.Login__error{\r\n padding: 5px;\r\n text-align: center;\r\n border: 1px solid red;\r\n border-radius: 5px;\r\n color: red;\r\n width: 80%;\r\n margin-left: 10%;\r\n margin-bottom: -10px;\r\n}\r\n\r\n",".Footer{\r\n display: flex;\r\n flex-direction: column;\r\n background: #151414;\r\n color: #fff;\r\n margin-top: auto;\r\n}\r\n\r\n.Footer__information{\r\n height: 100px;\r\n border-bottom: 1px solid #373636;\r\n}\r\n.Footer__copyright{\r\n display: flex;\r\n justify-content: space-between;\r\n flex-wrap: wrap;\r\n align-items: center;\r\n background: #202020;\r\n padding: 20px 30px;\r\n}\r\n.Footer__copyright-text{\r\n font-size: 14px;\r\n}\r\n.Footer__copyright-text a{\r\n color: #ff5e14;\r\n text-decoration: none;\r\n padding-left: 3px;\r\n}\r\n.Footer__menu ul{\r\n display: flex;\r\n}\r\n.Footer__menu li{\r\n list-style: none;\r\n\r\n}\r\n.Footer__menu li > *{\r\n margin-right: 20px;\r\n}\r\n\r\n.Footer__menu li:hover a{\r\n color: #ff5e14;\r\n }\r\n .Footer__menu li a {\r\n font-size: 14px;\r\n color: #878787;\r\n text-decoration: none;\r\n }\r\n\r\n @media (max-width:440px){\r\n .Footer__copyright{\r\n flex-direction: column;\r\n }\r\n .Footer__menu ul{\r\n justify-content: center;\r\n padding-left: 20px;\r\n }\r\n }",".Dashboard{\r\n display: flex;\r\n flex-direction: column;\r\n min-height: 100vh;\r\n width: 100vw;\r\n color: white;\r\n}\r\n.Dashboard__header{\r\n padding: 5px 20px;\r\n background-color: #e74c3c;\r\n}\r\n.Dashboard__nav{\r\n display: flex;\r\n justify-content: space-between;\r\n}\r\n.Dashboard__img{\r\n width: 80px;\r\n height: 80px;\r\n border-radius: 50%;\r\n border: 1px solid #fff;\r\n background-color: #333;\r\n background-image: url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"49\" viewBox=\"0 0 28 49\"%3E%3Cg fill-rule=\"evenodd\"%3E%3Cg id=\"hexagons\" fill=\"%239C92AC\" fill-opacity=\"0.4\" fill-rule=\"nonzero\"%3E%3Cpath d=\"M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');\r\n}\r\n.Dashboard__img img{\r\n width: 100%;\r\n height: auto;\r\n border-radius: 50%;\r\n}\r\n\r\n.Dashboard__details{\r\n display: flex;\r\n align-items: flex-end;\r\n flex-direction: column;\r\n padding: 10px;\r\n}\r\n.Dashboard__username,.Dashboard__email{\r\n margin-top: 3px;\r\n}\r\n.Dashboard__title{\r\n text-align: center;\r\n font-size: 48px;\r\n margin-bottom: 0;\r\n}\r\n.Dashboard__list{\r\n list-style: none;\r\n}",".AddTaskForm{\r\n display: flex;\r\n justify-content: center;\r\n flex-wrap: wrap;\r\n margin-top: 10px;\r\n margin-bottom: 10px;\r\n}\r\n.AddTaskForm__input{\r\n background-color:#f7f1f1;\r\n border:none;\r\n font-size:medium;\r\n padding:0 1em;\r\n outline: none;\r\n}\r\n.AddTaskForm__btn{\r\n background-color:#8e0e00;\r\n border:none;\r\n outline: none;\r\n border:1px solid hsla(0,0%,100%,.3);\r\n color:#fff;\r\n cursor:pointer;\r\n font-weight:700;\r\n margin-left:5px;\r\n padding:0 1em;\r\n text-transform:uppercase;\r\n transition:background 0.2s ease-out;\r\n}\r\n.AddTaskForm__btn{\r\n background-color:#a01809\r\n}\r\n\r\n.AddTaskForm__btn,\r\n.AddTaskForm__input{\r\n font-family:Quicksand,sans-serif;\r\n padding: 1rem 1em;\r\n}",".EditTaskForm{\r\n display:flex;\r\n margin: 0 -3rem 4px;\r\n padding: 1rem 3rem;\r\n justify-content: space-between;\r\n align-items: center;\r\n background: rgba(255, 255, 255, 0.2);\r\n overflow: hidden;\r\n position: relative;\r\n transition: opacity 500ms ease-in-out;\r\n \r\n }\r\n .EditTaskForm__input{\r\n flex-grow:1;\r\n background-color:#f7f1f1;\r\n border:none;\r\n font-size:medium;\r\n outline:none;\r\n padding:0 1em;\r\n }\r\n .EditTaskForm__btn{\r\n background-color:#8e0e00;\r\n border:none;\r\n border:1px solid hsla(0,0%,100%,.3);\r\n color:#fff;\r\n cursor:pointer;\r\n font-weight:700;\r\n margin-left:5px;\r\n padding:0 1em;\r\n text-transform:uppercase;\r\n transition:background 0.2s ease-out;\r\n }\r\n .EditTaskForm__btn{\r\n background-color:#a01809;\r\n }\r\n \r\n .EditTaskForm__btn,\r\n .EditTaskForm__input{\r\n font-family:Quicksand,sans-serif;\r\n padding: 1rem 1em;\r\n }",".Task{\r\n display: flex;\r\n margin: 3px -3px 4px;\r\n padding: 1rem 3rem;\r\n justify-content: space-between;\r\n align-items: center;\r\n background: rgba(255, 255, 255, 0.2);\r\n overflow: hidden;\r\n position: relative;\r\n transition: opacity 500ms ease-in-out;\r\n}\r\n.Task__item{\r\n list-style: none;\r\n font-size: 1.25rem;\r\n}\r\n.Task__btns{\r\n flex-shrink: 0;\r\n padding-left: 0.7em;\r\n margin-left: auto;\r\n}\r\n.Task__btn{\r\n font-family:Quicksand,sans-serif;\r\n padding:0.7rem 1.3rem;\r\n border: none;\r\n font-size: 1em;\r\n margin: 0.4em;\r\n background-color:#8e0e00;\r\n -webkit-appearance: none;\r\n cursor: pointer;\r\n color: #fff;\r\n}",".TaskList{\r\n display: flex;\r\n justify-content: center;\r\n}\r\n.TaskList__lists{\r\n margin: 0;\r\n padding: 0;\r\n}"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /client/build/static/js/main.1b0e90fa.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | 3 | /** 4 | * @license React 5 | * react-dom.production.min.js 6 | * 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | /** 14 | * @license React 15 | * react-jsx-runtime.production.min.js 16 | * 17 | * Copyright (c) Facebook, Inc. and its affiliates. 18 | * 19 | * This source code is licensed under the MIT license found in the 20 | * LICENSE file in the root directory of this source tree. 21 | */ 22 | 23 | /** 24 | * @license React 25 | * react.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | /** 34 | * @license React 35 | * scheduler.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | 43 | /** 44 | * React Router DOM v6.3.0 45 | * 46 | * Copyright (c) Remix Software Inc. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE.md file in the root directory of this source tree. 50 | * 51 | * @license MIT 52 | */ 53 | 54 | /** 55 | * React Router v6.3.0 56 | * 57 | * Copyright (c) Remix Software Inc. 58 | * 59 | * This source code is licensed under the MIT license found in the 60 | * LICENSE.md file in the root directory of this source tree. 61 | * 62 | * @license MIT 63 | */ 64 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.2.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.1.0", 10 | "react-dom": "^18.1.0", 11 | "react-router-dom": "^6.3.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": " react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/node-react-task-manager/14ad231a294dbd4d2b61eab6d52caa98a114a91d/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/node-react-task-manager/14ad231a294dbd4d2b61eab6d52caa98a114a91d/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/node-react-task-manager/14ad231a294dbd4d2b61eab6d52caa98a114a91d/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import Signup from "./components/Signup"; 2 | import Login from "./components/Login"; 3 | import {Route, Routes} from 'react-router-dom' 4 | import Dashboard from "./components/Dashboard"; 5 | import { TaskContextProvider } from "./contexts/TaskContext"; 6 | 7 | function App() { 8 | return ( 9 |
10 | 11 | 12 | } /> 13 | } /> 14 | 16 | 17 | 18 | } /> 19 | } /> 20 | 21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /client/src/components/AddTaskForm.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import useFormInput from '../hooks/useFormInput'; 3 | import { createTask } from '../services/service'; 4 | import { TaskContext } from '../contexts/TaskContext'; 5 | import './css/AddTaskForm.css'; 6 | 7 | function AddTaskForm() { 8 | const {token,setUpdated} = useContext(TaskContext); 9 | const [task,setTask,resetTask] = useFormInput(''); 10 | 11 | const handleSubmit =(e)=>{ 12 | e.preventDefault(); 13 | 14 | if(token){ 15 | createTask(task,token) 16 | .then(()=>{ 17 | setUpdated(true); 18 | resetTask() 19 | }) 20 | } 21 | } 22 | 23 | return ( 24 |
25 | 26 | 27 |
28 | ) 29 | } 30 | 31 | export default AddTaskForm -------------------------------------------------------------------------------- /client/src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import Logout from "./Logout"; 4 | import Footer from "./Footer"; 5 | import'./css/Dashboard.css' 6 | import AddTaskForm from "./AddTaskForm"; 7 | import TaskList from "./TaskList"; 8 | import {getUser} from '../services/service'; 9 | import {TaskContext} from '../contexts/TaskContext' 10 | 11 | function Dashboard() { 12 | const {mounted,token} = useContext(TaskContext); 13 | const [username,setUsername] = useState(''); 14 | const [email,setEmail] = useState(''); 15 | const [userId,setUserId] = useState(''); 16 | let navigate = useNavigate(); 17 | 18 | useEffect(()=>{ 19 | mounted.current = true; 20 | if(!token){ 21 | return navigate('/login') 22 | } 23 | getUser(token) 24 | .then(data=> { 25 | if(mounted.current){ 26 | setUsername(data.name); 27 | setEmail(data.email); 28 | setUserId(data._id); 29 | } 30 | }) 31 | return ()=> { 32 | mounted.current = false; 33 | 34 | } 35 | 36 | },[token,mounted,navigate]) 37 | 38 | return ( 39 |
40 | 41 |
42 | 53 |
54 | 55 |
56 |

Tasks

57 | 58 | 59 |
60 | 61 |
63 | ) 64 | } 65 | 66 | export default Dashboard -------------------------------------------------------------------------------- /client/src/components/EditTaskForm.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import useFormInput from '../hooks/useFormInput'; 3 | import { updateTask } from '../services/service'; 4 | import {TaskContext} from '../contexts/TaskContext' 5 | import './css/EditTaskForm.css' 6 | 7 | function EditTaskForm({task,edit}) { 8 | const {token,setUpdated} = useContext(TaskContext); 9 | const [newTask,editTask,resetTask] = useFormInput(task.description); 10 | 11 | const handleSubmit =(e)=>{ 12 | e.preventDefault(); 13 | updateTask(task._id,newTask,token) 14 | .then(()=>{ 15 | if(newTask !== task.description){ 16 | setUpdated(true); 17 | } 18 | resetTask(); 19 | edit(); 20 | }) 21 | } 22 | return ( 23 |
24 | 29 | 32 |
33 | ) 34 | } 35 | 36 | export default EditTaskForm -------------------------------------------------------------------------------- /client/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './css/Footer.css' 3 | 4 | function Footer() { 5 | return ( 6 | 25 | ) 26 | } 27 | 28 | export default Footer -------------------------------------------------------------------------------- /client/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React,{useContext, useState} from 'react' 2 | import { useNavigate,Link } from 'react-router-dom'; 3 | import { loginUser } from '../services/service'; 4 | import {TaskContext} from '../contexts/TaskContext' 5 | import './css/Login.css' 6 | 7 | function Login() { 8 | const {setToken} = useContext(TaskContext); 9 | const [password, setPassword] = useState(''); 10 | const [email,setEmail] =useState(''); 11 | const [error,setError] =useState(null); 12 | let navigate = useNavigate(); 13 | 14 | 15 | const handleSubmit = (e) => { 16 | e.preventDefault() 17 | 18 | loginUser(email,password) 19 | .then(data =>{ 20 | if(data.error){ 21 | setError(data.error) 22 | setTimeout(()=>{ 23 | setError(null) 24 | },5000) 25 | }else{ 26 | setToken(data.token); 27 | setError(null); 28 | setPassword(''); 29 | setEmail(''); 30 | navigate('/dashboard',{replace:true}) 31 | } 32 | 33 | }) 34 | .catch(err=>console.log(err)) 35 | } 36 | 37 | return ( 38 |
39 |
40 |

Login

41 | 42 | {error &&

{error}

} 43 |
44 | setEmail(e.target.value )} 49 | required 50 | placeholder='Email' 51 | className='Login__email' 52 | /> 53 | 54 | setPassword( e.target.value )} 59 | required 60 | placeholder='Password' 61 | className='Login__password' 62 | /> 63 | 64 | 65 |
66 |
67 |

Don't have an account? 68 | 69 | Sign up 70 | 71 |

72 |
73 | 74 |
75 |
76 | ) 77 | } 78 | 79 | export default Login -------------------------------------------------------------------------------- /client/src/components/Logout.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import {TaskContext} from '../contexts/TaskContext'; 3 | import {logoutUser} from '../services/service'; 4 | import {useNavigate} from 'react-router-dom'; 5 | 6 | function Logout() { 7 | const {token,setToken} = useContext(TaskContext); 8 | let navigate = useNavigate(); 9 | 10 | const handleLogout =()=>{ 11 | if(token){ 12 | logoutUser(token); 13 | setToken(''); 14 | navigate('/login'); 15 | } 16 | } 17 | 18 | return ( 19 | 22 | ) 23 | } 24 | 25 | export default Logout 26 | -------------------------------------------------------------------------------- /client/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | 3 | function Navbar() { 4 | return ( 5 | 13 | ) 14 | } 15 | 16 | export default Navbar -------------------------------------------------------------------------------- /client/src/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React,{useState,useContext} from 'react' 2 | import { useNavigate,Link } from 'react-router-dom'; 3 | import {TaskContext} from '../contexts/TaskContext' 4 | import {createUser} from '../services/service'; 5 | import './css/Signup.css'; 6 | 7 | function Signup() { 8 | const {setToken} = useContext(TaskContext); 9 | const [name, setName] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | const [email,setEmail] =useState(''); 12 | const [error,setError] =useState(null); 13 | let navigate = useNavigate(); 14 | 15 | const handleSubmit = async (e) => { 16 | e.preventDefault() 17 | createUser(email,password,name) 18 | .then(data=>{ 19 | 20 | if(data.error?.message){ 21 | setError(data.error.message) 22 | setTimeout(() => { 23 | setError(null) 24 | }, 5000); 25 | }else if(data.error?.index === 0){ 26 | setError('Email already exists!') 27 | setTimeout(() => { 28 | setError(null) 29 | }, 5000); 30 | }else{ 31 | setToken(data.token) 32 | setError(null); 33 | setName(''); 34 | setPassword(''); 35 | setEmail(''); 36 | navigate('/dashboard',{replace:true}) 37 | } 38 | }) 39 | .catch(err=>{ 40 | if(err){ 41 | setError('Network Error!') 42 | setTimeout(() => { 43 | setError(null) 44 | }, 5000); 45 | } 46 | }) 47 | } 48 | 49 | return ( 50 |
51 |
52 |

Sign Up

53 | 54 | {error &&

{error}

} 55 | 56 |
57 | setName(e.target.value )} 62 | placeholder='Username' 63 | className='Signup__username' 64 | required 65 | /> 66 | 67 | setEmail(e.target.value )} 72 | placeholder='Email' 73 | className='Signup__email' 74 | required 75 | /> 76 | 77 | setPassword( e.target.value )} 82 | placeholder='Password' 83 | className='Signup__password' 84 | required 85 | /> 86 | 87 | 88 |
89 |
90 |

Already have an account? 91 | 92 | Login 93 | 94 |

95 |
96 |
97 |
98 | ) 99 | } 100 | 101 | export default Signup -------------------------------------------------------------------------------- /client/src/components/Task.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import EditTaskForm from './EditTaskForm' 3 | import useToggle from '../hooks/useToggle'; 4 | import { deleteTask } from '../services/service'; 5 | import {TaskContext} from '../contexts/TaskContext' 6 | import './css/Task.css' 7 | 8 | 9 | function Task({task}) { 10 | const {token,setUpdated} = useContext(TaskContext) 11 | const [isEdit,setIsEdit] = useToggle(false); 12 | 13 | const handleDelete =()=>{ 14 | deleteTask(task._id,token); 15 | setUpdated(true); 16 | } 17 | 18 | return ( 19 | 20 | isEdit ? : 21 |
22 |
  • {task.description}
  • 23 | 24 |
    25 | 26 | 27 |
    28 |
    29 | 30 | ) 31 | } 32 | 33 | export default Task -------------------------------------------------------------------------------- /client/src/components/TaskList.js: -------------------------------------------------------------------------------- 1 | import React,{useContext, useEffect, useState} from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { getTasks } from '../services/service'; 4 | import Task from './Task' 5 | import './css/TaskList.css' 6 | import { TaskContext } from '../contexts/TaskContext'; 7 | 8 | function TaskList() { 9 | const {token,mounted,updated,setUpdated} = useContext(TaskContext) 10 | const [tasks,setTasks] = useState([]); 11 | let navigate = useNavigate(); 12 | 13 | useEffect(()=>{ 14 | if(!token){ 15 | return navigate('/login') 16 | } 17 | getTasks(token) 18 | .then(data=>{ 19 | if(mounted.current || updated){ 20 | setTasks(data); 21 | setUpdated(false) 22 | } 23 | }) 24 | },[mounted,token,updated,setUpdated,navigate]) 25 | return ( 26 |
    27 | 31 |
    32 | ) 33 | } 34 | 35 | export default TaskList -------------------------------------------------------------------------------- /client/src/components/css/AddTaskForm.css: -------------------------------------------------------------------------------- 1 | .AddTaskForm{ 2 | display: flex; 3 | justify-content: center; 4 | flex-wrap: wrap; 5 | margin-top: 10px; 6 | margin-bottom: 10px; 7 | } 8 | .AddTaskForm__input{ 9 | background-color:#f7f1f1; 10 | border:none; 11 | font-size:medium; 12 | padding:0 1em; 13 | outline: none; 14 | } 15 | .AddTaskForm__btn{ 16 | background-color:#8e0e00; 17 | border:none; 18 | outline: none; 19 | border:1px solid hsla(0,0%,100%,.3); 20 | color:#fff; 21 | cursor:pointer; 22 | font-weight:700; 23 | margin-left:5px; 24 | padding:0 1em; 25 | text-transform:uppercase; 26 | transition:background 0.2s ease-out; 27 | } 28 | .AddTaskForm__btn{ 29 | background-color:#a01809 30 | } 31 | 32 | .AddTaskForm__btn, 33 | .AddTaskForm__input{ 34 | font-family:Quicksand,sans-serif; 35 | padding: 1rem 1em; 36 | } -------------------------------------------------------------------------------- /client/src/components/css/Dashboard.css: -------------------------------------------------------------------------------- 1 | .Dashboard{ 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | width: 100vw; 6 | color: white; 7 | } 8 | .Dashboard__header{ 9 | padding: 5px 20px; 10 | background-color: #e74c3c; 11 | } 12 | .Dashboard__nav{ 13 | display: flex; 14 | justify-content: space-between; 15 | } 16 | .Dashboard__img{ 17 | width: 80px; 18 | height: 80px; 19 | border-radius: 50%; 20 | border: 1px solid #fff; 21 | background-color: #333; 22 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="28" height="49" viewBox="0 0 28 49"%3E%3Cg fill-rule="evenodd"%3E%3Cg id="hexagons" fill="%239C92AC" fill-opacity="0.4" fill-rule="nonzero"%3E%3Cpath d="M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E'); 23 | } 24 | .Dashboard__img img{ 25 | width: 100%; 26 | height: auto; 27 | border-radius: 50%; 28 | } 29 | 30 | .Dashboard__details{ 31 | display: flex; 32 | align-items: flex-end; 33 | flex-direction: column; 34 | padding: 10px; 35 | } 36 | .Dashboard__username,.Dashboard__email{ 37 | margin-top: 3px; 38 | } 39 | .Dashboard__title{ 40 | text-align: center; 41 | font-size: 48px; 42 | margin-bottom: 0; 43 | } 44 | .Dashboard__list{ 45 | list-style: none; 46 | } -------------------------------------------------------------------------------- /client/src/components/css/EditTaskForm.css: -------------------------------------------------------------------------------- 1 | .EditTaskForm{ 2 | display:flex; 3 | margin: 0 -3rem 4px; 4 | padding: 1rem 3rem; 5 | justify-content: space-between; 6 | align-items: center; 7 | background: rgba(255, 255, 255, 0.2); 8 | overflow: hidden; 9 | position: relative; 10 | transition: opacity 500ms ease-in-out; 11 | 12 | } 13 | .EditTaskForm__input{ 14 | flex-grow:1; 15 | background-color:#f7f1f1; 16 | border:none; 17 | font-size:medium; 18 | outline:none; 19 | padding:0 1em; 20 | } 21 | .EditTaskForm__btn{ 22 | background-color:#8e0e00; 23 | border:none; 24 | border:1px solid hsla(0,0%,100%,.3); 25 | color:#fff; 26 | cursor:pointer; 27 | font-weight:700; 28 | margin-left:5px; 29 | padding:0 1em; 30 | text-transform:uppercase; 31 | transition:background 0.2s ease-out; 32 | } 33 | .EditTaskForm__btn{ 34 | background-color:#a01809; 35 | } 36 | 37 | .EditTaskForm__btn, 38 | .EditTaskForm__input{ 39 | font-family:Quicksand,sans-serif; 40 | padding: 1rem 1em; 41 | } -------------------------------------------------------------------------------- /client/src/components/css/Footer.css: -------------------------------------------------------------------------------- 1 | .Footer{ 2 | display: flex; 3 | flex-direction: column; 4 | background: #151414; 5 | color: #fff; 6 | margin-top: auto; 7 | } 8 | 9 | .Footer__information{ 10 | height: 100px; 11 | border-bottom: 1px solid #373636; 12 | } 13 | .Footer__copyright{ 14 | display: flex; 15 | justify-content: space-between; 16 | flex-wrap: wrap; 17 | align-items: center; 18 | background: #202020; 19 | padding: 20px 30px; 20 | } 21 | .Footer__copyright-text{ 22 | font-size: 14px; 23 | } 24 | .Footer__copyright-text a{ 25 | color: #ff5e14; 26 | text-decoration: none; 27 | padding-left: 3px; 28 | } 29 | .Footer__menu ul{ 30 | display: flex; 31 | } 32 | .Footer__menu li{ 33 | list-style: none; 34 | 35 | } 36 | .Footer__menu li > *{ 37 | margin-right: 20px; 38 | } 39 | 40 | .Footer__menu li:hover a{ 41 | color: #ff5e14; 42 | } 43 | .Footer__menu li a { 44 | font-size: 14px; 45 | color: #878787; 46 | text-decoration: none; 47 | } 48 | 49 | @media (max-width:440px){ 50 | .Footer__copyright{ 51 | flex-direction: column; 52 | } 53 | .Footer__menu ul{ 54 | justify-content: center; 55 | padding-left: 20px; 56 | } 57 | } -------------------------------------------------------------------------------- /client/src/components/css/Login.css: -------------------------------------------------------------------------------- 1 | body{ 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | min-height: 100vh; 7 | margin: 0; 8 | background: #333; 9 | background-image: url('data:image/svg+xml,%3Csvg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M8 16c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm33.414-6l5.95-5.95L45.95.636 40 6.586 34.05.636 32.636 2.05 38.586 8l-5.95 5.95 1.414 1.414L40 9.414l5.95 5.95 1.414-1.414L41.414 8zM40 48c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zM9.414 40l5.95-5.95-1.414-1.414L8 38.586l-5.95-5.95L.636 34.05 6.586 40l-5.95 5.95 1.414 1.414L8 41.414l5.95 5.95 1.414-1.414L9.414 40z" fill="%239C92AC" fill-opacity="0.4" fill-rule="evenodd"/%3E%3C/svg%3E'); 10 | font-family: 'Helvetica Neue', Arial, Sans-Serif; 11 | } 12 | 13 | .Login__wrap{ 14 | max-width: 550px; 15 | min-width: 330px; 16 | background: #ecf0f1; 17 | border-radius: 5px; 18 | box-shadow: 3px 3px 10px #333; 19 | padding: 15px; 20 | } 21 | .Login__wrap h2{ 22 | text-align: center; 23 | font-weight: 200; 24 | font-size: 2em; 25 | margin-top: 10px; 26 | color: #34495e; 27 | } 28 | .Login__link{ 29 | text-align: center; 30 | color: #34495e; 31 | text-decoration: none; 32 | } 33 | .Login__link a{ 34 | text-decoration: none; 35 | font-weight: 600; 36 | padding-left: 3px; 37 | } 38 | .Login__form{ 39 | padding-top: 20px; 40 | 41 | } 42 | .Login__email,.Login__password,.Login__button{ 43 | width: 80%; 44 | margin-left: 10%; 45 | margin-bottom: 25px; 46 | height: 40px; 47 | border-radius: 5px; 48 | outline: none; 49 | 50 | } 51 | .Login__email,.Login__password{ 52 | border: 1px solid #bbb; 53 | padding: 0 0 0 10px; 54 | font-size: 14px; 55 | } 56 | .Login__email:focus,.Login__password:focus{ 57 | border: 1px solid #3498db; 58 | } 59 | .Login__button{ 60 | background: #e74c3c; 61 | border:none; 62 | color: white; 63 | font-size: 18px; 64 | font-weight: 200; 65 | cursor: pointer; 66 | transition: box-shadow .4s ease; 67 | width: 83%; 68 | } 69 | .Login__button:hover,.Login__button:active{ 70 | box-shadow: 1px 1px 5px #555; 71 | } 72 | 73 | .Login__error{ 74 | padding: 5px; 75 | text-align: center; 76 | border: 1px solid red; 77 | border-radius: 5px; 78 | color: red; 79 | width: 80%; 80 | margin-left: 10%; 81 | margin-bottom: -10px; 82 | } 83 | 84 | -------------------------------------------------------------------------------- /client/src/components/css/Signup.css: -------------------------------------------------------------------------------- 1 | 2 | .Signup__wrapper{ 3 | background: #ecf0f1; 4 | max-width: 550px; 5 | min-width: 330px; 6 | border-radius: 5px; 7 | box-shadow: 3px 3px 10px #333; 8 | padding: 15px; 9 | } 10 | .Signup__wrapper h2{ 11 | text-align: center; 12 | font-weight: 200; 13 | font-size: 2em; 14 | margin-top: 10px; 15 | color: #34495e; 16 | } 17 | .Signup__link{ 18 | text-align: center; 19 | color: #34495e; 20 | text-decoration: none; 21 | } 22 | .Signup__link a{ 23 | text-decoration: none; 24 | font-weight: 600; 25 | padding-left: 3px; 26 | } 27 | .Signup__form{ 28 | padding-top: 20px; 29 | } 30 | .Signup__username,.Signup__email,.Signup__password,.Signup__button{ 31 | width: 80%; 32 | margin-left: 10%; 33 | margin-bottom: 25px; 34 | height: 40px; 35 | border-radius: 5px; 36 | outline: none; 37 | } 38 | 39 | .Signup__username,.Signup__email,.Signup__password{ 40 | border: 1px solid #bbb; 41 | padding: 0 0 0 10px; 42 | font-size: 14px; 43 | } 44 | 45 | .Signup__username:focus,.Signup__email:focus,.Signup__password:focus{ 46 | border: 1px solid #3498db; 47 | } 48 | 49 | .Signup__button{ 50 | background: #e74c3c; 51 | border:none; 52 | color: white; 53 | font-size: 18px; 54 | font-weight: 200; 55 | cursor: pointer; 56 | width: 83%; 57 | transition: box-shadow .4s ease; 58 | } 59 | 60 | .Signup__button:hover,.Signup__button:active{ 61 | box-shadow: 1px 1px 5px #555; 62 | } 63 | 64 | .Signup__error{ 65 | padding: 5px; 66 | text-align: center; 67 | border: 1px solid red; 68 | border-radius: 5px; 69 | color: red; 70 | width: 80%; 71 | margin-left: 10%; 72 | margin-bottom: -10px; 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /client/src/components/css/Task.css: -------------------------------------------------------------------------------- 1 | .Task{ 2 | display: flex; 3 | margin: 3px -3px 4px; 4 | padding: 1rem 3rem; 5 | justify-content: space-between; 6 | align-items: center; 7 | background: rgba(255, 255, 255, 0.2); 8 | overflow: hidden; 9 | position: relative; 10 | transition: opacity 500ms ease-in-out; 11 | } 12 | .Task__item{ 13 | list-style: none; 14 | font-size: 1.25rem; 15 | } 16 | .Task__btns{ 17 | flex-shrink: 0; 18 | padding-left: 0.7em; 19 | margin-left: auto; 20 | } 21 | .Task__btn{ 22 | font-family:Quicksand,sans-serif; 23 | padding:0.7rem 1.3rem; 24 | border: none; 25 | font-size: 1em; 26 | margin: 0.4em; 27 | background-color:#8e0e00; 28 | -webkit-appearance: none; 29 | cursor: pointer; 30 | color: #fff; 31 | } -------------------------------------------------------------------------------- /client/src/components/css/TaskList.css: -------------------------------------------------------------------------------- 1 | .TaskList{ 2 | display: flex; 3 | justify-content: center; 4 | } 5 | .TaskList__lists{ 6 | margin: 0; 7 | padding: 0; 8 | } -------------------------------------------------------------------------------- /client/src/contexts/TaskContext.js: -------------------------------------------------------------------------------- 1 | import React,{createContext,useRef,useState} from "react"; 2 | 3 | export const TaskContext = createContext(); 4 | 5 | export function TaskContextProvider(props){ 6 | const getToken =()=>{ 7 | const tokenString = localStorage.getItem('token'); 8 | const userToken = JSON.parse(tokenString || null); 9 | return userToken; 10 | } 11 | 12 | const [token,setToken] = useState(getToken()); 13 | const [msg,setMsg] = useState(''); 14 | const mounted = useRef(true); 15 | const [updated,setUpdated] = useState(false) 16 | 17 | const saveToken =(userToken)=>{ 18 | let token; 19 | if(!userToken.length){ 20 | token = '' 21 | }else{ 22 | const b = 'Bearer '; 23 | token = b.concat(userToken) 24 | } 25 | return setToken(localStorage.setItem('token', JSON.stringify(token) 26 | )); 27 | } 28 | return ( 29 | 30 | {props.children} 31 | 32 | ) 33 | 34 | } -------------------------------------------------------------------------------- /client/src/hooks/useFormInput.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react' 2 | 3 | function useFormInput(initialVal) { 4 | const [state,setState] = useState(initialVal); 5 | const handleChange = e =>{ 6 | setState(e.target.value); 7 | } 8 | const reset =()=>{ 9 | setState('') 10 | } 11 | return [state,handleChange,reset] 12 | } 13 | 14 | export default useFormInput; -------------------------------------------------------------------------------- /client/src/hooks/useToggle.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react' 2 | 3 | function useToggle(initialVal=false) { 4 | const [state,setState] = useState(initialVal); 5 | const toggle =(e)=>{ 6 | setState(!state); 7 | } 8 | return [state,toggle] 9 | } 10 | 11 | export default useToggle -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import {BrowserRouter} from 'react-router-dom' 5 | import { TaskContextProvider } from "./contexts/TaskContext"; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | -------------------------------------------------------------------------------- /client/src/services/service.js: -------------------------------------------------------------------------------- 1 | // SIGNUP A NEW USER 2 | export const createUser = async (email,password,name)=>{ 3 | const response = await fetch('/users', { 4 | method: 'POST', 5 | body: JSON.stringify({email,password,name}), 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | } 9 | }); 10 | 11 | return response.json(); 12 | 13 | } 14 | 15 | 16 | // LOGIN A USER 17 | export const loginUser = async (email,password)=>{ 18 | const response = await fetch('/users/login', { 19 | method: 'POST', 20 | body: JSON.stringify({email,password}), 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | } 24 | }); 25 | return response.json(); 26 | } 27 | // FETCH USER DETAILS AFTER LOGIN 28 | export const getUser = async (token)=>{ 29 | const user = await fetch('/users/me',{ 30 | method:'GET', 31 | headers:{ 32 | 'Content-Type': 'application/json', 33 | 'Authorization':token 34 | } 35 | }); 36 | return user.json(); 37 | } 38 | // GET USER TASKS 39 | export const getTasks = async (token)=>{ 40 | const tasks = await fetch('/tasks',{ 41 | method:'GET', 42 | headers:{ 43 | 'Content-Type': 'application/json', 44 | 'Authorization':token 45 | } 46 | }) 47 | 48 | return tasks.json(); 49 | } 50 | 51 | 52 | // CREATE NEW TASKS 53 | export const createTask =async (newTask,token)=>{ 54 | await fetch('/tasks',{ 55 | method:'POST', 56 | body:JSON.stringify({description:newTask}), 57 | headers:{ 58 | 'Authorization': token, 59 | 'Content-Type':'application/json' 60 | } 61 | }); 62 | 63 | } 64 | 65 | // UPDATE AN EXISTING TASK 66 | export const updateTask =async(id,newTask,token)=>{ 67 | await fetch(`/tasks/${id}`,{ 68 | method:'PATCH', 69 | body:JSON.stringify({description:newTask}), 70 | headers:{ 71 | 'Content-Type':'application/json', 72 | 'Authorization':token 73 | } 74 | }); 75 | } 76 | 77 | // DELETE A TASK 78 | export const deleteTask = async(id,token)=>{ 79 | await fetch(`/tasks/${id}`,{ 80 | method:'DELETE', 81 | headers:{ 82 | 'Content-Type':'application/json', 83 | 'Authorization':token 84 | } 85 | }) 86 | } 87 | 88 | // LOGOUT A USER 89 | export const logoutUser = async (token)=>{ 90 | await fetch('/users/logoutAll', { 91 | method:'POST', 92 | headers:{ 93 | 'Content-Type': 'application/json', 94 | 'Authorization': token, 95 | } 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server/index.js", 8 | "dev": "nodemon server/index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@sendgrid/mail": "^7.7.0", 15 | "bcrypt": "^5.0.1", 16 | "dotenv": "^16.0.1", 17 | "express": "^4.18.1", 18 | "jsonwebtoken": "^8.5.1", 19 | "mongoose": "^6.4.0", 20 | "multer": "^1.4.5-lts.1", 21 | "sharp": "^0.30.7", 22 | "validator": "^13.7.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/db/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const dotenv = require('dotenv'); 3 | dotenv.config({path:'config/.env'}) 4 | // mongodb url 5 | const url = process.env.MONGODB_URL; 6 | 7 | mongoose.connect(url) 8 | .then( 9 | (res)=>{console.log('connected to the database')}, 10 | err=>{console.log('could not connect to the database')} 11 | ) -------------------------------------------------------------------------------- /server/emails/email.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config({path:'config/.env'}) 3 | const sgMail = require('@sendgrid/mail'); 4 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 5 | 6 | const sendWelcomeEmail = (email,name)=>{ 7 | sgMail.send({ 8 | to:email, 9 | from:'dev.johnmwendwa@gmail.com', 10 | subject:'Welcome to Task App', 11 | html:` 12 | 13 | 14 | 15 | 16 | Email 17 | 57 | 58 | 59 |
    60 | 61 |
    62 |
    63 |

    Welcome!

    64 |

    Hey ${name}

    65 |

    I'm glad to see you're using Task App to plan your everyday tasks. Please feel free to recommend any feature improvements that you may like to see in the app.

    66 |

    For any feedback, just reply to this email - I'll reply to you as soon as possible.

    67 |
    68 | 72 | 76 | 77 | 78 | 79 | ` 80 | }) 81 | } 82 | 83 | const sendCancellationEmail = (email,name)=>{ 84 | sgMail.send({ 85 | to:email, 86 | from:'dev.johnmwendwa@gmail.com', 87 | subject:'We regret to see you leave', 88 | html:`

    Hey ${name}

    89 |

    We regret to see you leave. We hope to see you back soon.

    90 |

    Kindly suggest any feature improvement that you'd like to see in the app

    91 | ` 92 | }) 93 | } 94 | 95 | module.exports = { 96 | sendWelcomeEmail, 97 | sendCancellationEmail 98 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Connect to database 2 | require('./db/mongoose') 3 | 4 | const path = require('path'); 5 | const express = require('express'); 6 | const userRouter = require('./routers/userRouter') 7 | const taskRouter = require('./routers/taskRouter') 8 | 9 | // Initialize app 10 | const app = express(); 11 | 12 | // Environment port 13 | const port =process.env.PORT 14 | 15 | // set path to react build folder 16 | const reactAssets = path.join(__dirname,'../client/build'); 17 | const indexPage = path.join(__dirname,'../client/build','index.html'); 18 | 19 | //set express server to serve react static assets 20 | app.use(express.static(reactAssets)); 21 | 22 | app.use(express.json()) 23 | app.use(userRouter) 24 | app.use(taskRouter) 25 | 26 | app.get('/*',(req,res)=>{ 27 | res.sendFile(indexPage) 28 | }) 29 | 30 | //listen for connection 31 | app.listen(port,()=>console.log(`Server started on port ${port}`)) -------------------------------------------------------------------------------- /server/middleware/authToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('../models/userModel') 3 | const secret = process.env.CONSUMER_SECRET; 4 | 5 | const auth = async (req,res,next)=>{ 6 | try{ 7 | const token = req.header('Authorization').replace('Bearer ',''); 8 | const decoded = jwt.verify(token,secret); 9 | const user = await User.findOne({_id:decoded._id,'tokens.token':token}) 10 | 11 | if(!user){ 12 | throw new Error() 13 | } 14 | 15 | req.token = token; 16 | req.user = user; 17 | next() 18 | }catch(e){ 19 | res.status(401).send('Please authenticate!') 20 | } 21 | } 22 | 23 | module.exports= auth; 24 | -------------------------------------------------------------------------------- /server/models/taskModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const taskSchema = new mongoose.Schema({ 4 | description:{ 5 | type:String, 6 | required:true, 7 | trim:true, 8 | }, 9 | completed:{ 10 | type:Boolean, 11 | default:false 12 | }, 13 | author:{ 14 | type:mongoose.Schema.Types.ObjectId, 15 | required:true, 16 | ref:'User' 17 | } 18 | },{ 19 | timestamps:true 20 | }); 21 | 22 | const Task = mongoose.model('Task',taskSchema); 23 | 24 | module.exports = Task; -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator') 3 | const bcrypt = require('bcrypt') 4 | const jwt = require('jsonwebtoken'); 5 | const Task = require('./taskModel'); 6 | const secret = process.env.CONSUMER_SECRET 7 | 8 | const userSchema= new mongoose.Schema({ 9 | name:{ 10 | type:String, 11 | required:true, 12 | trim:true 13 | }, 14 | email:{ 15 | type:String, 16 | required:true, 17 | lowercase:true, 18 | unique:true, 19 | trim:true, 20 | validate(value){ 21 | if(!validator.isEmail(value)){ 22 | throw new Error('Invalid email') 23 | } 24 | } 25 | }, 26 | password:{ 27 | type:String, 28 | required:true, 29 | minlength:7, 30 | }, 31 | tokens:[{ 32 | token:{ 33 | type:String, 34 | required:true 35 | } 36 | }], 37 | avatar:{type:Buffer}, 38 | }, 39 | { 40 | timestamps:true 41 | }) 42 | 43 | // Create a vitual field for storing tasks 44 | userSchema.virtual('tasks',{ 45 | ref:'Task', 46 | localField:'_id', 47 | foreignField:'author' 48 | }) 49 | 50 | 51 | // Hide sensitive data 52 | userSchema.methods.toJSON = function (){ 53 | const user = this; 54 | const userObject = user.toObject(); 55 | 56 | delete userObject.password; 57 | delete userObject.tokens; 58 | delete userObject.avatar; 59 | 60 | return userObject 61 | } 62 | 63 | 64 | // function to generate authentication tokens 65 | userSchema.methods.generateAuthToken = async function (){ 66 | const user = this; 67 | const token = jwt.sign({_id:user._id},secret); 68 | user.tokens = user.tokens.concat({token}) 69 | await user.save() 70 | return token; 71 | } 72 | 73 | // Custom function to log in users 74 | userSchema.statics.findByCredentials = async (email,password)=>{ 75 | const user = await User.findOne({email}); 76 | if(!user){ 77 | throw new Error("Invalid credentials!") 78 | } 79 | 80 | // Check if stored password and the one provided match 81 | const isMatch = await bcrypt.compare(password,user.password); 82 | 83 | if(!isMatch){ 84 | throw new Error("Wrong password") 85 | } 86 | 87 | return user 88 | } 89 | 90 | 91 | userSchema.pre('save', async function (next){ 92 | const user = this; 93 | // Hash password only when creating or updating it 94 | if(user.isModified('password')){ 95 | user.password = await bcrypt.hash(user.password,8) 96 | } 97 | // call the next middleware after hashing is complete 98 | next() 99 | }) 100 | 101 | // DELETE all user tasks when user deletes their account 102 | userSchema.pre('remove', async function (next){ 103 | const user = this; 104 | await Task.deleteMany({author:user._id}) 105 | next() 106 | }) 107 | 108 | const User = mongoose.model('User',userSchema); 109 | 110 | module.exports= User; -------------------------------------------------------------------------------- /server/routers/taskRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const auth = require('../middleware/authToken'); 3 | const Task = require('../models/taskModel'); 4 | const router = new express.Router(); 5 | 6 | //ROUTES 7 | 8 | // CREATE a task 9 | router.post('/tasks',auth,async (req,res)=>{ 10 | try{ 11 | const task = new Task({ 12 | ...req.body, 13 | author:req.user._id 14 | }) 15 | if(!task){ 16 | throw new Error("You cannot create an empty task") 17 | } 18 | await task.save(); 19 | res.status(201).send(task) 20 | }catch(e){ 21 | res.status(400).send({error:e.message}) 22 | } 23 | }) 24 | 25 | // GET all tasks 26 | router.get('/tasks', auth, async (req,res)=>{ 27 | // ?completed=true 28 | const match = {}; 29 | const sort = {} 30 | 31 | if(req.query.completed){ 32 | match.completed = req.query.completed === 'true' 33 | } 34 | 35 | // ?sortBy=createdAt_desc || ?sortBy=createdAt_asc 36 | if(req.query.sortBy){ 37 | const parts = req.query.sortBy.split('_'); 38 | sort[parts[0]] = parts[1] === 'desc' ? -1 : 1; 39 | } 40 | 41 | try{ 42 | const tasks = await Task.find({author:req.user._id,...match},null,{ 43 | limit:parseInt(req.query.limit), 44 | skip:parseInt(req.query.skip), 45 | sort 46 | }); 47 | 48 | if(tasks.length === 0){ 49 | return res.send([]) 50 | } 51 | res.send(tasks) 52 | }catch(e){ 53 | res.status(500).send({error:e.message}) 54 | } 55 | }) 56 | 57 | // GET a single task 58 | router.get('/tasks/:id',auth,async (req,res)=>{ 59 | const _id = req.params.id 60 | try{ 61 | const task = await Task.findOne({_id,author:req.user._id}); 62 | 63 | if(!task){ 64 | return res.status(404).send("Task doesn't exist") 65 | } 66 | res.send(task) 67 | }catch(e){ 68 | res.status(404).send("Task doesn't exist") 69 | } 70 | }) 71 | 72 | // UPDATE a task 73 | router.patch('/tasks/:id', auth, async (req,res)=>{ 74 | const updates = Object.keys(req.body); 75 | try{ 76 | const task = await Task.findOne({_id:req.params.id,author:req.user._id}); 77 | 78 | if(!task){ 79 | return res.status(404).send("Task doesn't exist!") 80 | } 81 | updates.forEach(update=>task[update] = req.body[update]) 82 | await task.save() 83 | res.send(task) 84 | }catch(e){ 85 | res.status(404).send() 86 | } 87 | }) 88 | 89 | // DELETE a single task 90 | router.delete('/tasks/:id', auth, async (req,res)=>{ 91 | try{ 92 | const task = await Task.findOneAndDelete({_id:req.params.id,author:req.user._id}); 93 | 94 | if(!task){ 95 | return res.status(404).send("Task doesn't exist!") 96 | } 97 | 98 | res.send('Task Deleted successfully') 99 | }catch(e){ 100 | res.status(404).send("Task doesn't exist!") 101 | } 102 | }) 103 | 104 | // DELETE all tasks 105 | router.delete('/tasks', auth, async (req,res)=>{ 106 | try{ 107 | const tasks = await Task.find({author:req.user._id}); 108 | if(tasks.length === 0){ 109 | return res.status(404).send("You currently do not have any tasks") 110 | } 111 | await Task.deleteMany(); 112 | res.send('All tasks have been removed') 113 | }catch(e){ 114 | res.status(500).send(e.message) 115 | } 116 | }) 117 | 118 | module.exports= router; -------------------------------------------------------------------------------- /server/routers/userRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const User = require('../models/userModel'); 3 | const auth = require('../middleware/authToken'); 4 | const {sendWelcomeEmail,sendCancellationEmail} = require('../emails/email'); 5 | const router = new express.Router(); 6 | const multer = require('multer'); 7 | const sharp = require('sharp'); 8 | 9 | // Setup how Images will be uploaded 10 | const upload = multer({ 11 | // limit image files to 1MB size 12 | limits:{ 13 | fileSize:1000000 14 | }, 15 | fileFilter(req,file,cb){ 16 | if(!file.originalname.match(/\.(jpg|jpeg|png)$/)){ 17 | return cb(new Error('Please upload an image of type jpg,jpeg or png')) 18 | } 19 | cb(null,true) 20 | } 21 | }) 22 | 23 | 24 | 25 | // ROUTES 26 | 27 | // CREATE a new user 28 | router.post('/users',async (req,res)=>{ 29 | try{ 30 | const user = new User(req.body) 31 | await user.save(); 32 | sendWelcomeEmail(user.email,user.name) 33 | const token = await user.generateAuthToken(); 34 | 35 | res.status(201).send({user,token}) 36 | }catch(e){ 37 | res.status(400).send({error:e}) 38 | } 39 | }) 40 | 41 | // LOGIN a user 42 | router.post('/users/login',async (req,res)=>{ 43 | const email = req.body.email.toLowerCase(); 44 | const password =req.body.password; 45 | try{ 46 | const user = await User.findByCredentials(email,password); 47 | 48 | const token = await user.generateAuthToken(); 49 | res.send({user,token}) 50 | }catch(e){ 51 | res.status(401).send({error:e.message}) 52 | } 53 | }) 54 | 55 | // LOGOUT a user 56 | router.post('/users/logout',auth,async (req,res)=>{ 57 | try{ 58 | req.user.tokens = req.user.tokens.filter(token=> token.token !== req.token); 59 | await req.user.save() 60 | res.send('Logged out') 61 | }catch(e){ 62 | res.status(500).send({error:e}) 63 | } 64 | }) 65 | 66 | // LOGOUT a user for all active sessions 67 | router.post('/users/logoutAll',auth,async (req,res)=>{ 68 | try{ 69 | req.user.tokens = [] 70 | await req.user.save() 71 | res.send('Successfully logged out of all active sessions') 72 | }catch(e){ 73 | res.status(500).send({error:e}) 74 | } 75 | }) 76 | 77 | // GET a single user after authentication 78 | router.get('/users/me',auth,async (req,res)=>{ 79 | res.send(req.user) 80 | }) 81 | 82 | 83 | // UPDATE a user 84 | router.patch('/users/me',auth,async (req,res)=>{ 85 | // determine which keys are being updated 86 | const updates = Object.keys(req.body) 87 | try{ 88 | updates.forEach(update=>(req.user[update] = req.body[update])) 89 | await req.user.save(); 90 | 91 | res.send(req.user) 92 | }catch(e){ 93 | res.status(400).send({error:e}) 94 | } 95 | }) 96 | 97 | 98 | // DELETE user 99 | router.delete('/users/me',auth, async (req,res)=>{ 100 | try{ 101 | await req.user.remove() 102 | sendCancellationEmail(req.user.email,req.user.name) 103 | res.send(req.user) 104 | }catch(e){ 105 | res.status(404).send({error:e}) 106 | } 107 | }) 108 | 109 | // UPLOAD a profile pic 110 | router.post('/users/me/avatar', auth, upload.single('avatar'),async (req,res)=>{ 111 | const buffer = await sharp(req.file.buffer) 112 | .resize({width:250,height:250}) 113 | .png() 114 | .toBuffer() 115 | req.user.avatar = buffer; 116 | await req.user.save() 117 | res.send() 118 | },(error,req,res,next)=>{ 119 | res.status(400).send({error:e}) 120 | }) 121 | 122 | // REMOVE profile pic 123 | router.delete('/users/me/avatar',auth, async (req,res)=>{ 124 | try{ 125 | req.user.avatar = undefined; 126 | await req.user.save(); 127 | res.send() 128 | }catch(e){ 129 | res.status(500).send({error:e}) 130 | } 131 | }) 132 | 133 | // SERVER UP Image files 134 | router.get('/users/:id/avatar',async (req,res)=>{ 135 | try{ 136 | const user = await User.findById(req.params.id); 137 | 138 | if(!user || !user.avatar){ 139 | throw new Error() 140 | } 141 | 142 | res.set('Content-Type','image/png') 143 | res.send(user.avatar) 144 | }catch(e){ 145 | res.status(404).send({error:e}) 146 | } 147 | }) 148 | 149 | 150 | module.exports = router; --------------------------------------------------------------------------------