├── .babelrc ├── .gitignore ├── README.md ├── Untitled.png ├── arc.png ├── classdiagramfrontend.jpg ├── design.png ├── jest.config.ts ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── schemadiagram.jpg ├── src ├── App.tsx ├── Goalify-logos.jpeg ├── components │ ├── AboutUs.tsx │ ├── HeaderNavBar.css │ ├── HeaderNavBar.tsx │ ├── Profile.tsx │ ├── account_setup │ │ ├── Login.css │ │ ├── Login.tsx │ │ └── Register.tsx │ ├── common │ │ ├── Footer.css │ │ ├── Footer.tsx │ │ ├── LogoFooter.tsx │ │ └── utilities.tsx │ └── dashboard │ │ ├── Dashboard.tsx │ │ ├── Goal.css │ │ ├── Goal.tsx │ │ ├── GoalsList.css │ │ ├── GoalsList.tsx │ │ ├── MilestoneList.css │ │ ├── MilestoneList.tsx │ │ └── statistics │ │ ├── GoalStats.tsx │ │ └── Stats.tsx ├── hooks │ └── useAuth.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── redux │ ├── account_setup │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── type.d.ts │ └── store.ts ├── reportWebVitals.ts ├── setupTests.ts ├── test │ ├── componenets │ │ └── account_setup │ │ │ └── Login.test.tsx │ └── setupTests.js ├── testcases │ └── samples.tsx └── tsInterfaces │ └── interfaces.tsx ├── tsconfig.json └── usecasediagram.jpg /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [] 8 | } -------------------------------------------------------------------------------- /.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* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goalify 2 | ## Table of Contents 3 | 1. [ Introdution. ](#intro) 4 | 2. [ Business Goals and Objectives ](#business) 5 | 3. [ Requirements ](#req) 6 | 4. [ Project Glossary ](#glossary) 7 | 5. [ Technical Stack ](#techstack) 8 | 6. [ Stakeholders Roles. ](#stake) 9 | 7. [ User Stories ](#stories) 10 | 8. [ Design Documentation ](#design) 11 | 9. [ Architecture And Diagrams ](#arc) 12 | 10. [ Run the App ](#run-app) 13 | 11. [ Demo ](#demo) 14 | 12. [ Authors ](#auth) 15 | 16 | 17 | ## 1. Introdution 18 | Goalify is a web application used by users to set their goals, track their progress and achievements, view statistics on their progress, and share that with other users who would also be posting their progress creating some kind of a motivational feedback loop. 19 | 20 | 21 | ## 2. Business Goals and Objectives 22 | The problem: the lack of motivation to get up and pursue whatever any of us is trying to achieve has been a problem for almost everyone. 23 | 24 | Solution: Create an application to assist people put forward objectives and keep tabs on their development towards those objectives. It gives an organized method to follow and track objective advancement through highlights like dashboards and progress bars. Additionally, the platform is a magnificent apparatus for inspiration. 25 | 26 | The app allows users to stay focused on the most critical aspects of their goals. It aids in the identification of potential roadblocks as well as ideas for overcoming them. It can assist in setting more realistic goals and maintaining a happy attitude along the way. Most importantly, uses can share that with other users who would also be posting their progress creating some kind of a motivational feedback loop. 27 | 28 | Competition: There are many apps out there made for the purpose of tracking goals but they all lack in the social side. 29 | 30 | 31 | ## 3. Requirements 32 | ### 3.1. Functioanl Requirements 33 | - Account setup. 34 | - Creating goals. 35 | - Logging daily progress. 36 | - Specifying milestones and tracking them. 37 | - Generating statistics on progress. 38 | - Share goals, progress, achievements and stats. 39 | ### 3.2. Non-Functioanl Requirements 40 | - **Privacy**: Users’ data should be completely safe and private unless it’s shared by the user. This requirement will be met by having two types of data (private and public). This will be enforced by the middle-ware. 41 | - **Security**: We will meet this requirement by using a middle-ware server as a security abstraction between the front-end and the back-end. Servers must be secured against: 42 | - cross-site scripting attacks 43 | - cross-site injections 44 | - insecure connections to the servers 45 | - MIME-sniffing 46 | - Clickjacking 47 | - **Simplicity**: UI should be easy to use without complications as it will be used by young adults and designed for daily use. We will meet this requirement by keeping a minimal and intuitive design in the front-end. 48 | - **Scalability**: The platform should be able to handle an increasing amount of users which at some point could hit millions. This can be handled by the middle-ware by load balancing between the back-end micro services. 49 | - **Extensibility**: The source code of the platform should be able to handle additional features. The developers will meet this by following software engineering design principles such as *S.O.L.I.D.* and *KISS*. 50 | 51 | 52 | ## 4. Project Glossary 53 | - Middle-ware: An intermediate server acting as an interface between the front-end and the back-end. 54 | - Core server: The server which stores all data other the users' credentials. 55 | - Goal: A desired ambition that a person aims to achieve. 56 | - Milestone: Partial actions and achievements necessary to make progress toward a particular goal. 57 | - Discover page: A page where latest public goals are published. 58 | - Private goal: A goal only visible by its creator. 59 | - Public goal: A goal that can visible on the discover page. 60 | - Access token: An access token contains the security credentials for a login session and identifies the user. 61 | - Authentication request: A request sent from the frontend to the middleware to check if the user is granted an access token. 62 | 63 | 64 | ## 5. Technical Stack 65 | - front-end client: ReactJS + Typescript 66 | - user-proxy middleware: Node ExpressJs + Typescript 67 | - user accounts database: MongoDB 68 | - core-server: Python Flask 69 | - main database: MySQL 70 | 71 | 72 | 73 | ## 6. Stakeholders Roles 74 | Stakeholder's Name | Roles | Responsibilities 75 | | :---: | :--- | :--- 76 | Users | Suggest new features, Report existing bugs. | Contact technical support to report bugs and problems. Provide feedback to improve the website. 77 | Board of executives | Direct and lead the project. | Choosing the main functions of the project. Managing and organizing the whole project. Take lead in Decision Making. 78 | Investors | Providing financial support to the development process. | Determining if some features are worth the funding. Deciding the amount of money to spend on certain features. 79 | Developers | Develop and maintain the platform. | Fixing bugs. Adding features. Unit testing. 80 | Project owners | Manage the project. | Accept or reject the project, and provide high level business requirements. 81 | QA testers | Assures the quality of the new features. | End to end testing. 82 | 83 | 84 | 85 | ## 7. User Stoires 86 | User Type | User Story Title | User stories 87 | | :--- | :---: | :--- 88 | Web User | Registration | As a user, I can register on the website by entering my email and password and confirming the password so that I can log in to the platform. 89 | Web User | Login | As a user, I want to able to log in to the website by entering my confidentials (email, password) so that I can use the platform. 90 | Web User | Change confidentials | As a user, I want to be able to change my confidentials (email, password) in order to keep my account safe. 91 | Web User | Profile | As a user, I should be able to upload my profile picture and add a nickname to my account. 92 | Web User | Creating goals | As a user, I want to be able to add new goals so that I can have an organized to-do list. 93 | Web User | Edit goals | As a user, I want to be able to edit my goals so that I can adjust my goals to new circumstances. 94 | Web User | Creating milestones | As a user, I want to be able to create milestones in a specific goal so that I can divide my goal into small achievable steps. 95 | Web User | Publish goals | As a user, I want to be able to publish my goals so I can share my progress. 96 | Web User | View public goals | As a user, I want to be able to view other people's public goals so I can be more motiviated to achieve my own goals. 97 | Web User | Progress tracking | As a user, I should be able to update my progress in any of my goals so that I can view the progression. 98 | Web User | Statistics | As a user, I want to generate stats about my progress so that I could have a better understanding of my performance. 99 | 100 | 101 | ## 8. Design Documentation: 102 | We decided to have one client web app used by users. Another administration interface might be introduced in the future. 103 | 104 | We are using ReactJS as our frontend framework since it’s simple and efficient. Typescript and linter were added to make sure the code is easily debugged, easily read, and as clean as possible. Using Redux to store and organize the data retrieved from the backend, making it available to all of the frontend components. 105 | 106 | Unit tests using Jest and Enzyme were added to make sure that future changes won’t break the existing code. The SOLID principle was followed to make each of our components visible and with clear responsibilities, each of our components does only one thing at a time. 107 | 108 | That client communicates with the backend microservices using a middleware/proxy called user-proxy, which is a Node ExpressJS app responsible for load-balancing and assuring the safety of the backend servers and microservices as well as the user’s data since such data might be sensitive. Typescript and Linter were also added to the middleware for the same purpose explained above. SOLID was also followed, as each of our API middlewares has only one clear job. 109 | 110 | User authentication will be done by user-proxy which communicates with its own MongoDB database, this was done to make sure that no one can access any of the backend microservices and databases unless they’re authored and have access to the exact resource they’re trying to access. A middleware function will be called before any of the API middle-points checking the authority of the accessor. 111 | 112 | Here’s a sample diagram of the flow of a test post request to the endpoint/test: 113 | 114 | ![alt text](https://github.com/Goalify/front-end/blob/main/design.png?raw=true) 115 | 116 | After all of the user authentications are handled by the middleware, the valid requests are sent to the core server. 117 | 118 | We have decided to use Flask as the main framework for the core server because of its flexibility and the compatibility it embraces with the latest technologies. Moreover, it is easy and highly scalable on simple projects. Principles such as KISS and SOLID were followed during the development to make the process of debugging and adding new features simpler and more efficient. The core server is connected to a MySQL database, which stores all data except for the users’ credentials. MySQL was selected as it is secure, flexible, and easy to set up. 119 | 120 | Showing how we applied SOLID to our front-end, we only let our classes have a single responsibilty. For example class `addGoalModal` is only resposible for adding a new goal. Class `DbClickField` only shows some text that can be edited by pressing twice on the text, and so on. We followed Liskov substitution principle by using typescript since it can easily swap components if they share the same contract. We followed interface segregation principle by having multiple interfaces (e.g. `goal`, `milestone`) instead of designing a single interface to substitute them all. We followed dependency inversion principle by making sure that child classes depended on abstraction from parent classes, for example the `GoalsList` sends functions to the class `GoalItem` and these functions will be used by the child class to edit the goal. `GoalItem` doesn't depend on the implementation of the funtions from `GoalsList`. Those functions can be edited without affecting the child class. 121 | 122 | We followed KISS principle by not over-engineering anything and using the simplest methods to achieve objectives. We don't for example use any advanced techniques in react in our front-end such as Compound Components, Props Getters, and State Reducer. 123 | 124 | 125 | ## 9. Architecture And Diagrams 126 | 127 | Architecture: 128 | 129 | ![alt text](https://github.com/Goalify/front-end/blob/main/arc.png?raw=true) 130 | 131 | 132 | Front-End Class Diagram: 133 | 134 | ![alt text](https://github.com/Goalify/front-end/blob/main/classdiagramfrontend.jpg?raw=true) 135 | 136 | 137 | Use Case Diagram: 138 | 139 | ![alt text](https://github.com/Goalify/front-end/blob/main/usecasediagram.jpg?raw=true) 140 | 141 | 142 | Schema Diagram: 143 | 144 | ![alt text](https://github.com/Goalify/front-end/blob/main/schemadiagram.jpg?raw=true) 145 | 146 | 147 | Sequence Diagram: 148 | 149 | ![alt text](https://github.com/Goalify/front-end/blob/main/Untitled.png?raw=true) 150 | 151 | 152 | ## 10. Run the App 153 | To run the app without any errors, you need to first run the `Core-server`, then the `Middleware`, and finally the `front-end`. 154 | ### Core-server 155 | This parts explains how to run the core server on Ubuntu 20.04 or higher and python 3.8 or higher. 156 | #### Database 157 | As prevoiusly mentioned, the core server uses MySQL as its main database. So you need to install MySQL using the following steps. 158 | 1. You need to update the package index on your server if you’ve not done so recently using the command below. 159 | ``` 160 | $ sudo apt update 161 | ``` 162 | 163 | 2. Install the mysql-server package: 164 | ``` 165 | $ sudo apt install mysql-server 166 | ``` 167 | 168 | 3. Then you need to access the MySQL shell as the root user using the following command: 169 | ``` 170 | $ sudo mysql 171 | ``` 172 | NOTE: If you already had installed MySQL and enabled password authentication for root, you will need to use a different command to access the MySQL shell. The command to use in that case: 173 | ``` 174 | $ mysql -u root -p 175 | ``` 176 | This will enable you to access the MySQL shell after entering the password you set. 177 | 178 | 4. Now you need to create a database called 'goalify'. Type this command in the MySQL shell. 179 | ``` 180 | > CREATE DATABASE goalify; 181 | ``` 182 | 183 | 5. Create a user for this database to be used by the core server using the following command: 184 | ``` 185 | > CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password'; 186 | ``` 187 | NOTE: keep `newuser` and `password` as they are. Otherwise you will need to change the user and the password in `db.yaml` file. 188 | 189 | 6. Finally granting the new user a full access to the newly created database 'goalify'. 190 | ``` 191 | > GRANT ALL PRIVILEGES ON goalify.* TO 'newuser'@'localhost'; 192 | ``` 193 | 194 | #### Python 195 | All of the dependencies and necessary modules used are listed in the file `requirements.txt` you need to run the following command in the terminal in the project directory: 196 | ``` 197 | pip install -r requirements.txt 198 | ``` 199 | Now you can run the core server using the command in the project directory: 200 | ``` 201 | python app.py 202 | ``` 203 | This will make the app run on the localhost (127.0.0.1) and the port 3001. If you would like to change that, you can modify `app.py` file and change the values of `IP` and `PORT`. 204 | ### Middleware 205 | 1. You need to install `npm`: 206 | ``` 207 | sudo apt install npm 208 | ``` 209 | 2. You need to `cd` to the directory of the `user-proxy` and install node modules 210 | 211 | ```bash 212 | $ npm install 213 | ``` 214 | 215 | 3. Run the development server 216 | 217 | ```bash 218 | $ npm run dev 219 | ``` 220 | 221 | ### Frontend 222 | 1. You need to install `npm`: 223 | ``` 224 | sudo apt install npm 225 | ``` 226 | 2. You need to `cd` to the directory of the `front-end` repo and run: 227 | ``` 228 | npm install 229 | ``` 230 | 3. Now, to run the app use the following command: 231 | ``` 232 | npm start 233 | ``` 234 | 235 | 236 | ## 11. Demo 237 | [Here](https://youtu.be/ixyBmpZGab0) you can find a link to a video-demo of goalify with some explanations on how the platform works. 238 | 239 | 240 | ## 12. Authors 241 | This project was created and is maintained by: Hasan Khadra, Mahmood Darwish, Mohamad Dwik, Mohammad Shahin. 242 | -------------------------------------------------------------------------------- /Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/Untitled.png -------------------------------------------------------------------------------- /arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/arc.png -------------------------------------------------------------------------------- /classdiagramfrontend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/classdiagramfrontend.jpg -------------------------------------------------------------------------------- /design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/design.png -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | // jest.config.ts 2 | import type {Config} from '@jest/types'; 3 | 4 | // Sync object 5 | export const config: Config.InitialOptions = { 6 | verbose: true, 7 | }; 8 | 9 | // Or async function 10 | export default async (): Promise => { 11 | return { 12 | verbose: true, 13 | }; 14 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end", 3 | "version": "0.1.0", 4 | "jest": { 5 | "name": "my-project", 6 | "jest": { 7 | "verbose": true 8 | } 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "@testing-library/jest-dom": "^5.11.4", 13 | "@testing-library/react": "^11.1.0", 14 | "@testing-library/user-event": "^12.1.10", 15 | "@types/node": "^12.0.0", 16 | "@types/react": "^17.0.0", 17 | "@types/react-dom": "^17.0.0", 18 | "axios": "^0.21.4", 19 | "axios-mock-adapter": "^1.20.0", 20 | "bootstrap": "^5.1.1", 21 | "enzyme": "^3.11.0", 22 | "js-cookie": "^3.0.1", 23 | "mdbreact": "^5.1.0", 24 | "rc-footer": "^0.6.6", 25 | "react": "^17.0.2", 26 | "react-bootstrap": "^2.0.0-rc.0", 27 | "react-dom": "^17.0.2", 28 | "react-hot-toast": "^2.1.1", 29 | "react-redux": "^7.2.5", 30 | "react-router-dom": "^5.3.0", 31 | "react-scripts": "4.0.3", 32 | "react-test-renderer": "^17.0.2", 33 | "redux": "^4.1.1", 34 | "redux-thunk": "^2.3.0", 35 | "simple-react-footer": "^1.0.2", 36 | "styled-components": "^5.3.1", 37 | "ts-node": "^10.2.1", 38 | "typescript": "^4.1.2", 39 | "universal-cookie": "^4.0.4", 40 | "web-vitals": "^1.0.1" 41 | }, 42 | "scripts": { 43 | "start": "react-scripts start", 44 | "build": "react-scripts build", 45 | "test": "jest", 46 | "eject": "react-scripts eject" 47 | }, 48 | "eslintConfig": { 49 | "extends": [ 50 | "react-app", 51 | "react-app/jest" 52 | ] 53 | }, 54 | "browserslist": { 55 | "production": [ 56 | ">0.2%", 57 | "not dead", 58 | "not op_mini all" 59 | ], 60 | "development": [ 61 | "last 1 chrome version", 62 | "last 1 firefox version", 63 | "last 1 safari version" 64 | ] 65 | }, 66 | "devDependencies": { 67 | "@babel/preset-typescript": "^7.15.0", 68 | "@types/enzyme": "^3.10.9", 69 | "@types/jest": "^27.0.1", 70 | "@types/react-redux": "^7.1.18", 71 | "@types/react-router-dom": "^5.3.0", 72 | "@types/react-test-renderer": "^17.0.1", 73 | "@types/redux": "^3.6.0", 74 | "@types/redux-mock-store": "^1.0.3", 75 | "@types/redux-persist": "^4.3.1", 76 | "@types/redux-thunk": "^2.1.0", 77 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.3", 78 | "babel-core": "^6.26.3", 79 | "babel-polyfill": "^6.26.0", 80 | "babel-preset-es2015": "^6.24.1", 81 | "babel-preset-stage-0": "^6.24.1", 82 | "fetch-mock": "^9.11.0", 83 | "html-loader": "^2.1.2", 84 | "redux-mock-store": "^1.5.4", 85 | "redux-persist": "^6.0.0", 86 | "regenerator-runtime": "^0.13.9" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/public/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/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 | -------------------------------------------------------------------------------- /schemadiagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/schemadiagram.jpg -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from './components/dashboard/Dashboard' 2 | import Footer from './components/common/Footer'; 3 | import AboutUs from './components/AboutUs'; 4 | import Profile from './components/Profile'; 5 | import { Route, Switch, Redirect } from 'react-router-dom'; 6 | import Login from './components/account_setup/Login'; 7 | import Register from './components/account_setup/Register'; 8 | import HeaderNavBar from './components/HeaderNavBar'; 9 | import { useSelector } from 'react-redux'; 10 | import { useAuth } from './hooks/useAuth'; 11 | 12 | 13 | function App(){ 14 | const auth = useAuth(); 15 | const user = useSelector((state: UserState) => state.user); 16 | return
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {(!auth && !user) && } 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 |

Discover component to be implemented

47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 |
57 | 58 | } 59 | export default App; -------------------------------------------------------------------------------- /src/Goalify-logos.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/src/Goalify-logos.jpeg -------------------------------------------------------------------------------- /src/components/AboutUs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | function AboutUs(){ 4 | 5 | return
6 |

About Us Page

7 |
8 | } 9 | 10 | export default AboutUs; -------------------------------------------------------------------------------- /src/components/HeaderNavBar.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/src/components/HeaderNavBar.css -------------------------------------------------------------------------------- /src/components/HeaderNavBar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Link, 3 | useHistory, 4 | } from "react-router-dom"; 5 | import { useDispatch } from "react-redux"; 6 | import { Dispatch } from "redux" 7 | import * as actions from "../redux/account_setup/actions"; 8 | import { Navbar, Nav } from "react-bootstrap"; 9 | import 'components/HeaderNavBar.css'; 10 | 11 | const HeaderNavBar = () => { 12 | const history = useHistory(); 13 | const dispatch: Dispatch = useDispatch(); 14 | 15 | const logout = () => { 16 | document.cookie = 'token=;'; 17 | history.push('/login'); 18 | dispatch({ type: actions.REMOVE_USER }); 19 | } 20 | 21 | return ( 22 | 27 | ) 28 | // return
29 | // 36 | //
37 | } 38 | 39 | export default HeaderNavBar; -------------------------------------------------------------------------------- /src/components/Profile.tsx: -------------------------------------------------------------------------------- 1 | 2 | function Profile(){ 3 | 4 | 5 | return
6 |

Profile

7 |
8 | } 9 | 10 | export default Profile; -------------------------------------------------------------------------------- /src/components/account_setup/Login.css: -------------------------------------------------------------------------------- 1 | .login-form{ 2 | margin-top: 20vh; 3 | margin-left: 40vw; 4 | margin-right: 40vw; 5 | } 6 | img { 7 | max-width:100%; 8 | height:auto; 9 | } 10 | 11 | .btn-primary{ 12 | width: 5vw; 13 | margin-left: 7.5vw; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/account_setup/Login.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Redirect } from "react-router-dom"; 4 | import React from 'react'; 5 | import * as actions from "../../redux/account_setup/actions"; 6 | import { Dispatch } from "redux" 7 | import { useSelector, useDispatch } from "react-redux" 8 | import { useAuth } from "../../hooks/useAuth"; 9 | import { Form, Button } from 'react-bootstrap'; 10 | import './Login.css'; 11 | import logo from '../../Goalify-logos.jpeg'; 12 | const Login: React.FC = () => { 13 | 14 | const usernameRef = useRef(null); 15 | const passwordRef = useRef(null); 16 | 17 | const dispatch: Dispatch = useDispatch(); 18 | const auth = useAuth(); 19 | const user = useSelector((state: UserState) => state.user); 20 | 21 | useEffect(() => { 22 | if (usernameRef && usernameRef.current) 23 | usernameRef.current.focus() 24 | }, []); 25 | 26 | const handleSubmit = () => { 27 | if (!usernameRef || !passwordRef || !usernameRef.current || !passwordRef.current) return; 28 | const requestOptions = { 29 | method: 'POST', 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | }, 33 | body: JSON.stringify({ 34 | username: usernameRef.current.value, 35 | password: passwordRef.current.value 36 | }) 37 | } 38 | 39 | fetch('http://localhost:4001/login', requestOptions) 40 | .then(response => response.json()) 41 | .then(data => { 42 | document.cookie = `token=${data.token};Secure`; 43 | document.cookie = `username=${data.username};Secure`; 44 | dispatch({ 45 | type: actions.ADD_USER, 46 | user: { 47 | id: data._id, 48 | username: data.username, 49 | encPassword: data.password, 50 | token: data.token 51 | } 52 | }) 53 | }); 54 | } 55 | 56 | if(auth || user){ 57 | return ; 58 | } 59 | return
60 |
61 | logo 62 |
63 | 64 | Username 65 | 66 | 67 | 68 | 69 | Password 70 | 71 | 72 | 75 |
76 | Don't have an account? 77 | 78 | Register. 79 | 80 |
81 |
82 |
83 |
84 | } 85 | 86 | export default Login; -------------------------------------------------------------------------------- /src/components/account_setup/Register.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Form, Button } from 'react-bootstrap'; 4 | import './Login.css'; 5 | import logo from '../../Goalify-logos.jpeg'; 6 | import { useRef } from "react"; 7 | import { useHistory } from "react-router"; 8 | 9 | const Register = () => { 10 | const usernameRef = useRef(null); 11 | const passwordRef = useRef(null); 12 | const emailRef = useRef(null); 13 | 14 | const history = useHistory(); 15 | 16 | const handleSubmit = () => { 17 | if (!usernameRef || 18 | !passwordRef || 19 | !emailRef || 20 | !usernameRef.current || 21 | !passwordRef.current || 22 | !emailRef.current) return; 23 | 24 | const requestOptions = { 25 | method: 'POST', 26 | headers: { 27 | 'Content-Type': 'application/json' 28 | }, 29 | body: JSON.stringify({ 30 | username: usernameRef.current.value, 31 | password: passwordRef.current.value, 32 | email: emailRef.current.value 33 | }) 34 | } 35 | 36 | fetch('http://localhost:4001/register', requestOptions) 37 | .then(response => response.json()) 38 | .then(data => { 39 | history.push('/login'); 40 | }); 41 | } 42 | 43 | return
44 |
45 | logo 46 |
47 | 48 | Email 49 | 50 | 51 | 52 | 53 | Username 54 | 55 | 56 | 57 | 58 | Password 59 | 60 | 61 | 62 | 65 |
66 | Have an account? 67 | 68 | Login. 69 | 70 |
71 |
72 |
73 |
74 | } 75 | 76 | export default Register; -------------------------------------------------------------------------------- /src/components/common/Footer.css: -------------------------------------------------------------------------------- 1 | footer { 2 | margin-top: 100vh; 3 | } -------------------------------------------------------------------------------- /src/components/common/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | function Footer(){ 4 | return
© Copyright 2021, Goalify
5 | 6 | } 7 | 8 | export default Footer; -------------------------------------------------------------------------------- /src/components/common/LogoFooter.tsx: -------------------------------------------------------------------------------- 1 | import logo from "../../Goalify-logos.jpeg" 2 | const LogoFooter = () => { 3 | 4 | return
5 | Goalify Logo 6 |
7 | } 8 | 9 | export default LogoFooter; -------------------------------------------------------------------------------- /src/components/common/utilities.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | 4 | // TODO integrate with backend 5 | 6 | 7 | export function authenticate(){ 8 | const current_token = localStorage.getItem('access_token'); 9 | return current_token === 'true' ? true : false; 10 | } 11 | 12 | export function getUserId(): string{ 13 | const id = localStorage.getItem('id'); 14 | if(id === null){ 15 | return ""; 16 | } 17 | return id; 18 | } 19 | 20 | export function logOut(){ 21 | localStorage.setItem('access_token', 'false'); 22 | } 23 | 24 | export function getCurrentDateFormat(){ 25 | const nlBEFormatter = new Intl.DateTimeFormat('nl-BE'); 26 | 27 | let today = new Date(); 28 | 29 | const tranformFormat = (time: string) => { 30 | const hours = time.length == 2 ? time : "0" + time; 31 | return hours; 32 | } 33 | 34 | let dateCreated = nlBEFormatter.format(today) + " " + tranformFormat(today.getHours().toString()) + ":" + tranformFormat(today.getMinutes().toString()); 35 | return dateCreated; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import GoalsList from "./GoalsList"; 3 | import HeaderNavBar from "components/HeaderNavBar"; 4 | import Footer from "components/common/Footer"; 5 | 6 | function Dashboard(){ 7 | 8 | return
9 | 10 | 11 |
12 |
13 | } 14 | 15 | export default Dashboard; -------------------------------------------------------------------------------- /src/components/dashboard/Goal.css: -------------------------------------------------------------------------------- 1 | .deadline{ 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | .milestone-item{ 6 | padding-right: 1%; 7 | display: inline-block; 8 | } 9 | .btn-close{ 10 | float: right 11 | } -------------------------------------------------------------------------------- /src/components/dashboard/Goal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import {Goal, Milestone} from "../../tsInterfaces/interfaces"; 3 | import "./Goal.css" 4 | import {MilestonesList} from "./MilestoneList" 5 | import { Card, Collapse, Form } from 'react-bootstrap'; 6 | 7 | export function DbClickField(props: {className?: string, text: string, setText: any}){ 8 | 9 | const nameRef = React.useRef(); 10 | 11 | const [toggle, setToggle] = React.useState(true); 12 | const [userText, setUserText] = React.useState(props.text); 13 | 14 | useEffect(() => { 15 | if(nameRef && nameRef.current){ 16 | nameRef.current.focus() 17 | } 18 | }, [toggle]) 19 | 20 | const handleBlur = () => { 21 | if(userText === ""){ 22 | setUserText(props.text); 23 | setToggle(true); 24 | return; 25 | } 26 | setToggle(true); 27 | props.setText(userText); 28 | } 29 | 30 | return( 31 | toggle ? ( 32 |
{ 35 | setToggle(false) 36 | }} 37 | >{props.text}
38 | ) : ( 39 |
40 | 41 | { setUserText(event.target.value) }} 44 | type="text" 45 | placeholder="Please enter some text" 46 | value={userText} 47 | onKeyDown={(event) => {if(event.key === "Enter") {handleBlur()}}} 48 | onBlur={() => handleBlur()} 49 | /> 50 | 51 |
52 | 53 | ) 54 | ); 55 | } 56 | 57 | function GoalItem(props: {goal: Goal, setGoal: any, deleteGoal: any}) { 58 | 59 | let goal = props.goal; 60 | function edit_name(name: string){ 61 | let new_goal = JSON.parse(JSON.stringify(goal)); 62 | new_goal.name = name; 63 | props.setGoal(new_goal); 64 | } 65 | function edit_description(description: string){ 66 | let new_goal = JSON.parse(JSON.stringify(goal)); 67 | new_goal.description = description; 68 | props.setGoal(new_goal); 69 | } 70 | function edit_state(state: string){ 71 | let new_goal = JSON.parse(JSON.stringify(goal)); 72 | new_goal.state = state; 73 | props.setGoal(new_goal); 74 | } 75 | function edit_published(published: boolean){ 76 | let new_goal = JSON.parse(JSON.stringify(goal)); 77 | new_goal.published = published; 78 | props.setGoal(new_goal); 79 | } 80 | function edit_milestones(milestones: Milestone[]){ 81 | let new_goal = JSON.parse(JSON.stringify(goal)); 82 | new_goal.milestones = milestones; 83 | props.setGoal(new_goal); 84 | } 85 | function edit_deadline(deadline: string){ 86 | let new_goal = JSON.parse(JSON.stringify(goal)); 87 | new_goal.deadline = deadline; 88 | props.setGoal(new_goal); 89 | } 90 | 91 | const [open, setOpen] = React.useState(false); 92 | 93 | return
94 | 95 | setOpen(!open)}> 96 | 97 | 98 | 99 | 100 | 101 |
102 |
103 | 108 | 112 | 113 |
114 | 115 | 116 |
Deadline:
117 | 118 | 119 |
120 |
121 |
122 | 123 | {goal.dateCreated} 124 |
125 |
126 | } 127 | 128 | export default GoalItem; -------------------------------------------------------------------------------- /src/components/dashboard/GoalsList.css: -------------------------------------------------------------------------------- 1 | .card-header{ 2 | border-bottom: 0px solid rgba(0,0,0,.125) !important; 3 | } 4 | .card-footer{ 5 | border-top: 0px solid rgba(0,0,0,.125) !important; 6 | } -------------------------------------------------------------------------------- /src/components/dashboard/GoalsList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Goals, Goal} from '../../tsInterfaces/interfaces' 3 | import GoalItem from './Goal'; 4 | import { Card, Button, Modal, Form } from 'react-bootstrap'; 5 | import './GoalsList.css'; 6 | import GoalStats from './statistics/GoalStats'; 7 | import { useAuth } from 'hooks/useAuth'; 8 | function GoalsList() { 9 | 10 | const [goals, setGoals] = React.useState({list: []}); 11 | const [modalShow, setModalShow] = React.useState(false); 12 | const auth = useAuth(); 13 | 14 | 15 | React.useEffect(() => { 16 | if (!auth) 17 | return; 18 | const requestOptions = { 19 | method: 'GET', 20 | headers: { 21 | 'Content-Type': 'application/json' 22 | } 23 | } 24 | 25 | fetch(`http://localhost:4001/get-goals?token=${auth?.token}`, requestOptions) 26 | .then(response => response.json()) 27 | .then(data => { 28 | console.log(data); 29 | setGoals(data); 30 | }).catch(e => console.log(e)); 31 | }, [auth]) 32 | 33 | const setGoal = (goal: Goal) => { 34 | const new_goals = goals.list.slice(); 35 | 36 | 37 | let j = 0; 38 | for (let i = 0; i < new_goals.length; i++) { 39 | if (new_goals[i].id === goal.id) { 40 | j = i; 41 | break; 42 | } 43 | } 44 | 45 | 46 | let count = 0; 47 | for (let i = 0; i < goal.milestones.length; i++) { 48 | if (goal.milestones[i].state) { 49 | count += 1; 50 | } 51 | } 52 | 53 | let new_goal = goal; 54 | if(count === new_goal.milestones.length && count !== 0) { 55 | let today = new Date(); 56 | 57 | const nlBEFormatter = new Intl.DateTimeFormat('nl-BE'); 58 | let dateFinished = nlBEFormatter.format(today); 59 | 60 | new_goal = { 61 | ...goal, 62 | dateFinished: dateFinished 63 | } 64 | } 65 | 66 | new_goals[j] = new_goal; 67 | 68 | let val = new_goal.dateFinished === null ? 'null' : new_goal.dateFinished.toString() 69 | 70 | let sent_goal: any = { 71 | ...new_goal, 72 | published: new_goal.published.toString(), 73 | milestones: new_goal.milestones.toString(), 74 | dateFinished: val 75 | } 76 | const requestOptions = { 77 | method: 'POST', 78 | headers: { 79 | 'Content-Type': 'application/json' 80 | }, 81 | body: JSON.stringify({ 82 | goal: sent_goal, 83 | token: auth?.token 84 | }) 85 | } 86 | 87 | fetch('http://localhost:4001/edit-goal', requestOptions) 88 | .then(response => { 89 | setGoals({ list: new_goals }); 90 | }).catch(e => console.log(e)); 91 | 92 | setGoals({ list: new_goals }); 93 | 94 | } 95 | 96 | const deleteGoal = (goal: Goal) => { 97 | 98 | const old_goals = goals.list.slice(); 99 | let new_goals: Goals = { list: [] }; 100 | 101 | for (let i = 0; i < old_goals.length; i++) { 102 | if (old_goals[i].id === goal.id) { 103 | continue; 104 | } 105 | new_goals.list.push(old_goals[i]); 106 | } 107 | 108 | const requestOptions = { 109 | method: 'POST', 110 | headers: { 111 | 'Content-Type': 'application/json' 112 | }, 113 | body: JSON.stringify({ 114 | id: goal.id, 115 | token: auth?.token 116 | }) 117 | } 118 | 119 | fetch('http://localhost:4001/remove-goal', requestOptions) 120 | .then(response => { 121 | setGoals(new_goals); 122 | }).catch(e => console.log(e)); 123 | } 124 | 125 | const addGoal = (goal: Goal) => { 126 | const new_goals = goals.list.slice(); 127 | new_goals.push(goal); 128 | setGoals({ list: new_goals }); 129 | } 130 | 131 | return
132 | {goals.list.map((goal) => 133 |
136 |
)} 137 | setModalShow(true)}> 138 | 139 | 140 | Add a new goal 141 | 142 | 143 | 144 | setModalShow(false)}/> 145 |
; 146 | } 147 | 148 | 149 | const AddGoalModal = ({addGoal, show, handleClose} : any) => { 150 | const nameRef = React.useRef(null); 151 | const descriptionRef = React.useRef(null); 152 | const auth = useAuth(); 153 | 154 | const handleAdd = () => { 155 | if (!nameRef || !nameRef.current || !descriptionRef || !descriptionRef.current) return; 156 | if(!nameRef.current.value){ 157 | alert('Fields shouldn\'t be empty') 158 | return; 159 | } 160 | 161 | let today = new Date(); 162 | 163 | const nlBEFormatter = new Intl.DateTimeFormat('nl-BE'); 164 | let dateCreated = nlBEFormatter.format(today); 165 | 166 | console.log("Reererer") 167 | const requestOptions = { 168 | method: 'POST', 169 | headers: { 170 | 'Content-Type': 'application/json' 171 | }, 172 | body: JSON.stringify({ 173 | goal:{ 174 | name: nameRef.current.value, 175 | description: descriptionRef.current.value, 176 | deadline: "None", 177 | dateCreated: dateCreated, 178 | state: "Idle", 179 | published: 'false', 180 | milestones: '[]', 181 | dateFinished: 'null' 182 | }, 183 | token: auth?.token 184 | }) 185 | } 186 | 187 | fetch('http://localhost:4001/add-goal', requestOptions) 188 | .then(response => response.json()) 189 | .then(data => { 190 | if (!nameRef || !nameRef.current || !descriptionRef || !descriptionRef.current) return; 191 | let new_goal: Goal = { 192 | id: data.id.toString(), 193 | name: nameRef.current.value, 194 | description: descriptionRef.current.value, 195 | deadline: "None", 196 | dateCreated: dateCreated, 197 | state: "Idle", 198 | published: false, 199 | milestones: [], 200 | dateFinished: null, 201 | } 202 | addGoal(new_goal); 203 | }); 204 | 205 | 206 | handleClose(); 207 | } 208 | 209 | React.useEffect(() => { 210 | if (nameRef && nameRef.current) 211 | nameRef.current.focus() 212 | }, []); 213 | 214 | return ( 215 | 216 | Add a new goal 217 | 218 | 219 |
220 | 221 | Name 222 | 223 | 224 | 225 | Description 226 | 227 | 228 |
229 |
230 | 231 | 234 | 237 | 238 |
) 239 | } 240 | 241 | export default GoalsList; -------------------------------------------------------------------------------- /src/components/dashboard/MilestoneList.css: -------------------------------------------------------------------------------- 1 | .milestones-list{ 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | } -------------------------------------------------------------------------------- /src/components/dashboard/MilestoneList.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, ChangeEvent} from "react"; 2 | import {Milestone} from "../../tsInterfaces/interfaces"; 3 | import {DbClickField} from "./Goal" 4 | import {Modal, Button, Form} from 'react-bootstrap'; 5 | import { useAuth } from "hooks/useAuth"; 6 | import {stringify} from "querystring"; 7 | 8 | function MilestoneItem(props: {milestone: Milestone, setMilestone: any, deleteMilestone: any}){ 9 | 10 | let milestone = props.milestone 11 | function edit_name(name: string){ 12 | let new_milestone = JSON.parse(JSON.stringify(milestone)); 13 | new_milestone.name = name; 14 | props.setMilestone(new_milestone); 15 | } 16 | const edit_state = (event:ChangeEvent) => { 17 | let new_milestone = JSON.parse(JSON.stringify(milestone)); 18 | new_milestone.state = !new_milestone.state; 19 | props.setMilestone(new_milestone); 20 | } 21 | 22 | return( 23 |
24 | 25 | Completed: edit_state(event)}/> 26 | 27 |
28 | ); 29 | } 30 | 31 | export function MilestonesList(props: {goal_id: string, milestonesList: Milestone[], editMilestoneList: any}){ 32 | 33 | const [milestones, setMilestones] = React.useState(props.milestonesList); 34 | const [modalShow, setModalShow] = React.useState(false); 35 | const nameRef = React.useRef(null); 36 | const auth = useAuth(); 37 | useEffect(() => { 38 | if(nameRef && nameRef.current){ 39 | nameRef.current.focus() 40 | } 41 | }, []) 42 | 43 | 44 | const setMilestone = (milestone: Milestone) => { 45 | const new_milestones = milestones.slice(); 46 | 47 | let j = 0; 48 | for(let i=0;i { 73 | setMilestones(new_milestones); 74 | props.editMilestoneList(new_milestones); 75 | }).catch(e => console.log(e)); 76 | 77 | } 78 | 79 | const deleteMilestone = (milestone: Milestone) => { 80 | 81 | const old_milestones = milestones.slice(); 82 | let new_milestones: Milestone[] = []; 83 | 84 | for(let i=0;i { 104 | setMilestones(new_milestones); 105 | props.editMilestoneList(new_milestones); 106 | }).catch(e => console.log(e)); 107 | } 108 | 109 | 110 | const handleKeyPress = (target: any) => { 111 | if(target.charCode === 13){ 112 | target.preventDefault(); 113 | addMilestone(); 114 | } 115 | } 116 | 117 | const addMilestone = () => { 118 | 119 | if(!nameRef || !nameRef.current)return; 120 | if(!nameRef.current.value){ 121 | alert("Field Name shouldn't be empty") 122 | return; 123 | } 124 | 125 | const requestOptions = { 126 | method: 'POST', 127 | headers: { 128 | 'Content-Type': 'application/json' 129 | }, 130 | body: JSON.stringify({ 131 | milestone:{ 132 | name: nameRef.current.value, 133 | goal_id: props.goal_id, 134 | state: 'false', 135 | }, 136 | token: auth?.token 137 | }) 138 | } 139 | 140 | fetch('http://localhost:4001/add-milestone', requestOptions) 141 | .then(response => response.json()) 142 | .then(data => { 143 | if (!nameRef || !nameRef.current) return; 144 | let new_milestone: Milestone = { 145 | id: data.id.toString(), 146 | name: nameRef.current.value, 147 | state: false, 148 | goal_id: data.goal_id 149 | } 150 | const new_milestones = milestones.slice(); 151 | new_milestones.push(new_milestone); 152 | setMilestones(new_milestones); 153 | setModalShow(false); 154 | props.editMilestoneList(new_milestones) 155 | }); 156 | 157 | } 158 | 159 | 160 | let milestoneList = 161 |
{milestones.map((milestone, ind) => 162 | )}
; 163 | 164 | return ( 165 |
166 | setModalShow(false)}> 167 | 168 | Add a new milestone 169 | 170 | 171 |
172 | 173 | Name 174 | 176 | 177 |
178 |
179 | 180 | 183 | 186 | 187 |
188 | {milestoneList} 189 |
190 | 191 |
192 |
) 193 | 194 | } -------------------------------------------------------------------------------- /src/components/dashboard/statistics/GoalStats.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Goal, Milestone} from '../../../tsInterfaces/interfaces' 3 | 4 | function GoalStats(props: {goal: Goal}){ 5 | 6 | const countCompletedMilstones = (milestones: Milestone[]) => { 7 | let count = 0; 8 | for(let i=0;i; 17 | if(props.goal.milestones.length !== 0){ 18 | 19 | let completePercentage: number = countCompletedMilstones(props.goal.milestones) / props.goal.milestones.length; 20 | 21 | completePercentageElement =
The goal is {(completePercentage*100).toFixed(0)}% complete. {" "} 22 | {countCompletedMilstones(props.goal.milestones)}/{props.goal.milestones.length} milestones finished.
23 | } 24 | 25 | 26 | let timeSpentElement: JSX.Element =
; 27 | if(props.goal.milestones.every((milestone) => milestone.state === true) && props.goal.milestones.length !== 0){ 28 | let timeSpent: number = 0; 29 | if(props.goal.dateFinished && props.goal.dateFinished !== "") 30 | timeSpent = new Date(props.goal.dateFinished).getTime() - new Date(props.goal.dateCreated).getTime(); 31 | timeSpentElement =
32 | Time spent: {msToTime(timeSpent)} 33 |
34 | } 35 | 36 | function msToTime(s: number) { 37 | let ms = s % 1000; 38 | s = (s - ms) / 1000; 39 | let secs = s % 60; 40 | s = (s - secs) / 60; 41 | let mins = s % 60; 42 | s = (s - mins) / 60; 43 | let hrs = s % 24; 44 | s = (s - hrs) / 24; 45 | let days = s; 46 | 47 | return days + " days, " + hrs + ' hours, ' + mins + ' minutes, and ' + secs + ' seconds'; 48 | } 49 | 50 | let expectedTimeToFinishElement: JSX.Element =
; 51 | if(props.goal.milestones.length !== 0){ 52 | let completedMilestones: number = countCompletedMilstones(props.goal.milestones); 53 | let allMilestones: number = props.goal.milestones.length; 54 | if(completedMilestones !== 0 && completedMilestones !== allMilestones){ 55 | let timeSpent: number = new Date().getTime() - new Date(props.goal.dateCreated).getTime(); 56 | let expectedTime: number = (timeSpent / completedMilestones) * (allMilestones - completedMilestones); 57 | 58 | expectedTimeToFinishElement =
Expected time to finish the goal is: {msToTime(expectedTime)}
59 | } 60 | } 61 | 62 | return(
63 | {completePercentageElement} 64 |
) 65 | } 66 | 67 | export default GoalStats; -------------------------------------------------------------------------------- /src/components/dashboard/statistics/Stats.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Goals} from '../../../tsInterfaces/interfaces' 3 | 4 | function Stats(props: {goals: Goals}){ 5 | 6 | } 7 | 8 | export default Stats; -------------------------------------------------------------------------------- /src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | type Auth = { 4 | token: string, 5 | username: string 6 | } 7 | 8 | export function useAuth() { 9 | const [auth, setAuth] = useState(null); 10 | 11 | const get_cookie = (name: string) => document.cookie.split('; ').find(row => row.startsWith(name))?.split('=')[1]; 12 | 13 | useEffect(() => { 14 | const token = get_cookie('token'); 15 | const username = get_cookie('username'); 16 | if(token && username){ 17 | setAuth({ 18 | token: token, 19 | username: username 20 | }) 21 | } 22 | return () => { 23 | setAuth(null); 24 | } 25 | }, []); 26 | 27 | return auth; 28 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .footer{ 2 | position: fixed; 3 | left: 0; 4 | bottom: 0; 5 | width: 100%; 6 | height: 3vh; 7 | padding-bottom: 20px; 8 | border-width: 1px; 9 | background-color: #5065a8; 10 | color: #f2eee3; 11 | text-align: center; 12 | } 13 | 14 | .headernavbar{ 15 | list-style-type: none; 16 | padding: 0; 17 | margin: 0; 18 | width: 100%; 19 | padding: 1px; 20 | color: red; 21 | background-color: #5065a8; 22 | } 23 | 24 | 25 | .headernavbar li a { 26 | display: block; 27 | padding: 8px; 28 | color: #f2eee3; 29 | text-decoration: none; 30 | } 31 | 32 | #dashboard-button{ 33 | margin-left: 600px; 34 | } 35 | 36 | .headernavbar li{ 37 | display: inline-block; 38 | } 39 | 40 | 41 | 42 | 43 | .authors{ 44 | font: 1.2em "Fira Sans", serif; 45 | font-size: 15px; 46 | float: left; 47 | padding-right: 400px; 48 | margin-left: 30px; 49 | } 50 | 51 | .aboutUs{ 52 | font: 1.2em "Fira Sans", serif; 53 | font-size: 15px; 54 | float: left; 55 | bottom: 20px; 56 | white-space: pre-line; 57 | } 58 | 59 | .logo{ 60 | margin-left: 200px; 61 | float: left; 62 | margin-top: 10px; 63 | border-radius: 50%; 64 | } 65 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import App from "./App" 6 | import {BrowserRouter as Router} from 'react-router-dom' 7 | import store from "./redux/store"; 8 | import { Provider } from 'react-redux'; 9 | import 'bootstrap/dist/css/bootstrap.min.css'; 10 | document.title = "Goalify" 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | 23 | // If you want to start measuring performance in your app, pass a function 24 | // to log results (for example: reportWebVitals(console.log)) 25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 26 | reportWebVitals(); 27 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/redux/account_setup/actions.ts: -------------------------------------------------------------------------------- 1 | export const ADD_USER = "ADD_USER"; 2 | export const REMOVE_USER = "REMOVE_USER"; 3 | -------------------------------------------------------------------------------- /src/redux/account_setup/reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from "./actions"; 2 | 3 | const initialState: UserState = { 4 | user: null 5 | } 6 | 7 | const reducer = (state: UserState = initialState, action: UserAction) => { 8 | switch (action.type) { 9 | case actions.ADD_USER: 10 | return { 11 | user: action.user 12 | } 13 | case actions.REMOVE_USER: 14 | return { 15 | user: null 16 | } 17 | } 18 | return state; 19 | } 20 | 21 | export default reducer; -------------------------------------------------------------------------------- /src/redux/account_setup/type.d.ts: -------------------------------------------------------------------------------- 1 | interface IUser{ 2 | id: string, 3 | username: string, 4 | encPassword: string, 5 | token: string 6 | } 7 | 8 | type UserState = { 9 | user: Iuser | null 10 | } 11 | 12 | type UserAction = { 13 | type: string, 14 | user?: Iuser 15 | } 16 | 17 | type DispatchType = (args: UserAction) => UserAction -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, Store } from "redux" 2 | import thunk from "redux-thunk" 3 | 4 | import reducer from "./account_setup/reducer"; 5 | 6 | const store: Store & { 7 | dispatch: DispatchType 8 | } = createStore(reducer, applyMiddleware(thunk)) 9 | 10 | export default store; -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/test/componenets/account_setup/Login.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Login from "../../../components/account_setup/Login" 3 | import { mount, ReactWrapper } from 'enzyme'; 4 | import {BrowserRouter as Router} from 'react-router-dom' 5 | import fetchMock from 'fetch-mock'; 6 | import { Provider } from "react-redux"; 7 | import "../../setupTests.js"; 8 | import configureMockStore, { MockStore } from 'redux-mock-store'; 9 | import thunk from 'redux-thunk'; 10 | 11 | const middlewares = [thunk]; 12 | const mockStore = configureMockStore(middlewares); 13 | 14 | describe('Login', () => { 15 | let wrapper: ReactWrapper; 16 | let store: MockStore; 17 | beforeEach(() => { 18 | store = mockStore({ user: null }); 19 | wrapper = mount( 20 | 21 | 22 | 23 | ); 24 | }) 25 | it('logs in', () => { 26 | 27 | const username = wrapper.find('#username'); 28 | const password = wrapper.find('#password'); 29 | const submit = wrapper.find('#submit'); 30 | 31 | username.simulate('change', { target: { value: 'test-username' } }); 32 | password.simulate('change', { target: { value: 'test-password' } }); 33 | const data = JSON.stringify({ 34 | response: true, 35 | "_id": "someid", 36 | "username": "test-username", 37 | "email": "test@test-mail.com", 38 | "password": "test-encrypted-password", 39 | "__v": 0, 40 | "token": "test-token" 41 | }); 42 | fetchMock.postOnce('http://localhost:4001/login', data); 43 | 44 | submit.simulate('click'); 45 | 46 | expect(store.getState().user).toBe({ 47 | id: "someid", 48 | username: "test-username", 49 | email: "test@test-mail.com", 50 | encPassword: "test-encrypted-password", 51 | token: "test-token" 52 | }) 53 | }) 54 | }) -------------------------------------------------------------------------------- /src/test/setupTests.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime' 2 | import Enzyme from 'enzyme'; 3 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /src/testcases/samples.tsx: -------------------------------------------------------------------------------- 1 | import {Goal, Goals} from '../tsInterfaces/interfaces' 2 | /* 3 | id: string, 4 | state: string, 5 | name: string, 6 | description: string, 7 | milestones: milestones[], 8 | dateCreated: string; 9 | 10 | */ 11 | 12 | 13 | export const goal1: Goal = { 14 | id: "1", 15 | state: "InProgress", 16 | name: "Presentation", 17 | description: "this is the description", 18 | milestones: [], 19 | published: true, 20 | deadline: "12/Sep", 21 | dateCreated: "10/9/2021", 22 | dateFinished: null 23 | } 24 | 25 | export const goal2: Goal = { 26 | id: "2", 27 | state: "Done", 28 | name: "Finish SSAD project", 29 | description: "description here", 30 | milestones: [], 31 | published: false, 32 | deadline: "24/Sep", 33 | dateCreated: "10/7/2021", 34 | dateFinished: "10/9/2021" 35 | } 36 | 37 | export const goal3: Goal = { 38 | id: "3", 39 | state: "ToDo", 40 | name: "Lose weight", 41 | description: "description here", 42 | milestones: [], 43 | published: false, 44 | deadline: "24/Sep", 45 | dateCreated: "10/8/2021", 46 | dateFinished: "10/9/2021" 47 | 48 | } 49 | 50 | // export const goal3: Goal = { 51 | // id: "3", 52 | // state: "idle", 53 | // name: "play the piano", 54 | // description: "learn bella ciao song", 55 | // milestones: {list: [{id: "1", state: true, description: "description", dateFinished: "fds", dateCreated: 'ww'}, 56 | // {id: "1", state: false, description: "description", dateFinished: "fds", dateCreated: 'ww'}]}, 57 | // published: false, 58 | // deadline: "14/Oct", 59 | // dateFinished: "", 60 | // dateCreated: "10/Oct", 61 | // } 62 | 63 | // export const goal4: Goal = { 64 | // id: "4", 65 | // state: "idle", 66 | // name: "play the piano", 67 | // description: "learn bella ciao song", 68 | // milestones: {list: [{id: "1", state: true, description: "description", dateFinished: "fds", dateCreated: 'ww'}, 69 | // {id: "1", state: false, description: "description", dateFinished: "fds", dateCreated: 'ww'}]}, 70 | // published: true, 71 | // deadline: "11/Nov", 72 | // dateFinished: "", 73 | // dateCreated: "10/Oct", 74 | // } -------------------------------------------------------------------------------- /src/tsInterfaces/interfaces.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useContext } from 'react'; 3 | 4 | export interface Milestone{ 5 | id: string, 6 | state: boolean, 7 | name: string, 8 | goal_id: string 9 | } 10 | 11 | export interface Goal { 12 | id: string, 13 | state: string, 14 | name: string, 15 | description: string, 16 | milestones: Milestone[], 17 | published: boolean, 18 | deadline: string, 19 | dateFinished: string | null, 20 | dateCreated: string; 21 | }; 22 | 23 | export interface Goals { 24 | list: Goal[]; 25 | }; 26 | 27 | export interface LoginProps{ 28 | setIsLoggedIn: React.Dispatch>; 29 | } 30 | 31 | export interface HttpResponse extends Response { 32 | parsedBody?: JSON; 33 | } 34 | 35 | export interface User{ 36 | token: boolean, 37 | toggleToken: React.Dispatch>; 38 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /usecasediagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goalify/front-end/28774210e489d82e97b0ec30412296f71eb75d0f/usecasediagram.jpg --------------------------------------------------------------------------------