├── doto-frontend ├── .env.example ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ ├── reminderWorker.js │ ├── web.config │ └── index.html ├── src │ ├── tailwind.css │ ├── constants │ │ ├── Themes.js │ │ └── Categories.js │ ├── components │ │ ├── images │ │ │ ├── lock.png │ │ │ ├── streak.png │ │ │ ├── icons │ │ │ │ ├── icon_180x180.png │ │ │ │ ├── icon_192x192.png │ │ │ │ └── icon_512x512.png │ │ │ ├── modal-background-green.jpg │ │ │ └── modal-background-purple.jpg │ │ ├── Points.js │ │ ├── MarketPlace.css │ │ ├── pages │ │ │ ├── Header.css │ │ │ ├── NotFound.js │ │ │ ├── Calendar │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── Calendar.test.js.snap │ │ │ │ ├── Calendar.css │ │ │ │ ├── CalendarListView.js │ │ │ │ ├── TaskShifter.js │ │ │ │ ├── Calendar.test.js │ │ │ │ ├── TaskScheduler.js │ │ │ │ └── CalendarComponent.js │ │ │ ├── Pages.css │ │ │ ├── Login │ │ │ │ ├── Login.test.js │ │ │ │ ├── Login.css │ │ │ │ ├── Login.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Login.test.js.snap │ │ │ ├── Settings │ │ │ │ ├── SettingsPage.css │ │ │ │ ├── SettingsPage.test.js │ │ │ │ └── SettingsPage.js │ │ │ └── Header.js │ │ ├── ProductivityScore.css │ │ ├── UserStats.css │ │ ├── updateModal │ │ │ └── UpdateModalContent.css │ │ ├── ModalContent.css │ │ ├── ProductivityScore.js │ │ ├── Streak.js │ │ ├── AvailableTheme.js │ │ ├── UserStats.js │ │ └── MarketPlace.js │ ├── context │ │ ├── ThemeContext.js │ │ └── ActiveHoursContext.js │ ├── index.css │ ├── setupTests.js │ ├── helpers │ │ ├── CookieManager.js │ │ ├── PrivateRoute.js │ │ └── DotoService.js │ ├── index.js │ ├── routes │ │ ├── Route.test.js │ │ └── Route.js │ └── serviceWorker.js ├── .prettierrc ├── postcss.config.js ├── .gitignore ├── .eslintrc.json ├── package.json └── README.md ├── doto-backend ├── .prettierrc ├── src │ ├── constants │ │ └── http-response.js │ ├── common │ │ └── logging.js │ ├── routes │ │ ├── reminder-route.js │ │ ├── auth-route.js │ │ ├── user-route.js │ │ └── task-route.js │ ├── config │ │ ├── token-setup.js │ │ └── passport-setup.js │ ├── models │ │ ├── User.js │ │ └── Task.js │ └── webpush │ │ └── reminder-service.js ├── .env.example ├── .eslintrc.json ├── package.json ├── test │ ├── user.test.js │ └── task.test.js ├── index.js ├── web.config └── swagger.json ├── lerna.json ├── .github ├── ISSUE_TEMPLATE │ ├── user-story--documentation-other-.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── doto_backend_deploy.yml │ ├── doto_frontend_deploy.yml │ └── doto_ci.yml └── pull_request_template.md ├── package.json ├── CONTRIBUTIONS.md ├── license.md ├── wiki_guidelines.md ├── .all-contributorsrc ├── Code_of_conduct.md ├── .gitignore └── README.md /doto-frontend/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_VAPID_PUBLIC_KEY= 2 | -------------------------------------------------------------------------------- /doto-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/public/favicon.ico -------------------------------------------------------------------------------- /doto-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/public/logo192.png -------------------------------------------------------------------------------- /doto-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/public/logo512.png -------------------------------------------------------------------------------- /doto-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /doto-frontend/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /doto-backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /doto-frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /doto-frontend/src/constants/Themes.js: -------------------------------------------------------------------------------- 1 | export const Themes = { 2 | LIGHT: "light", 3 | DARK: "dark", 4 | }; 5 | -------------------------------------------------------------------------------- /doto-frontend/src/components/images/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/lock.png -------------------------------------------------------------------------------- /doto-frontend/src/components/images/streak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/streak.png -------------------------------------------------------------------------------- /doto-frontend/src/context/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const ThemeContext = createContext({}); 4 | -------------------------------------------------------------------------------- /doto-frontend/src/context/ActiveHoursContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const ActiveHoursContext = createContext({}); 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["doto-frontend", "doto-backend"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": false, 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /doto-frontend/src/components/images/icons/icon_180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/icons/icon_180x180.png -------------------------------------------------------------------------------- /doto-frontend/src/components/images/icons/icon_192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/icons/icon_192x192.png -------------------------------------------------------------------------------- /doto-frontend/src/components/images/icons/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/icons/icon_512x512.png -------------------------------------------------------------------------------- /doto-frontend/src/components/images/modal-background-green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/modal-background-green.jpg -------------------------------------------------------------------------------- /doto-frontend/src/components/images/modal-background-purple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/se701g2/Doto/HEAD/doto-frontend/src/components/images/modal-background-purple.jpg -------------------------------------------------------------------------------- /doto-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require("tailwindcss"); 2 | module.exports = { 3 | plugins: [tailwindcss("./tailwind.js"), require("autoprefixer")], 4 | }; 5 | -------------------------------------------------------------------------------- /doto-backend/src/constants/http-response.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | UNAUTHORIZED: Number(401), 3 | FORBIDDEN: Number(403), 4 | SUCCESSFUL: Number(200), 5 | BADREQUEST: Number(400), 6 | }; 7 | -------------------------------------------------------------------------------- /doto-frontend/src/components/Points.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Avatar from "@material-ui/core/Avatar"; 3 | 4 | const Points = props => ( 5 | 6 | {props.value || 0} 7 | 8 | ); 9 | 10 | export default Points; 11 | -------------------------------------------------------------------------------- /doto-frontend/src/components/MarketPlace.css: -------------------------------------------------------------------------------- 1 | .market-content-box { 2 | display: inline-block; 3 | margin: 1px auto; 4 | } 5 | 6 | .theme-content-box { 7 | display: inline-block; 8 | } 9 | 10 | #color-palette { 11 | margin-top: 3vh; 12 | height: 40px; 13 | margin: 1px auto; 14 | } 15 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Header.css: -------------------------------------------------------------------------------- 1 | .Title { 2 | font-weight: bold; 3 | font-size: 70px; 4 | float: left; 5 | } 6 | 7 | .IconLarge { 8 | transform: scale(1.5); 9 | } 10 | 11 | nav { 12 | font-size: calc(10px + 2vmin); 13 | margin-right: 5vw; 14 | } 15 | 16 | a { 17 | color: black; 18 | } 19 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () => { 4 | return ( 5 |
6 |
7 |
404: Not Found
8 |
9 |
10 | ); 11 | }; 12 | 13 | export default NotFound; 14 | -------------------------------------------------------------------------------- /doto-backend/.env.example: -------------------------------------------------------------------------------- 1 | # The following environment variables will need to be set before you can start development, you will need to retrieve them from the repo maintainer 2 | # Remember to never push your .env file to github 3 | 4 | DEVELOPMENT_DB_CONN= 5 | ACCESS_TOKEN_SECRET= 6 | GOOGLE_API_SECRET= 7 | GOOGLE_API_CLIENT= 8 | VAPID_PUBLIC_KEY= 9 | VAPID_PRIVATE_KEY= -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/user-story--documentation-other-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: User Story (Documentation/Other) 3 | about: Standard user story 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### User Story: 11 | 12 | As a \, I want \ so that \ 13 | 14 | 15 | ### Acceptance Criteria: 16 | 17 | 1) 18 | 2) 19 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/__snapshots__/Calendar.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component being rendered Make sure render matches snapshot 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | " 11 | `; 12 | -------------------------------------------------------------------------------- /doto-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", 4 | "Droid Sans", "Helvetica Neue", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 11 | } 12 | -------------------------------------------------------------------------------- /doto-frontend/src/setupTests.js: -------------------------------------------------------------------------------- 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/extend-expect"; 6 | import Enzyme from "enzyme"; 7 | import Adapter from "enzyme-adapter-react-16"; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "start": "lerna run --parallel start", 6 | "postinstall": "lerna bootstrap" 7 | }, 8 | "devDependencies": { 9 | "husky": "^4.2.3", 10 | "lerna": "^3.20.2" 11 | }, 12 | "husky": { 13 | "hooks": { 14 | "pre-commit": "lerna run --concurrency 1 --stream precommit --since HEAD" 15 | } 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /doto-backend/src/common/logging.js: -------------------------------------------------------------------------------- 1 | const { createLogger, transports, format } = require("winston"); 2 | 3 | const logger = createLogger({ 4 | transports: [new transports.Console()], 5 | format: format.combine( 6 | format.timestamp(), 7 | format.colorize(), 8 | format.printf((info) => `${info.timestamp} [${info.level}] ${JSON.stringify(info.message)}`), 9 | ), 10 | }); 11 | 12 | module.exports.logger = logger; 13 | -------------------------------------------------------------------------------- /doto-backend/src/routes/reminder-route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authenticateToken = require("../config/token-setup").authenticateToken; 4 | const reminderService = require("../webpush/reminder-service"); 5 | 6 | router.post("/subscribe", authenticateToken, (req, res) => { 7 | reminderService.subscribe(req.user.email, req.body); 8 | res.status(201).json({}); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### User Story: 11 | 12 | As a \, I want \ so that \ 13 | 14 | 15 | ### Acceptance Criteria: 16 | 17 | 1) 18 | 2) 19 | 20 | 21 | ### Why is the feature required? 22 | 23 | ### Describe the solution you'd like 24 | 25 | ### Additional Context: 26 | -------------------------------------------------------------------------------- /doto-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | 26 | # Custom 27 | 28 | /src/tailwind-generated.css -------------------------------------------------------------------------------- /doto-frontend/src/helpers/CookieManager.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | 3 | // Simple getter-setter method to get the key values of the current session 4 | const CookieManager = { 5 | set: (key, value) => { 6 | Cookies.set(key, value); 7 | }, 8 | get: key => { 9 | return Cookies.get(key); 10 | }, 11 | clearAll: () => { 12 | Cookies.remove("jwt"); 13 | Cookies.remove("email"); 14 | }, 15 | }; 16 | 17 | export default CookieManager; 18 | -------------------------------------------------------------------------------- /doto-frontend/src/constants/Categories.js: -------------------------------------------------------------------------------- 1 | import { pink, deepPurple, blue, green } from "@material-ui/core/colors"; 2 | 3 | export const categoryData = [ 4 | { 5 | text: "Homework", 6 | id: 1, 7 | color: pink, 8 | }, 9 | { 10 | text: "Work", 11 | id: 2, 12 | color: deepPurple, 13 | }, 14 | { 15 | text: "Household", 16 | id: 3, 17 | color: blue, 18 | }, 19 | { 20 | text: "Personal", 21 | id: 4, 22 | color: green, 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /doto-frontend/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 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Pages.css: -------------------------------------------------------------------------------- 1 | .page-layout { 2 | display: flex; 3 | max-width: 100%; 4 | } 5 | 6 | .left-side-bar { 7 | margin-top: 20vh; 8 | width: 20vh; 9 | border-radius: 0px 79px 0px 0px; 10 | max-height: 86vh; 11 | } 12 | 13 | .left-side-bg-blue { 14 | background-color: #3700b3; 15 | } 16 | 17 | .left-side-bg-green { 18 | background-color: #2e7d32; 19 | } 20 | 21 | .right-side-bg-green { 22 | background-color: #a5d6a7; 23 | } 24 | 25 | .right-side-bg-blue { 26 | background-color: #8d6cd9; 27 | } 28 | 29 | .content-container { 30 | display: flex; 31 | width: 100%; 32 | height: 100vh; 33 | flex-direction: column; 34 | justify-content: space-between; 35 | } 36 | -------------------------------------------------------------------------------- /doto-frontend/public/reminderWorker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("push", ev => { 2 | const { title, description, startDate: startDateIsoString } = ev.data.json(); 3 | const startDate = new Date(startDateIsoString); 4 | let options = { 5 | hour: "2-digit", 6 | minute: "2-digit", 7 | }; 8 | if (new Date().getDate() < startDate.getDate()) { 9 | // Include more info if start date is on future date 10 | options = { ...options, weekday: "long", month: "numeric", day: "numeric" }; 11 | } 12 | const etaTitle = `${title} at ${startDate.toLocaleTimeString("en-nz", options)}`; 13 | self.registration.showNotification(etaTitle, { 14 | body: description, 15 | icon: "/favicon.ico", 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /doto-frontend/public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CONTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | If you'd like your contributions to be displayed in the repo readme, do the following: 2 | 3 | In an issue or pull request, simply comment the following: 4 | 5 | `@all-contributors please add @[github username] for [contribution]` 6 | 7 | 8 | NOTE: Multiple contributions can be added at the same time. You can find the contribution types 9 | [here](https://allcontributors.org/docs/en/emoji-key). 10 | 11 | For example: 12 | 13 | `@all-contributors please add @tbenning for design` 14 | 15 | `@all-contributors please add @AlexanderTheGrape for bug design` 16 | 17 | The bot should then comment in the same issue/pull request with a link to a pull request 18 | it'll have made to the master branch. Get this checked and approved by others, and then 19 | you'll see your amazing contributions in the readme on github! 20 | -------------------------------------------------------------------------------- /doto-frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "extends": ["plugin:react/recommended", "standard", "plugin:prettier/recommended"], 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["react"], 20 | "rules": { 21 | "indent": 0, 22 | "quotes": "off", 23 | "space-before-function-paren": "off", 24 | "semi": "off", 25 | "comma-dangle": "off", 26 | "quote-props": "off", 27 | "react/prop-types": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Login/Login.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { mount } from "enzyme"; 4 | import Login from "./Login"; 5 | import renderer from "react-test-renderer"; 6 | 7 | describe(" component being rendered", () => { 8 | let subject; 9 | 10 | beforeEach(() => { 11 | subject = mount(); 12 | }); 13 | 14 | afterEach(() => { 15 | subject.unmount(); 16 | }); 17 | 18 | it("Login component rendered without crashing", () => { 19 | const div = document.createElement("div"); 20 | ReactDOM.render(, div); 21 | }); 22 | 23 | it("Make sure render matches snapshot", () => { 24 | const tree = renderer.create().toJSON(); 25 | expect(tree).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Bug Summary: 11 | 12 | 13 | --- 14 | 15 | 16 | ### Steps To Reproduce 17 | 18 | 1) 19 | 2) 20 | 21 | ### Expected behaviour 22 | 23 | A clear and concise description of what you expected to happen. 24 | 25 | ### Observed Behaviour 26 | 27 | A clear and concise description of what actually happened 28 | 29 | ### Screenshots 30 | 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | ### Environment 34 | 35 | - Version: 36 | - Operating System: 37 | - Browser (if any): 38 | 39 | 40 | --- 41 | 42 | ## Additional Information: 43 | 44 | ### Stack Traces 45 | 46 | ### Test Cases 47 | 48 | ### Code Examples 49 | 50 | ### Error Reports 51 | 52 | ### Other 53 | -------------------------------------------------------------------------------- /doto-backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "mocha": true 6 | }, 7 | "extends": ["standard", "plugin:mocha/recommended", "plugin:prettier/recommended"], 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["mocha"], 17 | "rules": { 18 | "indent": 0, 19 | "quotes": "off", 20 | "space-before-function-paren": "off", 21 | "semi": "off", 22 | "comma-dangle": "off" 23 | }, 24 | "overrides": [ 25 | { 26 | "files": ["**/*.test.js"], 27 | "rules": { 28 | "no-undef": "off" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /doto-frontend/src/components/ProductivityScore.css: -------------------------------------------------------------------------------- 1 | .makeStyles-paper-507 { 2 | padding: 0px !important; 3 | } 4 | 5 | .makeStyles-paper-2 { 6 | padding: 0px !important; 7 | } 8 | 9 | .forum-content { 10 | text-align: left; 11 | margin-left: 100px; 12 | } 13 | 14 | .name-field { 15 | font-size: 40px; 16 | } 17 | 18 | .drop-down { 19 | padding-top: 30px; 20 | width: 200px; 21 | } 22 | 23 | .text-area { 24 | margin-top: 20px !important; 25 | width: 350px; 26 | } 27 | 28 | .small-text-area { 29 | width: 223px; 30 | } 31 | 32 | .MuiSelect-selectMenu { 33 | width: 300px; 34 | } 35 | 36 | #add-button { 37 | text-align: right; 38 | padding-top: 40px; 39 | padding-bottom: 15px; 40 | padding-right: 15px; 41 | } 42 | 43 | .spacing { 44 | margin-top: 55px !important; 45 | } 46 | 47 | .group-spacing { 48 | margin-top: 10px !important; 49 | } 50 | -------------------------------------------------------------------------------- /doto-backend/src/config/token-setup.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const response = require("../constants/http-response"); 3 | function generateAccessToken(user) { 4 | return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET); 5 | } 6 | 7 | function authenticateToken(req, res, next) { 8 | const authHeader = req.headers.authorization; 9 | const token = authHeader && authHeader.split(" ")[1]; 10 | 11 | if (token == null) { 12 | return res.sendStatus(response.UNAUTHORIZED); 13 | } 14 | 15 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { 16 | if (err) { 17 | res.sendStatus(response.FORBIDDEN); 18 | } // Error if mismatch between user and token 19 | 20 | req.user = user; 21 | next(); 22 | }); 23 | } 24 | 25 | module.exports.generateAccessToken = generateAccessToken; 26 | module.exports.authenticateToken = authenticateToken; 27 | -------------------------------------------------------------------------------- /.github/workflows/doto_backend_deploy.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: doto-backend-deploy 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | - name: Set up Node.js version 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: '12.13.0' 21 | - name: yarn install and build 22 | run: | 23 | cd doto-backend 24 | yarn install --frozen-lockfile 25 | 26 | - name: 'Deploy to Azure Web App' 27 | uses: azure/webapps-deploy@v1 28 | with: 29 | app-name: 'doto-backend' 30 | slot-name: 'production' 31 | publish-profile: ${{ secrets.AzureAppService_PublishProfile_1a48d7d3d58a4327a6f6e034ff403ce4 }} 32 | package: ./doto-backend/ 33 | -------------------------------------------------------------------------------- /.github/workflows/doto_frontend_deploy.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: doto-frontend-deploy 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | 18 | - name: Set up Node.js version 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '12.13.0' 22 | 23 | - name: yarn install and build 24 | run: | 25 | cd doto-frontend 26 | yarn install 27 | yarn build 28 | 29 | - name: 'Deploy to Azure Web App' 30 | uses: azure/webapps-deploy@v1 31 | with: 32 | app-name: 'doto' 33 | slot-name: 'production' 34 | publish-profile: ${{ secrets.AzureAppService_PublishProfile_cff2daf75fd04dffbc6244cf8a4727cb }} 35 | package: ./doto-frontend/build 36 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Settings/SettingsPage.css: -------------------------------------------------------------------------------- 1 | .right-side-bar { 2 | border-radius: 79px 0px 0px 0px; 3 | height: 100vh; 4 | margin-top: 10px; 5 | margin-left: 5vw; 6 | display: flex; 7 | justify-content: flex-start; 8 | flex-direction: column; 9 | } 10 | 11 | .right-side-bg-green { 12 | background-color: #a5d6a7; 13 | } 14 | 15 | .right-side-bg-blue { 16 | background-color: #8d6cd9; 17 | } 18 | 19 | #input-field { 20 | width: 250px; 21 | margin-left: 10vw; 22 | margin-top: 4vh; 23 | text-align: left; 24 | } 25 | 26 | .profile-photo { 27 | width: 150px; 28 | margin-left: 10vw; 29 | margin-top: 4vh; 30 | height: 150px; 31 | } 32 | 33 | #color-palette { 34 | margin-top: 3vh; 35 | height: 40px; 36 | margin-left: 2vw; 37 | border-radius: 50px 50px 50px 50px; 38 | } 39 | 40 | .market-content-box { 41 | margin: 20 auto; 42 | display: inline-block; 43 | vertical-align: top; 44 | vertical-align: top; 45 | margin: 20px 50px; 46 | text-align: center; 47 | } 48 | -------------------------------------------------------------------------------- /doto-frontend/src/helpers/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | // This function checks if the user has logged in by checking if an email 2 | // is registered in the CookieManager. It then redirects to the home page if none present. 3 | // Authors: Alex Monk and Shunji Takano 4 | 5 | import React from "react"; 6 | import { Route, Redirect } from "react-router-dom"; 7 | import CookieManager from "./CookieManager"; 8 | 9 | function PrivateRoute({ component: Component, ...rest}) { 10 | let emailStr = CookieManager.get("email"); 11 | let authed = false; 12 | if (emailStr !== undefined) { 13 | authed = true; 14 | } 15 | 16 | return ( 17 | 20 | authed ? ( // Want to replace authed with Calender.js customAuth. 21 | 22 | ) : ( 23 | 24 | ) 25 | } 26 | /> 27 | ); 28 | } 29 | 30 | export default PrivateRoute; -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/Calendar.css: -------------------------------------------------------------------------------- 1 | .calendar-buttons { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 20vh 10px 0px 10px; 5 | } 6 | 7 | .calendar-component { 8 | margin: 10px 10px 10px 0px; 9 | height: 80vh; 10 | max-width: 100vw; 11 | overflow: hidden; 12 | } 13 | 14 | .list-view { 15 | width: 100%; 16 | margin-left: 10px; 17 | margin-right: 10vw; 18 | } 19 | 20 | .list-view-components { 21 | width: 35vw; 22 | display: flex; 23 | align-items: center; 24 | border-bottom: 1px solid #e2e8f0; 25 | } 26 | 27 | .isComplete { 28 | text-decoration: line-through; 29 | } 30 | 31 | .centered { 32 | font-size: larger; 33 | position: absolute; 34 | top: 65%; 35 | left: 50%; 36 | transform: translate(-50%, -50%); 37 | } 38 | 39 | .streak-container { 40 | position: relative; 41 | box-shadow: black; 42 | } 43 | 44 | .footer-container { 45 | display: flex; 46 | align-items: center; 47 | justify-content: space-between; 48 | width: 100%; 49 | margin-left: 11px; 50 | } 51 | -------------------------------------------------------------------------------- /doto-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import "typeface-roboto"; 5 | import "./index.css"; 6 | import Routes from "./routes/Route"; 7 | 8 | const RouteWrapper = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | ReactDOM.render(, document.getElementById("root")); 17 | 18 | /** 19 | * We disable the service worker from CRA and use a custom service worker 20 | * which will handle displaying notifications for reminders 21 | */ 22 | 23 | // serviceWorker.unregister(); 24 | 25 | Notification.requestPermission(perm => { 26 | if (perm === "granted" && "serviceWorker" in navigator) { 27 | window.addEventListener("load", () => { 28 | navigator.serviceWorker 29 | .register("reminderWorker.js") 30 | .then(reg => reg && reg.active && console.log("Service worker registered", reg)) 31 | .catch(console.err); 32 | }); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Doto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - [ ] The pull request is complete according to the following criteria: 2 | - [ ] Acceptance criteria have been met 3 | - [ ] The documentation is kept up-to-date 4 | - [ ] Comprehensive tests (if applicable) have been generated and all pass. 5 | - [ ] The pull request describes the changes that have been made, and enough information is present in the description for any developer to understand what has changed 6 | - [ ] Commits have been squashed (or will be on merge). 7 | - [ ] The branch name is descriptive and follows the pull request title format : {issue/bug...}/(Issue Number) - Name of issue. E.g bug/30-Fix-Project 8 | - [ ] The pull request title is of the following format : {issue/bug...}/(Issue Number) - Name of issue. E.g bug/30-Fix-Project 9 | - [ ] The description uses [github syntax](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) to link to the issue. E,g Resolves se701g2/Doto#{Number} 10 | - [ ] At least two reviewers assigned. One of which must be the assigner of the issue. 11 | - [ ] If there are merge conflicts, run git rebase as opposed to git merge with master. 12 | -------------------------------------------------------------------------------- /doto-frontend/src/components/UserStats.css: -------------------------------------------------------------------------------- 1 | .makeStyles-paper-507 { 2 | padding: 0px !important; 3 | } 4 | 5 | .makeStyles-paper-2 { 6 | padding: 0px !important; 7 | } 8 | 9 | .modal-p { 10 | background-image: url("./images//modal-background-purple.jpg"); 11 | background-size: cover; 12 | } 13 | 14 | .modal-g { 15 | background-image: url("./images//modal-background-green.jpg"); 16 | background-size: cover; 17 | } 18 | 19 | .stats-content { 20 | display: table; 21 | border-spacing: 10px; 22 | margin-left: 100px; 23 | margin-top: 30px; 24 | font-size: 20px; 25 | width: 400px; 26 | height: 450px; 27 | } 28 | 29 | .stats-row { 30 | display: table-row; 31 | } 32 | 33 | .stats-graphic { 34 | text-align: center; 35 | display: table-cell; 36 | width: 120px; 37 | } 38 | 39 | .stats-text { 40 | vertical-align: top; 41 | display: table-cell; 42 | text-align: left; 43 | font-size: 20px; 44 | } 45 | 46 | .title { 47 | text-align: center; 48 | font-size: 40px; 49 | } 50 | 51 | .spacing { 52 | margin-top: 55px !important; 53 | } 54 | 55 | .group-spacing { 56 | margin-top: 10px !important; 57 | } -------------------------------------------------------------------------------- /doto-backend/src/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | var uniqueValidator = require("mongoose-unique-validator"); 3 | 4 | // Schema for User objects 5 | // Refer to https://github.com/se701g2/Doto/wiki/Database-Schema for details 6 | const userschema = mongoose.Schema({ 7 | email: { 8 | // Email (obtained from Google's OAuth 2) is used as ID 9 | type: String, 10 | required: true, 11 | unique: true, 12 | }, 13 | name: { 14 | type: String, 15 | }, 16 | picture: { 17 | type: String, 18 | }, 19 | themePreference: { 20 | type: String, 21 | default: "dark", 22 | }, 23 | startTime: { 24 | type: Date, 25 | default: new Date(new Date().setHours(9, 0, 0, 0)), 26 | }, 27 | endTime: { 28 | type: Date, 29 | default: new Date(new Date().setHours(19, 0, 0, 0)), 30 | }, 31 | points: { 32 | type: Number, 33 | default: 0, 34 | }, 35 | unlockedItems: [ 36 | { 37 | type: String, 38 | }, 39 | ], 40 | }); 41 | 42 | userschema.plugin(uniqueValidator); 43 | module.exports = mongoose.model("user", userschema); 44 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Login/Login.css: -------------------------------------------------------------------------------- 1 | .SettingsPage { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .google-btn { 7 | /* width: 184px; 8 | height: 42px; */ 9 | width: 30vh; 10 | height: 6vh; 11 | background-color: #4285f4; 12 | border-radius: 2px; 13 | box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.25); 14 | cursor: pointer; 15 | } 16 | .google-btn .google-icon-wrapper { 17 | position: absolute; 18 | /* margin-top: 1px; 19 | margin-left: 1px; */ 20 | /* width: 40px; 21 | height: 40px; */ 22 | width: 6vh; 23 | height: 6vh; 24 | border-radius: 2px; 25 | background-color: #fff; 26 | } 27 | .google-btn .google-icon { 28 | /* margin-top: 11px; */ 29 | /* width: 18px; 30 | height: 18px; */ 31 | margin-top: 1vh; 32 | margin-left: 1vh; 33 | width: 4vh; 34 | height: 4vh; 35 | } 36 | .google-btn .btn-text { 37 | padding-top: 1vh; 38 | margin-left: 7vh; 39 | color: #fff; 40 | /* font-size: 14px; */ 41 | font-size: 2.5vh; 42 | letter-spacing: 0.2px; 43 | } 44 | .google-btn:hover { 45 | box-shadow: 0 0 6px #4285f4; 46 | } 47 | .google-btn:active { 48 | background: #1669f2; 49 | } 50 | -------------------------------------------------------------------------------- /doto-frontend/src/components/updateModal/UpdateModalContent.css: -------------------------------------------------------------------------------- 1 | .modal-p { 2 | background-image: url("../images/modal-background-purple.jpg"); 3 | background-size: cover; 4 | overflow-y: scroll; 5 | overflow-x: hidden; 6 | } 7 | 8 | .modal-g { 9 | background-image: url("../images/modal-background-green.jpg"); 10 | background-size: cover; 11 | overflow-y: scroll; 12 | overflow-x: hidden; 13 | } 14 | 15 | .makeStyles-paper-507 { 16 | padding: 0px !important; 17 | } 18 | 19 | .makeStyles-paper-2 { 20 | padding: 0px !important; 21 | } 22 | 23 | .forum-content { 24 | text-align: left; 25 | margin-left: 100px; 26 | height: 80vh; 27 | min-width: 100px; 28 | width: 300px; 29 | } 30 | 31 | .name-field { 32 | font-size: 40px; 33 | } 34 | 35 | .drop-down { 36 | padding-top: 10px; 37 | width: 200px; 38 | } 39 | 40 | .text-area { 41 | margin-top: 0px !important; 42 | width: 350px; 43 | } 44 | 45 | .small-text-area { 46 | width: 223px; 47 | } 48 | 49 | .MuiSelect-selectMenu { 50 | width: 300px; 51 | } 52 | 53 | #add-button { 54 | text-align: right; 55 | padding-top: 40px; 56 | padding-bottom: 15px; 57 | padding-right: 15px; 58 | } 59 | 60 | .spacing { 61 | margin-top: 0px !important; 62 | } 63 | 64 | .group-spacing { 65 | margin-top: 0px !important; 66 | } 67 | -------------------------------------------------------------------------------- /doto-frontend/src/components/ModalContent.css: -------------------------------------------------------------------------------- 1 | .modal-p { 2 | background-image: url("./images/modal-background-purple.jpg"); 3 | background-size: cover; 4 | overflow-y: scroll; 5 | overflow-x: hidden; 6 | } 7 | 8 | .modal-g { 9 | background-image: url("./images/modal-background-green.jpg"); 10 | background-size: cover; 11 | overflow-y: scroll; 12 | overflow-x: hidden; 13 | } 14 | 15 | .makeStyles-paper-507 { 16 | padding: 0px !important; 17 | } 18 | 19 | .makeStyles-paper-2 { 20 | padding: 0px !important; 21 | } 22 | 23 | .forum-content { 24 | text-align: left; 25 | margin-left: 100px; 26 | height: 80vh; 27 | min-width: 100px; 28 | width: 300px; 29 | } 30 | 31 | .name-field { 32 | font-size: 40px; 33 | } 34 | 35 | .drop-down { 36 | padding-top: 10px; 37 | width: 200px; 38 | } 39 | 40 | .text-area { 41 | margin-top: 0px !important; 42 | width: 350px; 43 | } 44 | 45 | .small-text-area { 46 | width: 223px; 47 | } 48 | 49 | .MuiSelect-selectMenu { 50 | width: 300px; 51 | } 52 | 53 | #add-button { 54 | position: relative; 55 | right: -20px; 56 | text-align: right; 57 | padding-top: 10px; 58 | padding-bottom: 10px; 59 | padding-right: 10px; 60 | padding-left: 10px; 61 | } 62 | 63 | .spacing { 64 | margin-top: 0px !important; 65 | } 66 | 67 | .group-spacing { 68 | margin-top: 0px !important; 69 | } 70 | -------------------------------------------------------------------------------- /doto-backend/src/routes/auth-route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const passport = require("passport"); 4 | const generateAccessToken = require("../config/token-setup").generateAccessToken; 5 | const url = process.env.FRONTEND_URL || "http://localhost:3000"; 6 | 7 | // This function is called when user is redirected upon login (uses passport-setup) 8 | router.get( 9 | "/google", 10 | passport.authenticate("google", { 11 | // Data accessible 12 | scope: ["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"], 13 | }), 14 | ); 15 | 16 | router.get( 17 | "/google/redirect", // Destination of redirect 18 | passport.authenticate("google", { session: false }), 19 | function (req, res) { 20 | const user = { email: req.user.email }; 21 | const email = req.user.email; // Request sent from done() function in passport-setup 22 | const buffer = Buffer.from(email); 23 | const encode = buffer.toString("base64"); 24 | 25 | const accessToken = generateAccessToken(user); 26 | 27 | // Once authorisation is done, the user is redirected to a frontend page. 28 | // Email and accessToken are parameters in the URL because redirect doesn't support sending responses 29 | res.redirect(url + "/calendar?email=" + encode + "&accessToken=" + accessToken); 30 | }, 31 | ); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /doto-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doto-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "precommit": "lint-staged", 8 | "test": "mocha", 9 | "start": "nodemon ./src/index.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "body-parser": "^1.19.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.2.0", 18 | "express": "^4.17.1", 19 | "express-winston": "^4.0.3", 20 | "jsonwebtoken": "^8.5.1", 21 | "mocha": "^7.1.0", 22 | "mongoose": "^5.9.4", 23 | "mongoose-unique-validator": "^2.0.3", 24 | "node-cron": "^2.0.3", 25 | "nodemon": "^2.0.2", 26 | "passport": "^0.4.1", 27 | "passport-google-oauth20": "^2.0.0", 28 | "swagger-ui-express": "^4.1.3", 29 | "web-push": "^3.4.3", 30 | "winston": "^3.2.1" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^6.8.0", 34 | "eslint-config-prettier": "^6.10.1", 35 | "eslint-config-standard": "^14.1.1", 36 | "eslint-plugin-import": "^2.20.1", 37 | "eslint-plugin-mocha": "^6.3.0", 38 | "eslint-plugin-node": "^11.1.0", 39 | "eslint-plugin-prettier": "^3.1.2", 40 | "eslint-plugin-promise": "^4.2.1", 41 | "eslint-plugin-standard": "^4.0.1", 42 | "lint-staged": "^10.0.9", 43 | "prettier": "^2.0.2" 44 | }, 45 | "lint-staged": { 46 | "*.js": "eslint --fix", 47 | "*.+(json|md)": "prettier --write" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /doto-backend/src/models/Task.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const uniqueValidator = require("mongoose-unique-validator"); 3 | 4 | // Schema for Task objects (which are associated with a User) 5 | // REQUIRED PROPERTIES: user, taskId, duration, startDate, endDate 6 | // Refer to https://github.com/se701g2/Doto/wiki/Database-Schema for details 7 | const taskSchema = mongoose.Schema({ 8 | user: { 9 | type: String, 10 | required: true, 11 | }, 12 | taskId: { 13 | type: String, 14 | required: true, 15 | unique: true, 16 | }, 17 | title: { 18 | type: String, 19 | required: true, 20 | }, 21 | description: { 22 | type: String, 23 | }, 24 | location: { 25 | type: String, 26 | }, 27 | priority: { 28 | type: Number, 29 | }, 30 | duration: { 31 | type: Number, 32 | required: true, 33 | }, 34 | startDate: { 35 | type: Date, 36 | required: true, 37 | }, 38 | endDate: { 39 | type: Date, 40 | required: true, 41 | }, 42 | reminderDate: { 43 | type: Date, 44 | }, 45 | isComplete: { 46 | type: Boolean, 47 | default: false, 48 | }, 49 | dueDate: { 50 | type: Date, 51 | required: true, 52 | }, 53 | travelTime: { 54 | type: Number, 55 | required: true, 56 | }, 57 | reminderType: { 58 | type: Number, 59 | required: false, 60 | }, 61 | earliestDate: { 62 | type: Date, 63 | required: true, 64 | }, 65 | category: { 66 | type: Number, 67 | }, 68 | }); 69 | 70 | taskSchema.plugin(uniqueValidator); 71 | 72 | module.exports = mongoose.model("task", taskSchema); 73 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/CalendarListView.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Calendar.css"; 3 | import PropTypes from "prop-types"; 4 | import { Checkbox, Typography } from "@material-ui/core"; 5 | import moment from "moment"; 6 | 7 | const isToday = ({ endDate }) => { 8 | const today = new Date(); 9 | return ( 10 | endDate.getYear() === today.getYear() && 11 | endDate.getMonth() === today.getMonth() && 12 | endDate.getDate() === today.getDate() 13 | ); 14 | }; 15 | 16 | // This file provides a checklist of items on today's to-do list. The user is able to select tasks completed for the day 17 | const CalendarListView = props => { 18 | return ( 19 |
20 |
Tasks for Today
21 | 22 | {props.tasks.filter(isToday).map(task => ( 23 |
24 | props.onTaskStatusUpdated(task.taskId)} 28 | /> 29 |
30 | {task.title} 31 | {!task.isComplete && ( 32 | {moment(task.startDate).fromNow()} 33 | )} 34 |
35 |
36 | ))} 37 |
38 | ); 39 | }; 40 | 41 | CalendarListView.propTypes = { 42 | tasks: PropTypes.array.isRequired, 43 | }; 44 | export default CalendarListView; 45 | -------------------------------------------------------------------------------- /doto-frontend/src/components/ProductivityScore.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import Slider from "@material-ui/core/Slider"; 5 | import "./ProductivityScore.css"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | root: { 9 | height: 400, 10 | width: 200, 11 | paddingBottom: 30, 12 | paddingTop: 30, 13 | }, 14 | title: { 15 | paddingLeft: 20, 16 | }, 17 | })); 18 | 19 | const ProductivityScore = props => { 20 | const classes = useStyles(); 21 | 22 | const marks = [ 23 | { 24 | value: 0, 25 | label: "Lazy", 26 | }, 27 | { 28 | value: 10, 29 | label: "Recovering Procrastinator", 30 | }, 31 | { 32 | value: 20, 33 | label: "Caffeine Machine", 34 | }, 35 | { 36 | value: 30, 37 | label: "Energy Drinkaholic", 38 | }, 39 | { 40 | value: 40, 41 | label: "Workhorse God", 42 | }, 43 | ]; 44 | 45 | return ( 46 | // Setting .css properties based on theme selected 47 | 48 |
49 | 50 | Productivity Mode 51 | 52 |
53 | 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default ProductivityScore; 67 | -------------------------------------------------------------------------------- /doto-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Doto 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /doto-backend/src/config/passport-setup.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const GoogleStrategy = require("passport-google-oauth20"); 3 | const User = require("../models/User"); 4 | const { logger } = require("../common/logging"); 5 | 6 | // This function applies the Google strategy to passport. Used in auth-route. 7 | passport.use( 8 | new GoogleStrategy( 9 | { 10 | callbackURL: "/auth/google/redirect", // User is directed here after successful login from Google login 11 | 12 | // Google API keys from dev account 13 | clientID: process.env.GOOGLE_API_CLIENT, 14 | clientSecret: process.env.GOOGLE_API_SECRET, 15 | 16 | // Callback function called after user login but before redirect 17 | // Gets additional info about user from Google. In this case, profile info from Google. 18 | }, 19 | (accessToken, refreshToken, profile, done) => { 20 | const email = profile.emails[0].value; 21 | var user; 22 | 23 | // Check if user already exists in database. Creates a new database entry if they don't exist. 24 | User.findOne({ email }).then((currentUser) => { 25 | if (currentUser) { 26 | logger.info("User already exists " + currentUser.email); 27 | user = currentUser; 28 | } else { 29 | user = new User({ 30 | email, 31 | name: profile._json.name, 32 | picture: profile._json.picture, 33 | }); 34 | 35 | user.save().then((newUser) => { 36 | logger.info("Created New User " + newUser.email); 37 | }); 38 | } 39 | 40 | // User Callback function complete, User is returned. 41 | done(null, user); 42 | }); 43 | }, 44 | ), 45 | ); 46 | -------------------------------------------------------------------------------- /doto-backend/src/routes/user-route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authenticateToken = require("../config/token-setup").authenticateToken; 4 | const User = require("../models/User"); 5 | const { logger } = require("../common/logging"); 6 | const response = require("../constants/http-response"); 7 | // GET User information 8 | router.get("/get", authenticateToken, function (req, res) { 9 | const email = req.user.email; 10 | User.find({ email: email }, function (err, userinfo) { 11 | if (err) { 12 | logger.error(err); 13 | res.status(response.BADREQUEST).json("Error: " + err); 14 | } else { 15 | if (userinfo.length === 0) { 16 | res.status(response.BADREQUEST).json("Error: could not find user with specified email."); 17 | } 18 | res.status(response.SUCCESSFUL).json(userinfo[0]); 19 | } 20 | }); 21 | }); 22 | 23 | // UPDATE User information 24 | router.put("/update", authenticateToken, function (req, res) { 25 | const email = req.user.email; 26 | User.updateOne({ email: email }, req.body, { new: true }, function (err, updatedUser) { 27 | logger.info(updatedUser); 28 | if (err || !updatedUser) { 29 | logger.error(err); 30 | res.status(response.BADREQUEST).json({ email: email, Successful: "False" }); 31 | } else { 32 | res.status(response.SUCCESSFUL).json({ email: email, Successful: "True" }); 33 | } 34 | }); 35 | }); 36 | 37 | // GET ALL Users in the system 38 | router.get("/email", function (req, res) { 39 | User.find({}, function (err, users) { 40 | if (err) { 41 | logger.error(err); 42 | res.status(response.BADREQUEST).json({ msg: "failed" }); 43 | } else { 44 | logger.info(users); 45 | res.status(response.SUCCESSFUL).json(users); 46 | } 47 | }); 48 | }); 49 | 50 | module.exports = router; 51 | -------------------------------------------------------------------------------- /doto-frontend/src/components/Streak.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import streakImage from "./images/streak.png"; 3 | 4 | class Streak extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { points: 0 }; 8 | } 9 | 10 | incrementStreak(change) { 11 | this.setState({ 12 | points: this.state.points + change, 13 | }); 14 | } 15 | 16 | resetStreak() { 17 | this.setState({ 18 | points: 0, 19 | }); 20 | } 21 | 22 | updateStreak() { 23 | const currentDateTime = new Date(); 24 | let latestDate = null; 25 | 26 | // find the last, previous uncompleted task 27 | this.props.tasks.forEach(task => { 28 | if (task.endDate < currentDateTime) { 29 | if (!task.isComplete) { 30 | if (!latestDate && latestDate < task.startDate) { 31 | latestDate = task.endDate; 32 | } 33 | } 34 | } 35 | }); 36 | 37 | // Steak = sum the values of every task completed since then 38 | this.resetStreak(); 39 | this.props.tasks.forEach(task => { 40 | if (!currentDateTime || task.endDate < currentDateTime) { 41 | if (task.startDate >= latestDate) { 42 | this.incrementStreak(1); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | componentDidMount() { 49 | // update streak every minute incase a previous task becomes overdue; streak would be set to 0 50 | this.interval = setInterval(() => this.updateStreak(), 1000 * 60); 51 | } 52 | 53 | render() { 54 | return ( 55 |
56 |

Streak

57 |
58 | {`streak: 59 | {this.state.points} 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | export default Streak; 67 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import DateRangeIcon from "@material-ui/icons/DateRange"; 4 | import SettingsIcon from "@material-ui/icons/Settings"; 5 | import ExitToAppIcon from "@material-ui/icons/ExitToApp"; 6 | import Tooltip from "@material-ui/core/Tooltip"; 7 | import { Grid } from "@material-ui/core" 8 | import "./Header.css"; 9 | import { Link } from "react-router-dom"; 10 | import CookieManager from "../../helpers/CookieManager"; 11 | 12 | // A common header file for the page titles with associated links to settings, calendar and logout pages. 13 | // This file takes the input of the page name as a prop. 14 | const Header = props => { 15 | return ( 16 | 44 | ); 45 | }; 46 | 47 | Header.propTypes = { 48 | title: PropTypes.string.isRequired, 49 | }; 50 | 51 | export default Header; 52 | -------------------------------------------------------------------------------- /doto-backend/test/user.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const mongoose = require("mongoose"); 4 | const UserModel = require("../src/models/User"); 5 | const assert = require("assert"); 6 | 7 | process.env.TEST_SUITE = "user-test"; 8 | 9 | describe("User Model Test", function () { 10 | before(async function () { 11 | await mongoose.connect( 12 | `mongodb://127.0.0.1:27017/${process.env.TEST_SUITE}`, 13 | { useNewUrlParser: true }, 14 | (err) => { 15 | if (err) { 16 | console.error(err); 17 | process.exit(1); 18 | } 19 | }, 20 | ); 21 | }); 22 | 23 | after(async function () { 24 | await mongoose.connection.dropDatabase(); 25 | await mongoose.connection.close(); 26 | }); 27 | 28 | it("create user and save successfully.", async function () { 29 | const validUser = new UserModel({ 30 | email: "john", 31 | picture: "profile.png", 32 | themePreference: "dark", 33 | }); 34 | const savedUser = await validUser.save(); 35 | assert(savedUser.email === "john"); 36 | }); 37 | 38 | it("gets user information", async function () { 39 | const userinfo = await UserModel.find({ email: "john" }); 40 | assert(userinfo[0].email === "john"); 41 | }); 42 | 43 | it("create user with same name & throws error.", async function () { 44 | const invalidUser = new UserModel({ 45 | email: "john", 46 | picture: "profile.png", 47 | themePreference: "light", 48 | }); 49 | 50 | await invalidUser.save(function (err) { 51 | assert(err.name === "ValidationError"); 52 | }); 53 | }); 54 | 55 | it("create user without required name field & throws error.", async function () { 56 | const invalidUser = new UserModel({ 57 | picture: "profile.png", 58 | themePreference: "dark", 59 | }); 60 | 61 | var error = invalidUser.validateSync(); 62 | assert.equal(error.errors.email.message, "Path `email` is required."); 63 | }); 64 | 65 | it("delete user successfully.", async function () { 66 | UserModel.remove({ user: "john" }).then((user) => { 67 | assert(user === null); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /doto-frontend/src/routes/Route.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { MemoryRouter } from "react-router"; 4 | import Route from "./Route"; 5 | import NotFound from "../components/pages/NotFound"; 6 | import SettingsPage from "../components/pages/Settings/SettingsPage"; 7 | import Calendar from "../components/pages/Calendar/Calendar"; 8 | import Login from "../components/pages/Login/Login"; 9 | import CookieManager from "../helpers/CookieManager"; 10 | 11 | test("initial landing page should be Login", () => { 12 | const wrapper = mount( 13 | 14 | 15 | , 16 | ); 17 | expect(wrapper.find(Login)).toHaveLength(1); 18 | }); 19 | 20 | test("Settings page should redirect to / without logging in first", () => { 21 | const wrapper = mount( 22 | 23 | 24 | , 25 | ); 26 | expect(wrapper.find(SettingsPage)).toHaveLength(0); 27 | expect(wrapper.find(Login)).toHaveLength(1); 28 | }); 29 | 30 | test("Calendar page should redirect to / without logging in first", () => { 31 | const wrapper = mount( 32 | 33 | 34 | , 35 | ); 36 | expect(wrapper.find(Calendar)).toHaveLength(0); 37 | expect(wrapper.find(Login)).toHaveLength(1); 38 | }); 39 | 40 | test("invalid path should redirect to 404", () => { 41 | const wrapper = mount( 42 | 43 | 44 | , 45 | ); 46 | expect(wrapper.find(NotFound)).toHaveLength(1); 47 | }); 48 | 49 | test("Calendar page should load when logged in", () => { 50 | CookieManager.set("email", "defunct_email@gmail.com"); 51 | const wrapper = mount( 52 | 53 | 54 | , 55 | ); 56 | expect(wrapper.find(Calendar)).toHaveLength(1); 57 | }); 58 | 59 | test("Settings page should load when logged in", () => { 60 | CookieManager.set("email", "defunct_email@gmail.com"); 61 | const wrapper = mount( 62 | 63 | 64 | , 65 | ); 66 | expect(wrapper.find(SettingsPage)).toHaveLength(1); 67 | }); 68 | -------------------------------------------------------------------------------- /doto-backend/src/webpush/reminder-service.js: -------------------------------------------------------------------------------- 1 | const cron = require("node-cron"); 2 | const Task = require("../models/Task"); 3 | const webpush = require("web-push"); 4 | const { logger } = require("../common/logging"); 5 | 6 | webpush.setVapidDetails("mailto:Se701group2@gmail.com", process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY); 7 | 8 | // Maps user.email to a push manager subscription so we know which client to 9 | // send a reminder to. 10 | const subscriptions = new Map(); 11 | 12 | // Every minute query the database to check if there are tasks that should be 13 | // fired off via web push. Note this means a notification will be delivered 14 | // one minute late in the worst case. 15 | cron.schedule("* * * * *", () => { 16 | Task.find( 17 | { reminderDate: { $lte: new Date() }, user: { $in: [...subscriptions.keys()] }, isComplete: false }, 18 | (err, tasks) => { 19 | if (err) { 20 | logger.error(err); 21 | return; 22 | } 23 | for (let task of tasks) { 24 | const subscription = subscriptions.get(task.user); 25 | if (subscription) { 26 | webpush 27 | .sendNotification(subscription, JSON.stringify(task)) 28 | .then(() => { 29 | logger.info(`Fired notification id=${task.id} title=${task.title}`); 30 | // This is a bit of a hack. 31 | // Unsetting the field means the notification is fired so we can avoid duplicating. 32 | task.reminderDate = undefined; 33 | task.save(); 34 | }) 35 | .catch((err) => { 36 | logger.error(err.stack); 37 | }); 38 | } else { 39 | logger.error("Subscription not found. This should never occur."); 40 | } 41 | } 42 | }, 43 | ); 44 | }); 45 | 46 | const subscribe = (id, subscription) => { 47 | if (typeof id === "string" && subscription && subscription.endpoint) { 48 | subscriptions.set(id, subscription); 49 | logger.info(`Registered subscription for ${id}`); 50 | } 51 | }; 52 | const unsubscribe = (id) => { 53 | subscriptions.delete(id); 54 | logger.info(`Removed subscription for ${id}`); 55 | }; 56 | 57 | module.exports = { subscribe, unsubscribe }; 58 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Settings/SettingsPage.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import renderer from "react-test-renderer"; 4 | import { BrowserRouter as Router } from "react-router-dom"; 5 | import SettingsPage from "./SettingsPage"; 6 | import { ThemeContext } from "../../../context/ThemeContext"; 7 | import { ActiveHoursContext } from "../../../context/ActiveHoursContext"; 8 | 9 | describe(" component being rendered", () => { 10 | let theme; 11 | let activeHourStartTime; 12 | let activeHourEndTime; 13 | const setTheme = jest.fn(); 14 | const setActiveHourStartTime = jest.fn(); 15 | const setActiveHourEndTime = jest.fn(); 16 | const useStateSpy = jest.spyOn(React, "useState"); 17 | useStateSpy.mockImplementation(theme => [theme, setTheme]); 18 | useStateSpy.mockImplementation(activeHourStartTime => [activeHourStartTime, setActiveHourStartTime]); 19 | useStateSpy.mockImplementation(activeHourEndTime => [activeHourEndTime, setActiveHourEndTime]); 20 | 21 | const Wrapper = () => { 22 | return ( 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | beforeEach(() => { 39 | useStateSpy.mockImplementation(theme => [theme, setTheme]); 40 | useStateSpy.mockImplementation(activeHourStartTime => [activeHourStartTime, setActiveHourStartTime]); 41 | useStateSpy.mockImplementation(activeHourEndTime => [activeHourEndTime, setActiveHourEndTime]); 42 | }); 43 | 44 | afterEach(() => { 45 | jest.clearAllMocks(); 46 | }); 47 | 48 | it("SettingsPage component rendered without crashing", () => { 49 | const div = document.createElement("div"); 50 | ReactDOM.render(, div); 51 | }); 52 | 53 | it("Make sure render matches snapshot", () => { 54 | const tree = renderer.create().toJSON(); 55 | expect(tree).toMatchSnapshot(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/TaskShifter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * shift tasks within working hours 3 | * 4 | * NOTE: This function assumes the duration of tasks is not exceeding the working hours 5 | * 6 | * @param {Array} scheduledTasks The scheduled tasks to be shifted, without considering working hours 7 | * @param {Date} startTime the start working hour 8 | * @param {Date} endTime the end working hour 9 | * @returns An array of tasks and their startDate and endDate are modified based on working hours. 10 | */ 11 | const shiftTasks = (scheduledTasks, startTime, endTime) => { 12 | // TODO: Take into account the active hours the user specifies in the settings menu 13 | const tasks = scheduledTasks; 14 | const MILLISECONDS_PER_MINUTE = 60000; 15 | 16 | // transforming startTime and endTime to minutes format 17 | const startActingHour = startTime.getHours() * 60 + startTime.getMinutes(); 18 | const endActingHour = endTime.getHours() * 60 + endTime.getMinutes(); 19 | 20 | for (let i = 0; i < tasks.length; i++) { 21 | const taskStart = tasks[i].startDate.getHours() * 60 + tasks[i].startDate.getMinutes(); 22 | const taskEnd = tasks[i].endDate.getHours() * 60 + tasks[i].endDate.getMinutes(); 23 | const shiftToTomorrow = startActingHour + 1440 - taskStart; 24 | 25 | // if the start time of task is earlier than start working time, then shift it and all tasks after it based on startActingHour - taskStart 26 | // if the end time of task is later than end working time, then shift it and all tasks after it based on startActingHour + 1440 - taskStart 27 | if (taskStart < startActingHour) { 28 | for (let j = i; j < tasks.length; j++) { 29 | tasks[j].startDate = new Date( 30 | tasks[j].startDate.getTime() + (startActingHour - taskStart) * MILLISECONDS_PER_MINUTE, 31 | ); 32 | tasks[j].endDate = new Date( 33 | tasks[j].endDate.getTime() + (startActingHour - taskStart) * MILLISECONDS_PER_MINUTE, 34 | ); 35 | } 36 | } else if (taskEnd > endActingHour) { 37 | for (let j = i; j < tasks.length; j++) { 38 | tasks[j].startDate = new Date(tasks[j].startDate.getTime() + shiftToTomorrow * MILLISECONDS_PER_MINUTE); 39 | tasks[j].endDate = new Date(tasks[j].endDate.getTime() + shiftToTomorrow * MILLISECONDS_PER_MINUTE); 40 | } 41 | } 42 | } 43 | return { 44 | shiftedTasks: tasks, 45 | }; 46 | }; 47 | export { shiftTasks }; 48 | -------------------------------------------------------------------------------- /doto-backend/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | require("dotenv").config(); 4 | require("./src/config/passport-setup.js"); 5 | const app = express(); 6 | const apiPort = process.env.PORT || 3001; 7 | const passport = require("passport"); 8 | const winston = require("winston"); 9 | const expressWinston = require("express-winston"); 10 | const { logger } = require("./src/common/logging"); 11 | 12 | // Mongoose connection 13 | const mongoose = require("mongoose"); 14 | // db connection string will point to Azure string only in production, fallbacks to dev database string 15 | const connectionString = process.env.AZURE_CONN || process.env.DEVELOPMENT_DB_CONN; 16 | // Add authentication strings only in production environment 17 | const connParams = { useNewUrlParser: true }; 18 | if (process.env.AZURE_USER && process.env.AZURE_PW) { 19 | connParams.auth = { 20 | user: process.env.AZURE_USER, 21 | password: process.env.AZURE_PW, 22 | }; 23 | } 24 | mongoose.connect(connectionString, connParams); 25 | const db = mongoose.connection; 26 | 27 | // Checking for DB connection 28 | db.once("open", function () { 29 | logger.info("Connected to MongoDB."); 30 | }); 31 | db.on("error", function () { 32 | logger.error("Database error"); 33 | }); 34 | 35 | // logging 36 | app.use( 37 | expressWinston.logger({ 38 | transports: [new winston.transports.Console()], 39 | format: winston.format.combine( 40 | winston.format.timestamp(), 41 | winston.format.colorize(), 42 | winston.format.printf( 43 | (info) => `${info.timestamp} [${info.level}] ${info.message} ${info.meta.res.statusCode}`, 44 | ), 45 | ), 46 | }), 47 | ); 48 | 49 | app.use(express.urlencoded({ extended: true })); 50 | app.use(cors()); 51 | app.use(express.json()); 52 | app.use(passport.initialize()); 53 | 54 | // exporting Routes 55 | const task = require("./src/routes/task-route"); 56 | app.use("/task", task); 57 | const user = require("./src/routes/user-route"); 58 | app.use("/user", user); 59 | const authRoute = require("./src/routes/auth-route"); 60 | app.use("/auth", authRoute); 61 | const reminders = require("./src/routes/reminder-route"); 62 | app.use("/reminders", reminders); 63 | 64 | // Setup reminder service 65 | require("./src/webpush/reminder-service"); 66 | 67 | // Swagger UI Setup 68 | const swaggerUi = require("swagger-ui-express"); 69 | const swaggerDocument = require("./swagger.json"); 70 | app.use("/", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); 71 | 72 | app.listen(apiPort, () => logger.info(`Server running on port ${apiPort}`)); 73 | -------------------------------------------------------------------------------- /doto-backend/web.config: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /doto-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doto-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@date-io/date-fns": "^1.3.11", 7 | "@devexpress/dx-react-core": "^2.6.2", 8 | "@devexpress/dx-react-scheduler": "^2.6.2", 9 | "@devexpress/dx-react-scheduler-material-ui": "^2.6.2", 10 | "@material-ui/core": "^4.9.5", 11 | "@material-ui/icons": "^4.9.1", 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.5.0", 14 | "@testing-library/user-event": "^7.2.1", 15 | "axios": "^0.19.2", 16 | "classnames": "^2.2.6", 17 | "date-fns": "^2.0.0-beta.5", 18 | "js-cookie": "^2.2.1", 19 | "material-ui-popup-state": "^1.5.4", 20 | "minimist": ">=1.2.2", 21 | "moment": "^2.24.0", 22 | "react": "^16.13.0", 23 | "react-dom": "^16.13.0", 24 | "react-minimal-pie-chart": "^7.3.1", 25 | "react-router-dom": "^5.1.2", 26 | "react-scripts": "3.4.0", 27 | "typeface-roboto": "0.0.75", 28 | "uuid": "^7.0.2" 29 | }, 30 | "scripts": { 31 | "precommit": "lint-staged", 32 | "start": "yarn run watch:css && react-scripts start", 33 | "build": "yarn run build:css && react-scripts build", 34 | "test": "react-scripts test --updateSnapshot", 35 | "eject": "react-scripts eject", 36 | "build:css": "postcss src/tailwind.css -o src/tailwind-generated.css", 37 | "watch:css": "postcss src/tailwind.css -o src/tailwind-generated.css" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "autoprefixer": "^9.7.4", 53 | "enzyme": "^3.11.0", 54 | "enzyme-adapter-react-16": "^1.15.2", 55 | "eslint": "^6.8.0", 56 | "eslint-config-prettier": "^6.10.1", 57 | "eslint-config-standard": "^14.1.0", 58 | "eslint-plugin-import": "^2.20.1", 59 | "eslint-plugin-node": "^11.0.0", 60 | "eslint-plugin-prettier": "^3.1.2", 61 | "eslint-plugin-promise": "^4.2.1", 62 | "eslint-plugin-react": "^7.19.0", 63 | "eslint-plugin-standard": "^4.0.1", 64 | "lint-staged": "^10.0.8", 65 | "postcss-cli": "^7.1.0", 66 | "prettier": "1.19.1", 67 | "pretty-quick": "^2.0.1", 68 | "tailwindcss": "^1.2.0" 69 | }, 70 | "lint-staged": { 71 | "*.+(js|jsx)": "eslint --fix", 72 | "*.+(html|css|json|md)": "prettier --write" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /doto-backend/src/routes/task-route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authenticateToken = require("../config/token-setup").authenticateToken; 4 | const Task = require("../models/Task"); 5 | const { logger } = require("../common/logging"); 6 | const response = require("../constants/http-response"); 7 | // GET ALL tasks for user 8 | router.get("/get", authenticateToken, (req, res) => { 9 | Task.find({ user: req.user.email }) 10 | .then((tasks) => res.status(response.SUCCESSFUL).json(tasks)) 11 | .catch((err) => res.status(response.BADREQUEST).json("Error: " + err)); 12 | }); 13 | 14 | // ADD task 15 | router.post("/post", authenticateToken, function (req, res) { 16 | const task = new Task(); 17 | task.user = req.user.email; 18 | task.taskId = req.body.taskId; 19 | task.title = req.body.title; 20 | task.description = req.body.description; 21 | task.location = req.body.location; 22 | task.priority = req.body.priority; 23 | task.duration = req.body.duration; 24 | task.startDate = req.body.startDate; 25 | task.endDate = req.body.endDate; 26 | task.reminderDate = req.body.reminderDate; 27 | task.travelTime = req.body.travelTime; 28 | task.reminderType = req.body.reminderType; 29 | task.dueDate = req.body.dueDate; 30 | task.earliestDate = req.body.earliestDate; 31 | task.category = req.body.category; 32 | 33 | task.save(function (err) { 34 | if (err) { 35 | logger.error(err); 36 | res.status(response.BADREQUEST).json({ taskId: req.params.taskId, Successful: "False" }); 37 | } else { 38 | res.status(response.SUCCESSFUL).json({ taskId: req.params.taskId, Successful: "True" }); 39 | } 40 | }); 41 | }); 42 | 43 | // UPDATE task 44 | router.put("/:taskId", authenticateToken, function (req, res) { 45 | Task.updateOne({ taskId: req.params.taskId }, req.body, { new: true }, function (err, updatedTask) { 46 | logger.info(updatedTask); 47 | if (err || !updatedTask) { 48 | logger.error(err); 49 | res.status(response.BADREQUEST).json({ taskId: req.params.taskId, Successful: "False" }); 50 | } else { 51 | res.status(response.SUCCESSFUL).json({ taskId: req.params.taskId, Successful: "True" }); 52 | } 53 | }); 54 | }); 55 | 56 | // DELETE task 57 | router.delete("/:taskId", authenticateToken, function (req, res) { 58 | Task.remove({ taskId: req.params.taskId }, function (err) { 59 | if (err) { 60 | logger.error(err); 61 | res.status(400).json({ taskId: req.params.taskId, Deleted: "False" }); 62 | } else { 63 | res.status(response.SUCCESSFUL).json({ taskId: req.params.taskId, Deleted: "True" }); 64 | } 65 | }); 66 | }); 67 | 68 | module.exports = router; 69 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Login.css"; 3 | 4 | const DotoTitle = () => { 5 | return ( 6 |
7 | Doto 8 |
9 | ); 10 | }; 11 | 12 | const BlueBubble = () => { 13 | const url = 14 | process.env.NODE_ENV === "development" ? "http://localhost:3001" : "https://doto-backend.azurewebsites.net"; 15 | return ( 16 |
20 |
21 |
22 | Your life 23 |
24 |
25 | Planned for you 26 |
27 | 43 | 44 | 49 |
50 |
51 | ); 52 | }; 53 | 54 | const PurpleBubble = () => { 55 | return ( 56 |
57 | ); 58 | }; 59 | 60 | const Login = () => { 61 | return ( 62 |
63 |
64 | 65 | 66 |
67 | 68 |
69 | ); 70 | }; 71 | 72 | export default Login; 73 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Login/__snapshots__/Login.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component being rendered Make sure render matches snapshot 1`] = ` 4 |
7 |
10 |
18 | 21 | Doto 22 | 23 |
24 |
33 |
36 |
37 | 40 | Your life 41 | 42 |
43 |
44 | 47 | Planned for you 48 | 49 |
50 | 78 | 91 |
92 |
93 |
94 |
103 |
104 | `; 105 | -------------------------------------------------------------------------------- /.github/workflows/doto_ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: doto-CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | backend: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x] 19 | mongodb-version: [4.2] 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: Setup Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Start MongoDB ${{ matrix.mongodb-version }} 31 | uses: supercharge/mongodb-github-action@1.1.0 32 | with: 33 | mongodb-version: ${{ matrix.mongodb-version }} 34 | 35 | - name: Get Yarn Cache 36 | id: yarn-cache-dir-path 37 | run: echo "::set-output name=dir::$(yarn cache dir)" 38 | 39 | - name: Cache node_modules for Yarn 40 | uses: actions/cache@v1 41 | id: yarn-cache 42 | with: 43 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 44 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/doto-backend/yarn.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-yarn-- 47 | 48 | - name: Yarn Install 49 | working-directory: ./doto-backend 50 | run: yarn install --frozen-lockfile 51 | 52 | - name: Yarn Tests 53 | working-directory: ./doto-backend 54 | run: CI=true yarn test --passWithNoTests 55 | 56 | frontend: 57 | runs-on: ubuntu-latest 58 | 59 | strategy: 60 | matrix: 61 | node-version: [12.x] 62 | 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v2 66 | 67 | - name: Setup Node.js ${{ matrix.node-version }} 68 | uses: actions/setup-node@v1 69 | with: 70 | node-version: ${{ matrix.node-version }} 71 | 72 | - name: Get Yarn Cache 73 | id: yarn-cache-dir-path 74 | run: echo "::set-output name=dir::$(yarn cache dir)" 75 | 76 | - name: Cache node_modules for Yarn 77 | uses: actions/cache@v1 78 | id: yarn-cache 79 | with: 80 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 81 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/doto-frontend/yarn.lock') }} 82 | restore-keys: | 83 | ${{ runner.os }}-yarn-- 84 | 85 | - name: Yarn Install 86 | working-directory: ./doto-frontend 87 | run: yarn install --frozen-lockfile 88 | 89 | - name: Yarn Build 90 | working-directory: ./doto-frontend 91 | run: yarn build 92 | 93 | - name: Yarn Tests 94 | working-directory: ./doto-frontend 95 | run: CI=true yarn test --passWithNoTests 96 | 97 | -------------------------------------------------------------------------------- /doto-frontend/src/components/AvailableTheme.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@material-ui/core"; 3 | import { ThemeProvider } from "@material-ui/core/styles"; 4 | import lockImage from "./images/lock.png"; 5 | import VpnKeyIcon from "@material-ui/icons/VpnKey"; 6 | import PopupState, { bindToggle, bindPopper } from "material-ui-popup-state"; // added dependency 7 | import Fade from "@material-ui/core/Fade"; 8 | import Paper from "@material-ui/core/Paper"; 9 | import Popper from "@material-ui/core/Popper"; 10 | 11 | const AvailableTheme = props => { 12 | const handleClick = () => { 13 | if (!props.locked) { 14 | props.handleThemeClick(props.colour, props.cost); 15 | } else { 16 | props.buyItem(props.cost, props.colour); 17 | } 18 | }; 19 | 20 | return ( 21 |
22 | 23 | {props.locked ? ( 24 | 25 | {popupState => ( 26 |
27 | 34 | 35 | {({ TransitionProps }) => ( 36 | 37 | 38 | 44 | 45 | 46 | )} 47 | 48 |
49 | )} 50 |
51 | ) : ( 52 |
62 | ); 63 | }; 64 | 65 | export default AvailableTheme; 66 | -------------------------------------------------------------------------------- /wiki_guidelines.md: -------------------------------------------------------------------------------- 1 | # Wiki contribution guidelines 2 | 3 | ## Purpose 4 | The wiki is our main source of documentation on the project. For newcomers to the project as well as existing contributors, the wiki should contain comprehensive information about each aspect of the project. As the project grows, the wiki grows with it and is pivotal in the maintenance of the project. 5 | 6 | ## Wiki Structure 7 | We have a tree structure for our wiki, with relevant pages bundled together under the same cateogry. 8 | * In the Home page you will find a list of contributors to the project. 9 | * In the home section, you will find our code of conduct while working within the team as well as the overall timeline of the project. 10 | * In the team section there will be a list of meeting minutes as well as the contributions each team member has made. 11 | * A best practices section describes our strategy for the git workflow, issue writing, pull request guidelines, commenting guidelines and other best practices developers contributing to the project should follow. 12 | * The tech section describes the technology stack used in the project and the reasoning behind our approach. As well other implementation details pertaining to the project. 13 | * A single future works page will contain work that has not been started but could be something that future contributors could work on. 14 | 15 | 16 | ### Meeting minutes format 17 | Meeting minutes format should be standardised and should include: 18 | * Date 19 | * Agenda 20 | * Members present 21 | * Summary of meeting 22 | 23 | ### Documenting contributions 24 | When documenting contributions to the project, each contributor should be fairly credited with the work they have accomplished. So the contributions page should include a short description of the different work each team member contributed (both technical or non-technical). The contributions page should be split up into the different parts of the project for example. 25 | 26 | ``` 27 | DevOps 28 | - Casey: I worked on x feature, and y feature and helped on z feature. 29 | - Reshad: I worked on m feature and helped with x feature 30 | ... 31 | 32 | Frontend 33 | - Jason: I helped build s feature and also worked on p feature 34 | - Lucy: I worked on p feature and w feature 35 | ... 36 | 37 | Backend 38 | ... 39 | 40 | Documentation 41 | ... 42 | 43 | And so on.. 44 | ``` 45 | 46 | ## Future work 47 | If you are suggesting future work that someone could work on, it is important that the description of the work is clear what the work entails. Additionally, details on why the feature should be worked on and how to approach it should be given. For example: 48 | 49 | ``` 50 | Feature description: I want to add x feature to the front end 51 | Because: I think this feature will help improve y feature 52 | Feasibility: I think this is possible if we do y and z to the backend 53 | ``` 54 | 55 | ## Code of conduct when contributing 56 | Before you contribute to the wiki it is important you have reviewed our code of conduct. All contributors are expected to contribute while treating their fellow contributors with respect. 57 | 58 | ## Writing style 59 | Some important writing guidelines when contributing include: 60 | * Writing in first person but avoid using singular pronouns e.g. "we implemented y using ... " 61 | * Writing with proper grammar and spelling 62 | * Maintaining a level of professional etiquette while contributing 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/Calendar.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { mount, shallow } from "enzyme"; 4 | import { BrowserRouter as Router } from "react-router-dom"; 5 | import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted"; 6 | import CalendarTodayIcon from "@material-ui/icons/CalendarToday"; 7 | import Calendar from "./Calendar"; 8 | import CalendarListView from "./CalendarListView"; 9 | import { ThemeContext } from "../../../context/ThemeContext"; 10 | import { ActiveHoursContext } from "../../../context/ActiveHoursContext"; 11 | 12 | describe(" component being rendered", () => { 13 | let theme; 14 | let activeHourStartTime; 15 | let activeHourEndTime; 16 | const setTheme = jest.fn(); 17 | const setActiveHourStartTime = jest.fn(); 18 | const setActiveHourEndTime = jest.fn(); 19 | const useStateSpy = jest.spyOn(React, "useState"); 20 | useStateSpy.mockImplementation(theme => [theme, setTheme]); 21 | useStateSpy.mockImplementation(activeHourStartTime => [activeHourStartTime, setActiveHourStartTime]); 22 | useStateSpy.mockImplementation(activeHourEndTime => [activeHourEndTime, setActiveHourEndTime]); 23 | 24 | const Wrapper = () => { 25 | return ( 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | beforeEach(() => { 42 | useStateSpy.mockImplementation(theme => [theme, setTheme]); 43 | useStateSpy.mockImplementation(activeHourStartTime => [activeHourStartTime, setActiveHourStartTime]); 44 | useStateSpy.mockImplementation(activeHourEndTime => [activeHourEndTime, setActiveHourEndTime]); 45 | }); 46 | 47 | afterEach(() => { 48 | jest.clearAllMocks(); 49 | }); 50 | 51 | it("Calendar component rendered without crashing", () => { 52 | const div = document.createElement("div"); 53 | ReactDOM.render(, div); 54 | }); 55 | 56 | it("Make sure render matches snapshot", () => { 57 | const tree = shallow(); 58 | expect(tree.debug()).toMatchSnapshot(); 59 | tree.unmount(); 60 | }); 61 | 62 | it("Click on List View button should open CalendarListView while changing its icon", () => { 63 | const subject = mount(); 64 | 65 | const button = () => { 66 | // search for List View button 67 | return subject.find('button[title="List View"]'); 68 | }; 69 | 70 | expect(subject.find(CalendarListView)).toHaveLength(0); 71 | expect(button().find(FormatListBulletedIcon)).toHaveLength(1); 72 | expect(button().find(CalendarTodayIcon)).toHaveLength(0); 73 | 74 | button().simulate("click"); 75 | 76 | expect(subject.find(CalendarListView)).toHaveLength(1); 77 | expect(button().find(FormatListBulletedIcon)).toHaveLength(0); 78 | expect(button().find(CalendarTodayIcon)).toHaveLength(1); 79 | 80 | subject.unmount(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /doto-frontend/src/components/UserStats.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Themes } from "../constants/Themes"; 3 | import PieChart from "react-minimal-pie-chart"; 4 | import StarIcon from "@material-ui/icons/Star"; 5 | import AccessTimeIcon from "@material-ui/icons/AccessTime"; 6 | import "./UserStats.css"; 7 | 8 | const UserStats = props => { 9 | var highColor = "#2F7D32"; 10 | var medColor = "#3bb300"; 11 | var lowColor = "#41d900"; 12 | if (props.modalBackground === Themes.DARK) { 13 | highColor = "#3700b3"; 14 | medColor = "#6c00d3"; 15 | lowColor = "#cf00ff"; 16 | } 17 | var highPriority = props.priorityStats[0]; 18 | var medPriority = props.priorityStats[1]; 19 | var lowPriority = props.priorityStats[2]; 20 | var total = highPriority + medPriority + lowPriority; 21 | return ( 22 | // Setting .css properties based on theme selected 23 | 24 |
25 |
26 |

27 | Your Stats 28 |

29 |
30 |
31 |
32 |
33 | 41 |
42 |
43 |

44 | {total} Tasks Completed 45 |

46 |

47 | High Priority: 48 | 49 | {highPriority} 50 | 51 |

52 |

53 | Medium Priority: 54 | 55 | {medPriority} 56 | 57 |

58 |

59 | Low Priority: 60 | 61 | {lowPriority} 62 | 63 |

64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |

72 | {props.hoursWorked} Hours Worked 73 |

74 |
75 |
76 |
77 |
78 | 79 |
80 |
81 |

82 | 1-Day Record: {props.dayRecord} 83 |

84 |
85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default UserStats; 92 | -------------------------------------------------------------------------------- /doto-frontend/src/routes/Route.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import SettingsPage from "../components/pages/Settings/SettingsPage"; 4 | import Login from "../components/pages/Login/Login"; 5 | import Calendar from "../components/pages/Calendar/Calendar"; 6 | import NotFound from "../components/pages/NotFound"; 7 | import { ThemeContext } from "../context/ThemeContext"; 8 | import { ActiveHoursContext } from "../context/ActiveHoursContext"; 9 | import CookieManager from "../helpers/CookieManager"; 10 | import "../tailwind-generated.css"; 11 | import PrivateRoute from "../helpers/PrivateRoute"; 12 | import DotoService from "../helpers/DotoService"; 13 | 14 | /** 15 | * When the user is redirected after they are logged in to their google account 16 | * We take the current url and extract the Email and JWT Token from the query parameters 17 | * 18 | * There was also a weird issue where the JWT token with emails ending with "gmail.com" had a 19 | * "#" character at the end but the emails ending with "aucklanduni.ac.nz" did not have this issue 20 | * and that is why we are doing a hacky check to see if the email domain starts with "a". 21 | */ 22 | const extractEmailAndJwt = url => { 23 | const [endPoint, queryParams] = url.split("/")[3].split("?"); 24 | if (endPoint !== "calendar" || !queryParams) return; 25 | const [base64Email, jwt] = queryParams.split("&").map(param => param.split("=")[1]); 26 | const email = atob(base64Email); 27 | 28 | const isUoAEmail = email.split("@")[1].substring(0, 1) === "a"; 29 | return [email, isUoAEmail ? jwt : jwt.substring(0, jwt.length - 1)]; 30 | }; 31 | 32 | // Saving the email and jwt cookies to the current session 33 | const saveToCookies = params => { 34 | if (!params) return; 35 | const [email, jwt] = params; 36 | CookieManager.set("email", email); 37 | CookieManager.set("jwt", jwt); 38 | }; 39 | 40 | // Boilerplate from https://www.npmjs.com/package/web-push#using-vapid-key-for-applicationserverkey 41 | const urlBase64ToUint8Array = base64String => { 42 | const padding = "=".repeat((4 - (base64String.length % 4)) % 4); 43 | const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); 44 | 45 | const rawData = window.atob(base64); 46 | const outputArray = new Uint8Array(rawData.length); 47 | 48 | for (let i = 0; i < rawData.length; ++i) { 49 | outputArray[i] = rawData.charCodeAt(i); 50 | } 51 | return outputArray; 52 | }; 53 | 54 | const setupReminders = async params => { 55 | if (!params) return; 56 | const registration = await navigator.serviceWorker.getRegistration(); 57 | if (registration && registration.active) { 58 | const subscription = await registration.pushManager.subscribe({ 59 | userVisibleOnly: true, 60 | applicationServerKey: urlBase64ToUint8Array(process.env.REACT_APP_VAPID_PUBLIC_KEY), 61 | }); 62 | DotoService.subscribeToReminders(subscription); 63 | } 64 | }; 65 | 66 | // Sets the routing to the appropriate pages, passing in the colour theme based on user setting 67 | const Routes = () => { 68 | const [theme, setTheme] = React.useState(true); 69 | const [activeHourStartTime, setActiveHourStartTime] = React.useState(new Date()); 70 | const [activeHourEndTime, setActiveHourEndTime] = React.useState(new Date()); 71 | // Only when backend returns JWT and email then we save 72 | const params = extractEmailAndJwt(window.location.href); 73 | saveToCookies(params); 74 | setupReminders(params); 75 | return ( 76 | 77 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default Routes; 98 | -------------------------------------------------------------------------------- /doto-frontend/src/helpers/DotoService.js: -------------------------------------------------------------------------------- 1 | import CookieManager from "./CookieManager"; 2 | import axios from "axios"; 3 | 4 | // This file integrates the front-end and back-end together using GET and POST methods. 5 | const baseUrl = 6 | process.env.NODE_ENV === "development" ? "http://localhost:3001" : "https://doto-backend.azurewebsites.net"; 7 | 8 | const taskMapper = data => { 9 | return { 10 | taskId: data.taskId, 11 | id: data.taskId, 12 | title: data.title, 13 | startDate: new Date(data.startDate), 14 | endDate: new Date(data.endDate), 15 | duration: data.duration, 16 | travelTime: data.travelTime, 17 | reminderType: data.reminderType, 18 | dueDate: data.dueDate, 19 | reminderDate: data.reminderDate, 20 | ...(data.description && { description: data.description }), 21 | ...(data.priority && { priority: data.priority }), 22 | ...(data.category && { category: data.category }), 23 | ...(data.location && { location: data.location }), 24 | isComplete: data.isComplete, 25 | earliestDate: new Date(data.earliestDate), 26 | }; 27 | }; 28 | 29 | const DotoService = { 30 | getTasks: async () => { 31 | const path = baseUrl + "/task/get"; 32 | 33 | try { 34 | const response = await axios.get(path, { 35 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 36 | }); 37 | 38 | const tasks = response.data.map(task => taskMapper(task)); 39 | return tasks; 40 | } catch (e) { 41 | console.log(e); 42 | } 43 | }, 44 | updateTask: async task => { 45 | // Strip the 'id' property because its only needed by dev-express scheduler 46 | const { id, ...mongoTask } = task; 47 | const updatedTask = { 48 | user: CookieManager.get("email"), 49 | ...mongoTask, 50 | }; 51 | await axios({ 52 | method: "put", 53 | url: baseUrl + `/task/${task.taskId}`, 54 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 55 | data: updatedTask, 56 | }); 57 | 58 | // TODO: add error handling 59 | }, 60 | setNewTask: async task => { 61 | const newTask = { 62 | user: CookieManager.get("email"), 63 | taskId: task.taskId, 64 | title: task.title, 65 | dueDate: task.dueDate.toString(), 66 | startDate: task.startDate.toString(), 67 | endDate: task.endDate.toString(), 68 | duration: task.duration, 69 | travelTime: task.travelTime, 70 | reminderType: task.reminderType, 71 | ...(task.reminderDate && { reminderDate: task.reminderDate.toString() }), 72 | ...(task.description && { description: task.description }), 73 | ...(task.priority && { priority: task.priority }), 74 | ...(task.category && { category: task.category }), 75 | ...(task.location && { location: task.location }), 76 | isComplete: false, 77 | earliestDate: task.earliestDate.toString(), 78 | }; 79 | 80 | axios({ 81 | method: "post", 82 | url: baseUrl + "/task/post", 83 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 84 | data: newTask, 85 | }); 86 | 87 | // TODO: catch for errors depending if it didn't post properly or maybe retry mechanism 88 | }, 89 | deleteTask: async taskId => { 90 | await axios({ 91 | method: "delete", 92 | url: baseUrl + `/task/${taskId}`, 93 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 94 | }); 95 | }, 96 | getUserInfo: async () => { 97 | const path = baseUrl + "/user/get"; 98 | 99 | try { 100 | const response = await axios.get(path, { 101 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 102 | }); 103 | const userInfo = response.data; 104 | return userInfo; 105 | } catch (e) { 106 | console.log(e); 107 | } 108 | }, 109 | updateUserInfo: async userInfo => { 110 | const updatedUserInfo = { 111 | user: CookieManager.get("email"), 112 | ...userInfo, 113 | }; 114 | 115 | await axios({ 116 | method: "put", 117 | url: baseUrl + "/user/update", 118 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 119 | data: updatedUserInfo, 120 | }); 121 | 122 | // TODO: catch for errors depending if it didn't post properly or maybe retry mechanism 123 | }, 124 | subscribeToReminders: async subscription => { 125 | await axios({ 126 | method: "post", 127 | url: baseUrl + "/reminders/subscribe", 128 | headers: { Authorization: "Bearer " + CookieManager.get("jwt") }, 129 | data: subscription, 130 | }); 131 | }, 132 | }; 133 | 134 | export default DotoService; 135 | -------------------------------------------------------------------------------- /doto-frontend/src/components/MarketPlace.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./MarketPlace.css"; 3 | import AvailableTheme from "./AvailableTheme"; 4 | 5 | // @ref https://www.w3schools.com/colors/colors_names.asp 6 | const themeCost = { 7 | blue: 0, 8 | green: 20, 9 | gray: 300, 10 | magenta: 100, 11 | purple: 200, 12 | crimson: 300, 13 | black: 400, 14 | red: 500, 15 | darkSeaGreen: 600, 16 | antiqueWhite: 700, 17 | darkKhaki: 800, 18 | darkSlateBlue: 900, 19 | }; 20 | 21 | const MarketPlace = props => { 22 | const unlockedItems = new Set(props.unlockedItems); 23 | return ( 24 |
25 |
26 | 34 | 42 | 50 | 58 | 59 |

60 | 61 | 69 | 77 | 85 | 93 | 94 |

95 | 96 | 104 | 112 | 120 | 128 |
129 |
130 | ); 131 | }; 132 | 133 | export default MarketPlace; 134 | -------------------------------------------------------------------------------- /doto-frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Libraries & Toolchains 41 | 42 | The frontend system utilizes multiple libraries and tools that provide benefits in different aspects of the user interface. When committing to the frontend repository these tools should be used properly and where applicable. The content below gives short descriptions of the tooling. 43 | 44 | ### JEST & Enzyme Testing 45 | 46 | Both of these utilities provide an isolated JavaScript testing framework that is used for frontend testing. 47 | 48 | Implementation example: 49 | 50 | ``` 51 | test("Settings page should be loaded correctly", () => { 52 | const wrapper = mount( 53 | 54 | 55 | , 56 | ); 57 | expect(wrapper.find(SettingsPage)).toHaveLength(1); 58 | }); 59 | ``` 60 | 61 | All new components should include relevant tests using the thes two toolchains. 62 | 63 | More documentation [here](https://jestjs.io/) & [here](https://enzymejs.github.io/enzyme/) 64 | 65 | ### Material UI 66 | 67 | To keep the UI consistent the team has used the Material UI library where possible. 68 | Implementation example: 69 | 70 | ``` 71 | 77 | ``` 78 | 79 | Where possible, material ui components should be adopted and implemented according to the provided documentation. 80 | 81 | More documentation [here](https://material-ui.com/) 82 | 83 | ### React Router 84 | 85 | React router is a common library used to handle routing between pages in a webapp. 86 | 87 | Implementation example: 88 | 89 | ``` 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ``` 98 | 99 | New pages that are added should following the same routing structure setup in `Route.js`. 100 | 101 | More documentation [here](https://reacttraining.com/react-router/web/guides/quick-start) 102 | 103 | ### Tailwind 104 | 105 | Tailwind is a CSS framework that the team has used to add consistency styling. 106 | 107 | Implementation example: 108 | 109 | ``` 110 |

Calendar

111 |
Support
112 | ``` 113 | 114 | The root level `tailwind.js` file contains a list of the different CSS variants. 115 | 116 | More documentation [here](https://tailwindcss.com/) 117 | 118 | ## Learn More 119 | 120 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 121 | 122 | To learn React, check out the [React documentation](https://reactjs.org/). 123 | 124 | ### Code Splitting 125 | 126 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 127 | 128 | ### Analyzing the Bundle Size 129 | 130 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 131 | 132 | ### Making a Progressive Web App 133 | 134 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 135 | 136 | ### Advanced Configuration 137 | 138 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 139 | 140 | ### Deployment 141 | 142 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 143 | 144 | ### `yarn build` fails to minify 145 | 146 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify (despite referring to `npm`, it can still be useful) 147 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/TaskScheduler.js: -------------------------------------------------------------------------------- 1 | import { shiftTasks } from "./TaskShifter"; 2 | const MILLISECONDS_PER_MINUTE = 60000; 3 | 4 | /** 5 | * Takes in a single new unscheduled task as well as a list of existing scheduled tasks and returns 6 | * a newly-scheduled, full list of tasks. The existing tasks may have their schedules modified. 7 | * 8 | * NOTE: This function assumes existingTasks are ordered chronologically by startDate, i.e. have been 9 | * processed using this function. 10 | * 11 | * @param {object} newTasks The new tasks to schedule, with no start/end datetimes present 12 | * @param {Array} existingTasks The existing scheduled tasks, with start/end datetimes present 13 | * @returns A chronologically ordered (based on startDate) array of all tasks scheduled with start/end 14 | * datetimes - essentially existingTasks + newTasks 15 | */ 16 | const addTaskToSchedule = (newTask, existingTasks, startTime, endTime) => { 17 | // TODO: Take into account any possible gap between datetime and startDate of the first task in oldTasks 18 | // TODO: Take into account priority of tasks 19 | // TODO: Take into account location of tasks, add time gaps to allow for travel 20 | // TODO: Take into account working hour restriction 21 | 22 | const competingTasks = []; 23 | const oldTasks = []; 24 | 25 | // filter existing tasks to get tasks with a startDate > earliestDate 26 | existingTasks.forEach(task => { 27 | task.startDate > newTask.earliestDate ? competingTasks.push(task) : oldTasks.push(task); 28 | }); 29 | 30 | // If the endDate of the latest oldTask is after earliest start date, then the earliest any new task can be scheduled is oldTask.endDate 31 | const minDate = 32 | (oldTasks[0] && oldTasks[oldTasks.length - 1].endDate) > newTask.earliestDate 33 | ? oldTasks[oldTasks.length - 1].endDate 34 | : newTask.earliestDate; 35 | 36 | let cTask; 37 | 38 | const earliestPossibleEnd = new Date( 39 | newTask.earliestDate.getTime() + 40 | newTask.duration * MILLISECONDS_PER_MINUTE + 41 | newTask.travelTime * MILLISECONDS_PER_MINUTE, 42 | ); 43 | 44 | if (competingTasks.length > 0) { 45 | if (earliestPossibleEnd < competingTasks[0].startDate && newTask.dueDate < competingTasks[0].dueDate) { 46 | newTask.startDate = new Date(newTask.earliestDate.getTime()); 47 | newTask.endDate = earliestPossibleEnd; 48 | 49 | if (newTask.reminder) { 50 | newTask.reminderDate = new Date( 51 | newTask.startDate.getTime() - newTask.reminder * MILLISECONDS_PER_MINUTE, 52 | ); 53 | } 54 | return { 55 | newTaskOrder: [...oldTasks, newTask, ...competingTasks], 56 | updatedTask: newTask, 57 | }; 58 | } 59 | } 60 | 61 | // Schedule tasks based on earliest dueDate (Earliest Deadline First) 62 | for (let i = 0; i < competingTasks.length; i++) { 63 | cTask = competingTasks[i]; 64 | 65 | if (newTask.dueDate < cTask.dueDate) { 66 | const newTaskStartDate = cTask.startDate; 67 | 68 | // Shift all subsequent competing tasks forward and insert the new task at the start 69 | for (let j = i; j < competingTasks.length; j++) { 70 | competingTasks[j].startDate = new Date( 71 | competingTasks[j].startDate.getTime() + 72 | newTask.duration * MILLISECONDS_PER_MINUTE + 73 | newTask.travelTime * MILLISECONDS_PER_MINUTE, 74 | ); 75 | competingTasks[j].endDate = new Date( 76 | competingTasks[j].endDate.getTime() + 77 | newTask.duration * MILLISECONDS_PER_MINUTE + 78 | newTask.travelTime * MILLISECONDS_PER_MINUTE, 79 | ); 80 | } 81 | 82 | // Schedule the new task in place of the existing one 83 | newTask.startDate = newTaskStartDate; 84 | newTask.endDate = new Date( 85 | newTaskStartDate.getTime() + 86 | newTask.duration * MILLISECONDS_PER_MINUTE + 87 | newTask.travelTime * MILLISECONDS_PER_MINUTE, 88 | ); 89 | 90 | if (newTask.reminder) { 91 | newTask.reminderDate = new Date( 92 | newTask.startDate.getTime() - newTask.reminder * MILLISECONDS_PER_MINUTE, 93 | ); 94 | } 95 | // Insert the new task at the specified index 96 | competingTasks.splice(i, 0, newTask); 97 | 98 | return { 99 | newTaskOrder: [...oldTasks, ...competingTasks], 100 | updatedTask: newTask, 101 | }; 102 | } 103 | } 104 | 105 | newTask.startDate = cTask ? cTask.endDate : minDate; 106 | 107 | newTask.startDate = new Date(newTask.startDate.getTime()); 108 | 109 | if (newTask.reminder) { 110 | newTask.reminderType = newTask.reminder; 111 | newTask.reminderDate = new Date(newTask.startDate.getTime() - newTask.reminder * MILLISECONDS_PER_MINUTE); 112 | } 113 | 114 | newTask.endDate = 115 | (cTask && 116 | new Date( 117 | cTask.endDate.getTime() + 118 | newTask.duration * MILLISECONDS_PER_MINUTE + 119 | newTask.travelTime * MILLISECONDS_PER_MINUTE, 120 | )) || 121 | new Date( 122 | minDate.getTime() + 123 | newTask.duration * MILLISECONDS_PER_MINUTE + 124 | newTask.travelTime * MILLISECONDS_PER_MINUTE, 125 | ); 126 | 127 | // Shift the Tasks based on working hours 128 | const { shiftedTasks } = shiftTasks([...existingTasks, newTask], startTime, endTime); 129 | 130 | return { 131 | newTaskOrder: shiftedTasks, 132 | updatedTask: newTask, 133 | }; 134 | }; 135 | 136 | export { addTaskToSchedule }; 137 | -------------------------------------------------------------------------------- /doto-frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | export function register(config) { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Let's check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl, config); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | "This web app is being served cache-first by a service " + 44 | "worker. To learn more, visit https://bit.ly/CRA-PWA" 45 | ); 46 | }); 47 | } else { 48 | // Is not localhost. Just register service worker 49 | registerValidSW(swUrl, config); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl, config) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker == null) { 62 | return; 63 | } 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === "installed") { 66 | if (navigator.serviceWorker.controller) { 67 | // At this point, the updated precached content has been fetched, 68 | // but the previous service worker will still serve the older 69 | // content until all client tabs are closed. 70 | console.log( 71 | "New content is available and will be used when all " + 72 | "tabs for this page are closed. See https://bit.ly/CRA-PWA." 73 | ); 74 | 75 | // Execute callback 76 | if (config && config.onUpdate) { 77 | config.onUpdate(registration); 78 | } 79 | } else { 80 | // At this point, everything has been precached. 81 | // It's the perfect time to display a 82 | // "Content is cached for offline use." message. 83 | console.log("Content is cached for offline use."); 84 | 85 | // Execute callback 86 | if (config && config.onSuccess) { 87 | config.onSuccess(registration); 88 | } 89 | } 90 | } 91 | }; 92 | }; 93 | }) 94 | .catch(error => { 95 | console.error("Error during service worker registration:", error); 96 | }); 97 | } 98 | 99 | function checkValidServiceWorker(swUrl, config) { 100 | // Check if the service worker can be found. If it can't reload the page. 101 | fetch(swUrl, { 102 | headers: { "Service-Worker": "script" } 103 | }) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get("content-type"); 107 | if (response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1)) { 108 | // No service worker found. Probably a different app. Reload the page. 109 | navigator.serviceWorker.ready.then(registration => { 110 | registration.unregister().then(() => { 111 | window.location.reload(); 112 | }); 113 | }); 114 | } else { 115 | // Service worker found. Proceed as normal. 116 | registerValidSW(swUrl, config); 117 | } 118 | }) 119 | .catch(() => { 120 | console.log("No internet connection found. App is running in offline mode."); 121 | }); 122 | } 123 | 124 | export function unregister() { 125 | if ("serviceWorker" in navigator) { 126 | navigator.serviceWorker.ready 127 | .then(registration => { 128 | registration.unregister(); 129 | }) 130 | .catch(error => { 131 | console.error(error.message); 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "KimberleyEvans-Parker", 10 | "name": "Kimberley", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/45865186?v=4", 12 | "profile": "https://github.com/KimberleyEvans-Parker", 13 | "contributions": [ 14 | "code", 15 | "review", 16 | "design", 17 | "ideas" 18 | ] 19 | }, 20 | { 21 | "login": "AlexanderTheGrape", 22 | "name": "Alex Monk", 23 | "avatar_url": "https://avatars0.githubusercontent.com/u/20546002?v=4", 24 | "profile": "https://github.com/AlexanderTheGrape", 25 | "contributions": [ 26 | "code", 27 | "test", 28 | "doc" 29 | ] 30 | }, 31 | { 32 | "login": "Matteas-Eden", 33 | "name": "Matt Eden", 34 | "avatar_url": "https://avatars0.githubusercontent.com/u/45587386?v=4", 35 | "profile": "http://matteas.nz", 36 | "contributions": [ 37 | "code", 38 | "review", 39 | "design", 40 | "doc", 41 | "test", 42 | "bug" 43 | ] 44 | }, 45 | { 46 | "login": "jordansimsmith", 47 | "name": "Jordan Sim-Smith", 48 | "avatar_url": "https://avatars3.githubusercontent.com/u/18223858?v=4", 49 | "profile": "https://jordan.sim-smith.co.nz", 50 | "contributions": [ 51 | "code", 52 | "review", 53 | "design", 54 | "doc" 55 | ] 56 | }, 57 | { 58 | "login": "qibao0722", 59 | "name": "Xiaoji Sun", 60 | "avatar_url": "https://avatars3.githubusercontent.com/u/53366211?v=4", 61 | "profile": "https://github.com/qibao0722", 62 | "contributions": [ 63 | "code", 64 | "design" 65 | ] 66 | }, 67 | { 68 | "login": "PreetPatel", 69 | "name": "Preet Patel", 70 | "avatar_url": "https://avatars1.githubusercontent.com/u/22407548?v=4", 71 | "profile": "http://PreetPatel.com", 72 | "contributions": [ 73 | "code", 74 | "review", 75 | "design", 76 | "doc", 77 | "video", 78 | "bug" 79 | ] 80 | }, 81 | { 82 | "login": "EricPedrido", 83 | "name": "Eric Pedrido", 84 | "avatar_url": "https://avatars1.githubusercontent.com/u/43208889?v=4", 85 | "profile": "https://github.com/EricPedrido", 86 | "contributions": [ 87 | "code", 88 | "review", 89 | "design", 90 | "test" 91 | ] 92 | }, 93 | { 94 | "login": "harmanlamba", 95 | "name": "Harman Lamba", 96 | "avatar_url": "https://avatars1.githubusercontent.com/u/40023122?v=4", 97 | "profile": "https://github.com/harmanlamba", 98 | "contributions": [ 99 | "code", 100 | "review", 101 | "design", 102 | "test" 103 | ] 104 | }, 105 | { 106 | "login": "salma-s", 107 | "name": "salma-s", 108 | "avatar_url": "https://avatars0.githubusercontent.com/u/43306586?v=4", 109 | "profile": "https://github.com/salma-s", 110 | "contributions": [ 111 | "code", 112 | "review", 113 | "design", 114 | "test" 115 | ] 116 | }, 117 | { 118 | "login": "Minus20Five", 119 | "name": "Tony Liu", 120 | "avatar_url": "https://avatars3.githubusercontent.com/u/20623467?v=4", 121 | "profile": "https://github.com/Minus20Five", 122 | "contributions": [ 123 | "code", 124 | "review", 125 | "design", 126 | "doc", 127 | "test" 128 | ] 129 | }, 130 | { 131 | "login": "HarrisonLeach1", 132 | "name": "Harrison Leach", 133 | "avatar_url": "https://avatars3.githubusercontent.com/u/44953072?v=4", 134 | "profile": "https://harrisonleach1.github.io", 135 | "contributions": [ 136 | "code", 137 | "review", 138 | "design", 139 | "test" 140 | ] 141 | }, 142 | { 143 | "login": "DeshmukhChinmay", 144 | "name": "Chinmay Deshmukh", 145 | "avatar_url": "https://avatars3.githubusercontent.com/u/41243225?v=4", 146 | "profile": "https://github.com/DeshmukhChinmay", 147 | "contributions": [ 148 | "code", 149 | "review", 150 | "doc" 151 | ] 152 | }, 153 | { 154 | "login": "TCHE614", 155 | "name": "TCHE614", 156 | "avatar_url": "https://avatars3.githubusercontent.com/u/48304096?v=4", 157 | "profile": "https://github.com/TCHE614", 158 | "contributions": [ 159 | "code", 160 | "review", 161 | "doc" 162 | ] 163 | }, 164 | { 165 | "login": "brianzhang310", 166 | "name": "brianzhang310", 167 | "avatar_url": "https://avatars2.githubusercontent.com/u/43288000?v=4", 168 | "profile": "https://github.com/brianzhang310", 169 | "contributions": [ 170 | "code", 171 | "review", 172 | "doc" 173 | ] 174 | }, 175 | { 176 | "login": "nikotj1", 177 | "name": "nikotj1", 178 | "avatar_url": "https://avatars1.githubusercontent.com/u/43423740?v=4", 179 | "profile": "https://github.com/nikotj1", 180 | "contributions": [ 181 | "bug", 182 | "design" 183 | ] 184 | }, 185 | { 186 | "login": "Kalashnikkov", 187 | "name": "Finn", 188 | "avatar_url": "https://avatars2.githubusercontent.com/u/48403060?v=4", 189 | "profile": "https://github.com/Kalashnikkov", 190 | "contributions": [ 191 | "bug", 192 | "code" 193 | ] 194 | }, 195 | { 196 | "login": "utri092", 197 | "name": "utri092", 198 | "avatar_url": "https://avatars3.githubusercontent.com/u/41176826?v=4", 199 | "profile": "https://github.com/utri092", 200 | "contributions": [ 201 | "code", 202 | "design" 203 | ] 204 | }, 205 | { 206 | "login": "rmoradc", 207 | "name": "rmoradc", 208 | "avatar_url": "https://avatars3.githubusercontent.com/u/43200280?v=4", 209 | "profile": "https://github.com/rmoradc", 210 | "contributions": [ 211 | "code", 212 | "review", 213 | "design" 214 | ] 215 | } 216 | ], 217 | "contributorsPerLine": 7, 218 | "projectName": "Doto", 219 | "projectOwner": "se701g2", 220 | "repoType": "github", 221 | "repoHost": "https://github.com", 222 | "skipCi": true 223 | } 224 | -------------------------------------------------------------------------------- /Code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Doto's Code of Conduct 2 | 3 | The following code of conduct outlines the expectations for participation in the open-source community managed by Doto, as well as steps for reporting unacceptable behavior. 4 | 5 | At Doto, we are committed to providing a welcoming, open, diverse, inclusive and healthy workspace for everyone. 6 | 7 | Any individual violating this code of conduct may be banned from the community. 8 | 9 | This code is not exhaustive or complete. It serves to capture our common understanding of a productive, collaborative environment. We expect the code to be followed in spirit as much as in the letter. 10 | 11 | ## Standards 12 | 13 | As a part of the Doto community we will: 14 | 15 | - **Be patient and friendly:** Remember that you might not be communicating in someone else's primary 16 | spoken or programming language and others may not have your level of understanding. 17 | 18 | - **Be welcoming:** Doto welcomes and supports people from all backgrounds and communities. This includes but is not limited to the level of experience, education, socio-economic status, visible or invisible disability, race, ethnicity, nationality, religion, sexual identity, and orientation or gender identity. 19 | 20 | - **Be respectful:** As developers of the Doto community, we conduct ourselves professionally and practice acceptance and tolerance. Disagreement is no excuse for poor behavior and poor manners. Doto defines disrespectful and unacceptable behavior to be: 21 | - Belittling anyone's experience 22 | - Violent threats or language 23 | - Discriminatory or derogatory jokes and language 24 | - Posting sexually explicit or violent material. 25 | - Posting, or threatening to post, people's personally identifying information 26 | - Insults, especially those using discriminatory terms or slurs. 27 | - Behavior that could be perceived as sexual attention 28 | - Advocating for or encouraging any of the above behaviors. 29 | 30 | - **Demonstrate empathy and kindness toward other people:** At Doto we promote empathy as it helps us make more informed decisions as developers. We should also not taunt others or be show anger. It doesn't help anyone when we furiously tell others what they do is wrong and that they shouldn’t be doing what they are doing. 31 | 32 | - **Take constructive criticism:** Disagreements, both social and technical, are useful learning opportunities. Seek to understand the other viewpoints and resolve differences constructively. Give and gracefully accept constructive feedback. 33 | 34 | - **Focus on team rather than self:** Great results come from working together towards the same goal, 35 | not by working yourself towards your goal. 36 | 37 | ## Enforcement Responsibilities 38 | 39 | Everyone who belongs to Doto should be practicing this code and making sure that no one else is violating it. However, community leaders are responsible for clarifying these standards 40 | of acceptable behavior wherever the line is unclear. They will also take appropriate action for any behavior that they deem unaligned with this code. 41 | 42 | Community leaders have the right to remove, edit, or reject, comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this code of conduct, and will communicate the reasons for moderation decisions when appropriate. 43 | 44 | ## Scope 45 | This code of conduct applies within all community spaces including issues and pull requests, and also applies when an individual is officially representing the community in public spaces. These public spaces include using the official email address or delivering presentations to an online or offline event. 46 | 47 | ## Enforcement 48 | 49 | Instances of abusive, harassment or otherwise unacceptable behavior that breaks this code may be reported to the community leaders responsible for enforcement. If you encounter such behavior please inform us by emailing **se701group2@gmail.com**. 50 | 51 | In your email please include: 52 | 53 | - Your contact information. 54 | - Names of any individuals involved. If there are additional witnesses, please include them as well. 55 | - Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public chat log), please include a link or attachment. 56 | - Any additional information that may be helpful. 57 | 58 | All complaints will be reviewed and investigated promptly and fairly. 59 | 60 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 61 | 62 | The following is a detailed outline of the steps of enforcement when the code is broken. 63 | 64 | **1. Correction** 65 | 66 | Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 67 | 68 | Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 69 | 70 | **2. Warning** 71 | 72 | Community Impact: A violation through a single incident or series of actions. 73 | 74 | Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. 75 | 76 | Violating these terms may lead to a temporary or permanent ban. 77 | 78 | **3. Temporary Ban** 79 | 80 | Community Impact: A serious violation of community standards, including sustained inappropriate behavior. 81 | 82 | Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period. No public or 83 | private interaction with the people involved, including unsolicited 84 | interaction with those enforcing the Code of Conduct is allowed during 85 | this period. 86 | 87 | Violating these terms may lead to a permanent ban. 88 | 89 | **4. Permanent Ban** 90 | 91 | Community Impact: Demonstrating a pattern of violation of community 92 | standards, including sustained inappropriate behavior, harassment of an 93 | individual, or aggression toward or disparagement of classes of 94 | individuals. 95 | 96 | Consequence: A permanent ban from any sort of public interaction within the community. 97 | 98 | ## Attribution 99 | 100 | This Code of Conduct is adapted from the Contributor Covenant Code of Conduct 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/git,node,react,macos,windows,intellij,webstorm,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=git,node,react,macos,windows,intellij,webstorm,visualstudiocode 4 | 5 | ### Git ### 6 | # Created by git for backups. To disable backups in Git: 7 | # $ git config --global mergetool.keepBackup false 8 | *.orig 9 | 10 | # Created by git when using merge tools for conflicts 11 | *.BACKUP.* 12 | *.BASE.* 13 | *.LOCAL.* 14 | *.REMOTE.* 15 | *_BACKUP_*.txt 16 | *_BASE_*.txt 17 | *_LOCAL_*.txt 18 | *_REMOTE_*.txt 19 | 20 | ### Intellij ### 21 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 22 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 23 | 24 | # User-specific stuff 25 | .idea/**/workspace.xml 26 | .idea/**/tasks.xml 27 | .idea/**/usage.statistics.xml 28 | .idea/**/dictionaries 29 | .idea/**/shelf 30 | 31 | # Generated files 32 | .idea/**/contentModel.xml 33 | 34 | # Sensitive or high-churn files 35 | .idea/**/dataSources/ 36 | .idea/**/dataSources.ids 37 | .idea/**/dataSources.local.xml 38 | .idea/**/sqlDataSources.xml 39 | .idea/**/dynamic.xml 40 | .idea/**/uiDesigner.xml 41 | .idea/**/dbnavigator.xml 42 | 43 | # Gradle 44 | .idea/**/gradle.xml 45 | .idea/**/libraries 46 | 47 | # Gradle and Maven with auto-import 48 | # When using Gradle or Maven with auto-import, you should exclude module files, 49 | # since they will be recreated, and may cause churn. Uncomment if using 50 | # auto-import. 51 | # .idea/modules.xml 52 | # .idea/*.iml 53 | # .idea/modules 54 | # *.iml 55 | # *.ipr 56 | 57 | # CMake 58 | cmake-build-*/ 59 | 60 | # Mongo Explorer plugin 61 | .idea/**/mongoSettings.xml 62 | 63 | # File-based project format 64 | *.iws 65 | 66 | # IntelliJ 67 | out/ 68 | 69 | # mpeltonen/sbt-idea plugin 70 | .idea_modules/ 71 | 72 | # JIRA plugin 73 | atlassian-ide-plugin.xml 74 | 75 | # Cursive Clojure plugin 76 | .idea/replstate.xml 77 | 78 | # Crashlytics plugin (for Android Studio and IntelliJ) 79 | com_crashlytics_export_strings.xml 80 | crashlytics.properties 81 | crashlytics-build.properties 82 | fabric.properties 83 | 84 | # Editor-based Rest Client 85 | .idea/httpRequests 86 | 87 | # Android studio 3.1+ serialized cache file 88 | .idea/caches/build_file_checksums.ser 89 | 90 | ### Intellij Patch ### 91 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 92 | 93 | # *.iml 94 | # modules.xml 95 | # .idea/misc.xml 96 | # *.ipr 97 | 98 | # Sonarlint plugin 99 | .idea/**/sonarlint/ 100 | 101 | # SonarQube Plugin 102 | .idea/**/sonarIssues.xml 103 | 104 | # Markdown Navigator plugin 105 | .idea/**/markdown-navigator.xml 106 | .idea/**/markdown-navigator/ 107 | 108 | ### macOS ### 109 | # General 110 | .DS_Store 111 | .AppleDouble 112 | .LSOverride 113 | 114 | # Icon must end with two \r 115 | Icon 116 | 117 | # Thumbnails 118 | ._* 119 | 120 | # Files that might appear in the root of a volume 121 | .DocumentRevisions-V100 122 | .fseventsd 123 | .Spotlight-V100 124 | .TemporaryItems 125 | .Trashes 126 | .VolumeIcon.icns 127 | .com.apple.timemachine.donotpresent 128 | 129 | # Directories potentially created on remote AFP share 130 | .AppleDB 131 | .AppleDesktop 132 | Network Trash Folder 133 | Temporary Items 134 | .apdisk 135 | 136 | ### Node ### 137 | # Logs 138 | logs 139 | *.log 140 | npm-debug.log* 141 | yarn-debug.log* 142 | yarn-error.log* 143 | lerna-debug.log* 144 | 145 | # Diagnostic reports (https://nodejs.org/api/report.html) 146 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 147 | 148 | # Runtime data 149 | pids 150 | *.pid 151 | *.seed 152 | *.pid.lock 153 | 154 | # Directory for instrumented libs generated by jscoverage/JSCover 155 | lib-cov 156 | 157 | # Coverage directory used by tools like istanbul 158 | coverage 159 | *.lcov 160 | 161 | # nyc test coverage 162 | .nyc_output 163 | 164 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 165 | .grunt 166 | 167 | # Bower dependency directory (https://bower.io/) 168 | bower_components 169 | 170 | # node-waf configuration 171 | .lock-wscript 172 | 173 | # Compiled binary addons (https://nodejs.org/api/addons.html) 174 | build/Release 175 | 176 | # Dependency directories 177 | node_modules/ 178 | jspm_packages/ 179 | 180 | # TypeScript v1 declaration files 181 | typings/ 182 | 183 | # TypeScript cache 184 | *.tsbuildinfo 185 | 186 | # Optional npm cache directory 187 | .npm 188 | 189 | # Optional eslint cache 190 | .eslintcache 191 | 192 | # Microbundle cache 193 | .rpt2_cache/ 194 | .rts2_cache_cjs/ 195 | .rts2_cache_es/ 196 | .rts2_cache_umd/ 197 | 198 | # Optional REPL history 199 | .node_repl_history 200 | 201 | # Output of 'npm pack' 202 | *.tgz 203 | 204 | # Yarn Integrity file 205 | .yarn-integrity 206 | 207 | # dotenv environment variables file 208 | .env 209 | .env.test 210 | 211 | # parcel-bundler cache (https://parceljs.org/) 212 | .cache 213 | 214 | # next.js build output 215 | .next 216 | 217 | # nuxt.js build output 218 | .nuxt 219 | 220 | # rollup.js default build output 221 | dist/ 222 | 223 | # Uncomment the public line if your project uses Gatsby 224 | # https://nextjs.org/blog/next-9-1#public-directory-support 225 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 226 | # public 227 | 228 | # Storybook build outputs 229 | .out 230 | .storybook-out 231 | 232 | # vuepress build output 233 | .vuepress/dist 234 | 235 | # Serverless directories 236 | .serverless/ 237 | 238 | # FuseBox cache 239 | .fusebox/ 240 | 241 | # DynamoDB Local files 242 | .dynamodb/ 243 | 244 | # TernJS port file 245 | .tern-port 246 | 247 | # Temporary folders 248 | tmp/ 249 | temp/ 250 | 251 | ### react ### 252 | .DS_* 253 | **/*.backup.* 254 | **/*.back.* 255 | 256 | node_modules 257 | 258 | *.sublime* 259 | 260 | psd 261 | thumb 262 | sketch 263 | 264 | ### VisualStudioCode ### 265 | .vscode/* 266 | !.vscode/settings.json 267 | !.vscode/tasks.json 268 | !.vscode/launch.json 269 | !.vscode/extensions.json 270 | 271 | ### VisualStudioCode Patch ### 272 | # Ignore all local history of files 273 | .history 274 | 275 | ### WebStorm ### 276 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 277 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 278 | 279 | # User-specific stuff 280 | 281 | # Generated files 282 | 283 | # Sensitive or high-churn files 284 | 285 | # Gradle 286 | 287 | # Gradle and Maven with auto-import 288 | # When using Gradle or Maven with auto-import, you should exclude module files, 289 | # since they will be recreated, and may cause churn. Uncomment if using 290 | # auto-import. 291 | # .idea/modules.xml 292 | # .idea/*.iml 293 | # .idea/modules 294 | # *.iml 295 | # *.ipr 296 | 297 | # CMake 298 | 299 | # Mongo Explorer plugin 300 | 301 | # File-based project format 302 | 303 | # IntelliJ 304 | 305 | # mpeltonen/sbt-idea plugin 306 | 307 | # JIRA plugin 308 | 309 | # Cursive Clojure plugin 310 | 311 | # Crashlytics plugin (for Android Studio and IntelliJ) 312 | 313 | # Editor-based Rest Client 314 | 315 | # Android studio 3.1+ serialized cache file 316 | 317 | ### WebStorm Patch ### 318 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 319 | 320 | # *.iml 321 | # modules.xml 322 | # .idea/misc.xml 323 | # *.ipr 324 | 325 | # Sonarlint plugin 326 | 327 | # SonarQube Plugin 328 | 329 | # Markdown Navigator plugin 330 | 331 | ### Windows ### 332 | # Windows thumbnail cache files 333 | Thumbs.db 334 | Thumbs.db:encryptable 335 | ehthumbs.db 336 | ehthumbs_vista.db 337 | 338 | # Dump file 339 | *.stackdump 340 | 341 | # Folder config file 342 | [Dd]esktop.ini 343 | 344 | # Recycle Bin used on file shares 345 | $RECYCLE.BIN/ 346 | 347 | # Windows Installer files 348 | *.cab 349 | *.msi 350 | *.msix 351 | *.msm 352 | *.msp 353 | 354 | # Windows shortcuts 355 | *.lnk 356 | 357 | # End of https://www.gitignore.io/api/git,node,react,macos,windows,intellij,webstorm,visualstudiocode -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Calendar/CalendarComponent.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import "./Calendar.css"; 3 | import { ViewState, EditingState, IntegratedEditing } from "@devexpress/dx-react-scheduler"; 4 | import PropTypes from "prop-types"; 5 | import { 6 | Scheduler, 7 | Resources, 8 | WeekView, 9 | MonthView, 10 | DayView, 11 | ViewSwitcher, 12 | Toolbar, 13 | DateNavigator, 14 | TodayButton, 15 | Appointments, 16 | AppointmentTooltip, 17 | DragDropProvider, 18 | } from "@devexpress/dx-react-scheduler-material-ui"; 19 | import { Checkbox, Grid } from "@material-ui/core"; 20 | import DoneIcon from "@material-ui/icons/Done"; 21 | import IconButton from "@material-ui/core/IconButton"; 22 | import DeleteIcon from "@material-ui/icons/Delete"; 23 | import CreateIcon from "@material-ui/icons/Create"; 24 | import Button from "@material-ui/core/Button"; 25 | import Dialog from "@material-ui/core/Dialog"; 26 | import DialogActions from "@material-ui/core/DialogActions"; 27 | import DialogTitle from "@material-ui/core/DialogTitle"; 28 | import Modal from "@material-ui/core/Modal"; 29 | import Backdrop from "@material-ui/core/Backdrop"; 30 | import Fade from "@material-ui/core/Fade"; 31 | import UpdateModalContent from "../../updateModal/UpdateModalContent"; 32 | import { makeStyles } from "@material-ui/core/styles"; 33 | import { ThemeContext } from "../../../context/ThemeContext"; 34 | import { categoryData } from "../../../constants/Categories"; 35 | 36 | const CalendarComponent = ({ tasks, onTaskStatusUpdated, onTaskDeleted, onTaskUpdated, onCommitChanges }) => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ( 53 | 59 | )} 60 | /> 61 | 62 | 63 | ); 64 | }; 65 | 66 | const resources = [ 67 | { 68 | fieldName: "category", 69 | instances: categoryData, 70 | }, 71 | ]; 72 | 73 | const useStyles = makeStyles(theme => ({ 74 | modal: { 75 | display: "flex", 76 | alignItems: "center", 77 | justifyContent: "center", 78 | }, 79 | paper: { 80 | backgroundColor: theme.palette.background.paper, 81 | border: "2px solid #000", 82 | boxShadow: theme.shadows[5], 83 | padding: theme.spacing(2, 4, 3), 84 | }, 85 | dueContainer: { 86 | margin: "0 auto", 87 | color: "red", 88 | }, 89 | })); 90 | 91 | export function Content({ 92 | children, 93 | appointmentData, 94 | style, 95 | onTaskStatusUpdated, 96 | onTaskDeleted, 97 | onTaskUpdated, 98 | ...restProps 99 | }) { 100 | const [open, setOpen] = useState(false); 101 | const [openUpdateModal, setOpenUpdateModal] = useState(false); 102 | const classes = useStyles(); 103 | const [theme] = useContext(ThemeContext); 104 | 105 | const handleClickOpen = () => { 106 | setOpen(true); 107 | }; 108 | 109 | const handleClose = () => { 110 | setOpen(false); 111 | }; 112 | 113 | const deleteTask = taskId => { 114 | onTaskDeleted(taskId); 115 | document.getElementById("grid").click(); 116 | }; 117 | 118 | const handleOpenUpdateModal = () => { 119 | setOpenUpdateModal(true); 120 | }; 121 | 122 | const handleCloseUpdateModal = () => { 123 | setOpenUpdateModal(false); 124 | }; 125 | 126 | return ( 127 | 128 | 129 |
130 |
Due: {new Date(appointmentData.dueDate).toLocaleString()}
131 |
132 |
133 | 134 |
135 |
136 | onTaskStatusUpdated(appointmentData.taskId)} 140 | /> 141 | {appointmentData.isComplete ? "Task complete" : "Task incomplete"} 142 |
143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 | 157 | 158 | {"Are you sure you want to delete this task?"} 159 | 160 | 161 | 164 | 167 | 168 | 169 | 170 | {/* Update Task Modal */} 171 | 183 | {/* Transition effects for list view of to-do tasks for today */} 184 | 185 |
186 | 191 |
192 |
193 |
194 |
195 |
196 |
197 | ); 198 | } 199 | 200 | const Appointment = ({ children, style, ...restProps }) => ( 201 | 211 | {children} 212 | 213 | 214 | {restProps.data.isComplete ? "Task complete" : "Task incomplete"} 215 | 216 | {restProps.data.isComplete && } 217 | 218 | 219 | ); 220 | 221 | CalendarComponent.propTypes = { 222 | tasks: PropTypes.array.isRequired, 223 | }; 224 | 225 | export default CalendarComponent; 226 | -------------------------------------------------------------------------------- /doto-backend/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Doto", 6 | "description": "A brief visual description of backend API. NB. Authorization Header required for using API.", 7 | "license": { 8 | "name": "Swagger UI License: MIT", 9 | "url": "https://opensource.org/licenses/MIT" 10 | } 11 | }, 12 | "host": "localhost:3001", 13 | "basePath": "/", 14 | "tags": [ 15 | { 16 | "name": "Users", 17 | "description": "API for users in the system" 18 | } 19 | ], 20 | "schemes": ["http"], 21 | "consumes": ["application/json"], 22 | "produces": ["application/json"], 23 | "paths": { 24 | "/task/get": { 25 | "get": { 26 | "tags": ["Task"], 27 | "description": "Get all Tasks for user", 28 | "parameters": [ 29 | { 30 | "in": "User", 31 | "description": "email (String)" 32 | } 33 | ], 34 | "produces": ["application/json"], 35 | "responses": { 36 | "200": { 37 | "description": "List of tasks associated with this user", 38 | "schema": { 39 | "$ref": "#/definitions/Tasks" 40 | } 41 | } 42 | } 43 | } 44 | }, 45 | "/task/post": { 46 | "post": { 47 | "tags": ["Task"], 48 | "description": "Create new task in system", 49 | "parameters": [ 50 | { 51 | "in": "body", 52 | "schema": { 53 | "$ref": "#/definitions/Task" 54 | } 55 | } 56 | ], 57 | "produces": ["application/json"], 58 | "responses": { 59 | "200": { 60 | "description": "taskId: 'taskId', Successful: 'True'" 61 | }, 62 | "400": { 63 | "description": "taskId: 'taskId', Successful: 'False'" 64 | } 65 | } 66 | } 67 | }, 68 | "/task": { 69 | "put": { 70 | "tags": ["Task"], 71 | "description": "Edit Task in System", 72 | "parameters": [ 73 | { 74 | "in": "taskId", 75 | "description": "taskId (String)" 76 | }, 77 | { 78 | "in": "body", 79 | "schema": { 80 | "$ref": "#/definitions/Task" 81 | } 82 | } 83 | ], 84 | "produces": ["application/json"], 85 | "responses": { 86 | "200": { 87 | "description": "taskId: 'taskId', Successful: 'True'" 88 | }, 89 | "400": { 90 | "description": "taskId: 'taskId', Successful: 'False'" 91 | } 92 | } 93 | }, 94 | "delete": { 95 | "tags": ["Task"], 96 | "description": "Delete Task in System", 97 | "parameters": [ 98 | { 99 | "in": "taskId", 100 | "description": "taskId (String)" 101 | } 102 | ], 103 | "produces": ["application/json"], 104 | "responses": { 105 | "200": { 106 | "description": "taskId: 'taskId', Deleted: 'True'" 107 | }, 108 | "400": { 109 | "description": "taskId: 'taskId', Deleted: 'False'" 110 | } 111 | } 112 | } 113 | }, 114 | "/user/get": { 115 | "get": { 116 | "tags": ["Users"], 117 | "description": "Get user details", 118 | "parameters": [ 119 | { 120 | "in": "email", 121 | "description": "email (String)" 122 | } 123 | ], 124 | "produces": ["application/json"], 125 | "responses": { 126 | "200": { 127 | "description": "Here are all the User Details", 128 | "schema": { 129 | "$ref": "#/definitions/User" 130 | } 131 | }, 132 | "400": { 133 | "description": "Error: could not find user with specified email." 134 | } 135 | } 136 | } 137 | }, 138 | "/user/update": { 139 | "put": { 140 | "tags": ["Users"], 141 | "description": "Update user details", 142 | "parameters": [ 143 | { 144 | "in": "email", 145 | "description": "email (String)" 146 | }, 147 | { 148 | "in": "body", 149 | "schema": { 150 | "$ref": "#/definitions/User" 151 | } 152 | } 153 | ], 154 | "produces": ["application/json"], 155 | "responses": { 156 | "200": { 157 | "description": "email: 'email', Successful: 'True'" 158 | }, 159 | "400": { 160 | "description": "email: 'email', Successful: 'False'" 161 | } 162 | } 163 | } 164 | }, 165 | "/user/email": { 166 | "get": { 167 | "tags": ["Users"], 168 | "description": "Get all users details", 169 | "parameters": [], 170 | "produces": ["application/json"], 171 | "responses": { 172 | "200": { 173 | "description": "Here are all the Users and their Details", 174 | "schema": { 175 | "$ref": "#/definitions/Users" 176 | } 177 | }, 178 | "400": { 179 | "description": "msg: failed" 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | "definitions": { 186 | "User": { 187 | "required": ["email"], 188 | "properties": { 189 | "email": { 190 | "type": "string", 191 | "uniqueItems": true 192 | }, 193 | "name": { 194 | "type": "string", 195 | "uniqueItems": false 196 | }, 197 | "picture": { 198 | "type": "string" 199 | }, 200 | "themePreference": { 201 | "type": "string", 202 | "uniqueItems": false, 203 | "default": "dark" 204 | } 205 | } 206 | }, 207 | "Users": { 208 | "type": "array", 209 | "$ref": "#/definitions/User" 210 | }, 211 | "Task": { 212 | "required": ["user", "taskId", "title", "duration", "startDate", "endDate"], 213 | "properties": { 214 | "user": { 215 | "type": "User", 216 | "uniqueItems": true 217 | }, 218 | "taskId": { 219 | "type": "string", 220 | "uniqueItems": true 221 | }, 222 | "title": { 223 | "type": "string", 224 | "uniqueItems": false 225 | }, 226 | "description": { 227 | "type": "string", 228 | "uniqueItems": false 229 | }, 230 | "location": { 231 | "type": "string", 232 | "uniqueItems": false 233 | }, 234 | "priority": { 235 | "type": "integer", 236 | "uniqueItems": false 237 | }, 238 | "duration": { 239 | "type": "integer", 240 | "uniqueItems": false 241 | }, 242 | "startDate": { 243 | "type": "date", 244 | "uniqueItems": false 245 | }, 246 | "endDate": { 247 | "type": "date", 248 | "uniqueItems": false 249 | }, 250 | "reminderDate": { 251 | "type": "date", 252 | "uniqueItems": false 253 | }, 254 | "dueDate": { 255 | "type": "date", 256 | "uniqueItems": false 257 | }, 258 | "travelTime": { 259 | "type": "integer", 260 | "uniqueItems": false 261 | }, 262 | "reminderType": { 263 | "type": "integer", 264 | "uniqueItems": false 265 | }, 266 | "earliestDate": { 267 | "type": "date", 268 | "uniqueItems": false 269 | } 270 | } 271 | }, 272 | "Tasks": { 273 | "type": "array", 274 | "$ref": "#/definitions/Task" 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /doto-backend/test/task.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const mongoose = require("mongoose"); 4 | const TaskModel = require("../src/models/Task"); 5 | const UserModel = require("../src/models/User"); 6 | const assert = require("assert"); 7 | 8 | const validUser = new UserModel({ 9 | email: "john@mail.com", 10 | name: "john", 11 | picture: "profile.png", 12 | themePreference: "dark", 13 | }); 14 | 15 | const validTask = new TaskModel({ 16 | user: validUser, 17 | taskId: "1", 18 | title: "title", 19 | description: "Re-Doing all the things", 20 | location: "science building", 21 | priority: 0, 22 | duration: 120, 23 | reminderDate: "2020-07-14T07:50:00+12:00", 24 | startDate: "2020-08-14T08:50:00+12:00", 25 | endDate: "2020-08-14T07:50:00+12:00", 26 | isComplete: false, 27 | travelTime: 10, 28 | dueDate: "2020-08-14T07:50:00+12:00", 29 | earliestDate: "2020-08-14T07:50:00+12:00", 30 | }); 31 | 32 | process.env.TEST_SUITE = "task-test"; 33 | 34 | describe("Task Model Tests", function () { 35 | before(async function () { 36 | await mongoose.connect( 37 | `mongodb://127.0.0.1:27017/${process.env.TEST_SUITE}`, 38 | { useNewUrlParser: true }, 39 | (err) => { 40 | if (err) { 41 | console.error(err); 42 | process.exit(1); 43 | } 44 | }, 45 | ); 46 | }); 47 | 48 | after(async function () { 49 | await mongoose.connection.dropDatabase(); 50 | await mongoose.connection.close(); 51 | }); 52 | 53 | it("create & save task successfully.", async function () { 54 | const taskID = validTask._id; 55 | const savedTask = await validTask.save(); 56 | assert(savedTask._id === taskID); 57 | }); 58 | 59 | it("create task without required user & throws error.", async function () { 60 | const invalidTask = new TaskModel({ 61 | title: "Do the thing", 62 | description: "Re-Doing all the things", 63 | location: "science building", 64 | priority: 0, 65 | duration: 120, 66 | reminderDate: "2020-07-14T07:50:00+12:00", 67 | startDate: "2020-08-14T06:50:00+12:00", 68 | endDate: "2020-08-14T07:50:00+12:00", 69 | }); 70 | 71 | var error = invalidTask.validateSync(); 72 | assert.equal(error.errors.user.message, "Path `user` is required."); 73 | }); 74 | 75 | it("create task without required title & throws error.", async function () { 76 | const invalidTask = new TaskModel({ 77 | description: "Re-Doing all the things", 78 | location: "science building", 79 | priority: 0, 80 | duration: 120, 81 | reminderDate: "2020-07-14T07:50:00+12:00", 82 | startDate: "2020-08-14T06:50:00+12:00", 83 | endDate: "2020-08-14T07:50:00+12:00", 84 | }); 85 | 86 | var error = invalidTask.validateSync(); 87 | assert.equal(error.errors.title.message, "Path `title` is required."); 88 | }); 89 | 90 | it("create task with incorrect date type & throws error.", async function () { 91 | const invalidTask = new TaskModel({ 92 | user: validUser, 93 | taskId: "1234", 94 | title: "title", 95 | description: "Re-Doing all the things", 96 | location: "science building", 97 | priority: 0, 98 | duration: 120, 99 | reminderDate: "2020-07-14T07:50:00+12:00", 100 | startDate: "yesterday", 101 | endDate: "2020-08-14T07:50:00+12:00", 102 | }); 103 | 104 | var error = invalidTask.validateSync(); 105 | assert.ok(error.errors.startDate.message); 106 | }); 107 | 108 | it("create task with incorrect user type & throws error.", async function () { 109 | const invalidTask = new TaskModel({ 110 | title: "title", 111 | description: "Re-Doing all the things", 112 | location: "science building", 113 | priority: 0, 114 | duration: 120, 115 | reminderDate: "2020-07-14T07:50:00+12:00", 116 | startDate: "2020-08-14T08:50:00+12:00", 117 | endDate: "2020-08-14T07:50:00+12:00", 118 | }); 119 | 120 | var error = invalidTask.validateSync(); 121 | assert.ok(error.errors.user.message); 122 | }); 123 | 124 | it("create task with incorrect priority number type & throws error.", async function () { 125 | const invalidTask = new TaskModel({ 126 | user: validUser, 127 | title: "title", 128 | description: "Re-Doing all the things", 129 | location: "science building", 130 | priority: "High", 131 | duration: 120, 132 | reminderDate: "2020-07-14T07:50:00+12:00", 133 | startDate: "2020-08-14T08:50:00+12:00", 134 | endDate: "2020-08-14T07:50:00+12:00", 135 | }); 136 | 137 | var error = invalidTask.validateSync(); 138 | assert.ok(error.errors.priority.message); 139 | }); 140 | 141 | it("populating user field from user model.", async function () { 142 | await validTask.save(); 143 | 144 | TaskModel.findOne({ taskId: validTask.taskId }) 145 | .populate("user") 146 | .then((task) => { 147 | assert(task.user.name === "john"); 148 | done(); 149 | }); 150 | }); 151 | 152 | it("delete user successfully.", async function () { 153 | TaskModel.remove({ title: "Do the thing" }).then((task) => { 154 | assert(task === null); 155 | done(); 156 | }); 157 | }); 158 | 159 | it("update task sucessfully", async function () { 160 | await validTask.save(); 161 | const savedTask = await TaskModel.findOne({ _id: validTask._id }); 162 | 163 | await savedTask.update({ title: "updated title" }); 164 | 165 | const updatedTask = await TaskModel.findOne({ _id: validTask._id }); 166 | assert(updatedTask.title === "updated title"); 167 | }); 168 | 169 | it("delete task successfully.", async function () { 170 | await validTask.save(); 171 | const savedTask = await TaskModel.findOne(); 172 | 173 | await savedTask.remove(); 174 | const newSavedTask = await TaskModel.findOne({ _id: validTask._id }); 175 | 176 | assert(newSavedTask === null); 177 | }); 178 | 179 | it("update one isComplete status to true", async function () { 180 | TaskModel.updateOne({ taskId: validTask.taskId }, { isComplete: true }) 181 | .then(() => TaskModel.findOne({ taskId: validTask.taskId })) 182 | .then((task) => { 183 | assert(task.isComplete === true); 184 | }); 185 | }); 186 | 187 | it("update one isComplete status to false", async function () { 188 | TaskModel.updateOne({ taskId: validTask.taskId }, { isComplete: false }) 189 | .then(() => TaskModel.findOne({ taskId: validTask.taskId })) 190 | .then((task) => { 191 | assert(task.isComplete === false); 192 | }); 193 | }); 194 | 195 | it("update all isComplete status to true", async function () { 196 | TaskModel.update({ isComplete: true }) 197 | .then(() => TaskModel.find({})) 198 | .then((task) => { 199 | assert(task.isComplete === true); 200 | }); 201 | }); 202 | 203 | // Begin reminder service tests 204 | // 205 | // TODO - We should clear the database after each unit test. 206 | // 207 | // Current workaround is to create new model objects and 208 | // increment the (unique) id so that tests are 'stateless' 209 | // i.e. do not depend on order of execution. 210 | it("retrieves tasks with reminderDate lte to current date", async function () { 211 | const testTask = new TaskModel({ 212 | user: "john@mail.com", 213 | taskId: "2", 214 | title: "title", 215 | description: "Re-Doing all the things", 216 | location: "science building", 217 | priority: 0, 218 | duration: 120, 219 | reminderDate: "2020-07-14T07:50:00+12:00", 220 | startDate: "2020-08-14T08:50:00+12:00", 221 | endDate: "2020-08-14T07:50:00+12:00", 222 | isComplete: false, 223 | travelTime: 20, 224 | dueDate: "2020-08-14T07:50:00+12:00", 225 | earliestDate: "2020-08-14T07:50:00+12:00", 226 | }); 227 | await testTask.save(); 228 | const [retrievedTask] = await TaskModel.find({ 229 | taskId: "2", 230 | reminderDate: { $lte: new Date(testTask.reminderDate.getTime() + 1) }, 231 | user: { $in: [testTask.user] }, 232 | isComplete: false, 233 | }).exec(); 234 | 235 | assert.equal(retrievedTask.taskID, testTask.taskID); 236 | }); 237 | 238 | it("does not retrieve tasks with unset reminder date", async function () { 239 | const testTask = new TaskModel({ 240 | user: "john@mail.com", 241 | taskId: "3", 242 | title: "title", 243 | description: "Re-Doing all the things", 244 | location: "science building", 245 | priority: 0, 246 | duration: 120, 247 | startDate: "2020-08-14T08:50:00+12:00", 248 | endDate: "2020-08-14T07:50:00+12:00", 249 | isComplete: false, 250 | travelTime: 20, 251 | dueDate: "2020-08-14T07:50:00+12:00", 252 | earliestDate: "2020-08-14T07:50:00+12:00", 253 | }); 254 | await testTask.save(); 255 | const retrievedTasks = await TaskModel.find({ 256 | taskId: "3", 257 | reminderDate: { $lte: new Date(validTask.reminderDate) }, 258 | user: { $in: [testTask.user] }, 259 | isComplete: false, 260 | }).exec(); 261 | 262 | assert(retrievedTasks.length === 0); 263 | }); 264 | 265 | it("does not retrieve tasks with future reminder date", async function () { 266 | const testTask = new TaskModel({ 267 | user: "john@mail.com", 268 | taskId: "4", 269 | title: "title", 270 | description: "Re-Doing all the things", 271 | location: "science building", 272 | priority: 0, 273 | duration: 120, 274 | reminderDate: "2020-07-14T07:50:00+12:00", 275 | startDate: "2020-08-14T08:50:00+12:00", 276 | endDate: "2020-08-14T07:50:00+12:00", 277 | isComplete: false, 278 | travelTime: 20, 279 | dueDate: "2020-08-14T07:50:00+12:00", 280 | earliestDate: "2020-08-14T07:50:00+12:00", 281 | }); 282 | await testTask.save(); 283 | const retrievedTasks = await TaskModel.find({ 284 | taskId: "4", 285 | reminderDate: { $lte: new Date(testTask.reminderDate.getTime() - 1) }, 286 | user: { $in: [testTask.user] }, 287 | isComplete: false, 288 | }).exec(); 289 | 290 | assert(retrievedTasks.length === 0); 291 | }); 292 | 293 | it("does not retrieve tasks which are completed", async function () { 294 | const testTask = new TaskModel({ 295 | user: "john@mail.com", 296 | taskId: "5", 297 | title: "title", 298 | description: "Re-Doing all the things", 299 | location: "science building", 300 | priority: 0, 301 | duration: 120, 302 | reminderDate: "2020-07-14T07:50:00+12:00", 303 | startDate: "2020-08-14T08:50:00+12:00", 304 | endDate: "2020-08-14T07:50:00+12:00", 305 | isComplete: true, 306 | travelTime: 20, 307 | dueDate: "2020-08-14T07:50:00+12:00", 308 | earliestDate: "2020-08-14T07:50:00+12:00", 309 | }); 310 | await testTask.save(); 311 | const retrievedTasks = await TaskModel.find({ 312 | taskId: "5", 313 | reminderDate: { $lte: new Date(testTask.reminderDate) }, 314 | user: { $in: [testTask.user] }, 315 | isComplete: false, 316 | }).exec(); 317 | 318 | assert(retrievedTasks.length === 0); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /doto-frontend/src/components/pages/Settings/SettingsPage.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from "react"; 2 | import { FormControl, Button, Input, InputAdornment, Grid } from "@material-ui/core"; 3 | import { MuiPickersUtilsProvider, KeyboardTimePicker } from "@material-ui/pickers"; 4 | import EmailIcon from "@material-ui/icons/Email"; 5 | import { AccountCircle } from "@material-ui/icons"; 6 | import PropTypes from "prop-types"; 7 | import "date-fns"; 8 | import DateFnsUtils from "@date-io/date-fns"; 9 | import Header from "../Header"; 10 | import DotoService from "../../../helpers/DotoService"; 11 | import { ThemeContext } from "../../../context/ThemeContext"; 12 | import MarketPlace from "../../MarketPlace"; 13 | import { ActiveHoursContext } from "../../../context/ActiveHoursContext"; 14 | import { Themes } from "../../../constants/Themes"; 15 | import "./SettingsPage.css"; 16 | import "../Pages.css"; 17 | import Points from "../../Points"; 18 | import { makeStyles } from "@material-ui/core/styles"; 19 | import { blue } from "@material-ui/core/colors"; 20 | import Dialog from "@material-ui/core/Dialog"; 21 | import DialogActions from "@material-ui/core/DialogActions"; 22 | import DialogContent from "@material-ui/core/DialogContent"; 23 | import DialogContentText from "@material-ui/core/DialogContentText"; 24 | import DialogTitle from "@material-ui/core/DialogTitle"; 25 | import { shiftTasks } from "../Calendar/TaskShifter"; 26 | 27 | const classnames = require("classnames"); 28 | 29 | // TODO: Use input name field and display it on the calendar header page as [name]'s calendar 30 | const InputNameField = props => { 31 | return ( 32 | 33 | 36 | 37 | 38 | } 39 | value={props.name} 40 | disabled={true} 41 | /> 42 | 43 | ); 44 | }; 45 | 46 | // TODO: Use this field is to add any other email address's calendars 47 | const InputEmailField = props => { 48 | return ( 49 | 50 | 53 | 54 | 55 | } 56 | value={props.email} 57 | disabled={true} 58 | /> 59 | 60 | ); 61 | }; 62 | 63 | const ProfilePhoto = props => { 64 | return ( 65 |
66 | {/* Profile photo is taken from the associated google account */} 67 | profile-pic-from-google 68 |
69 | ); 70 | }; 71 | 72 | // TODO: Implement logic for working hours in sync with task-scheduling algorithm 73 | const WorkingHoursPicker = props => { 74 | const [dialog, setDialog] = useState(false); 75 | 76 | const handleClickOpen = () => { 77 | setDialog(true); 78 | }; 79 | 80 | const handleClose = () => { 81 | setDialog(false); 82 | }; 83 | 84 | const handleCloseAndSave = () => { 85 | setDialog(false); 86 | props.saveChanges(props.startTime, props.endTime); 87 | }; 88 | 89 | const handleStartTimeChange = date => { 90 | props.changeStartTime(date); 91 | }; 92 | 93 | const handleEndTimeChange = date => { 94 | props.changeEndTime(date); 95 | }; 96 | 97 | return ( 98 | 99 |

Working Hours:

100 |
101 | 102 | 111 | 112 |
113 |

to

114 |
115 | 116 | 125 | 126 |
127 |
128 | 131 | 137 | {"Want to save those changes?"} 138 | 139 | 140 | Your time-table will be re-managed automatically. Please check again. 141 | 142 | 143 | 144 | 147 | 150 | 151 | 152 |
153 |
154 | ); 155 | }; 156 | 157 | const useStyles = makeStyles(theme => ({ 158 | blue: { 159 | color: theme.palette.getContrastText(blue[500]), 160 | backgroundColor: blue[500], 161 | boxShadow: theme.shadows[5], 162 | marginLeft: "10vw", 163 | }, 164 | })); 165 | 166 | // Using props to change the colour theme of the webpage when changed by the user 167 | const ThemePicker = props => { 168 | const classes = useStyles(); 169 | 170 | const handleThemeClick = (themeColour, cost) => { 171 | // @params themeColour and cost 172 | 173 | switch (themeColour) { 174 | case "blue": 175 | props.changeTheme(Themes.DARK); 176 | break; 177 | case "green": 178 | props.changeTheme(Themes.LIGHT); 179 | break; 180 | 181 | case "gray": 182 | break; 183 | case "magenta": 184 | break; 185 | case "purple": 186 | break; 187 | case "crimson": 188 | break; 189 | case "black": 190 | break; 191 | case "red": 192 | break; 193 | case "darkSeaGreen": 194 | break; 195 | case "antiqueWhite": 196 | break; 197 | case "darkKhaki": 198 | break; 199 | case "darkSlateBlue": 200 | break; 201 | default: 202 | break; 203 | } 204 | }; 205 | 206 | return ( 207 |
208 |

Available Points:

209 | 210 |

211 |
212 |

Theme:

213 | 214 | 219 |
220 |
221 | ); 222 | }; 223 | 224 | const SettingsPage = () => { 225 | const [theme, setTheme] = useContext(ThemeContext); 226 | const { activeHoursStart, activeHoursEnd } = useContext(ActiveHoursContext); 227 | const [profilePic, setProfilePic] = useState(); 228 | const [name, setName] = useState(); 229 | const [email, setEmail] = useState(); 230 | const [startTime, setStartTime] = activeHoursStart; 231 | const [endTime, setEndTime] = activeHoursEnd; 232 | const [userPoints, setUserPoints] = useState(0); 233 | const [unlockedItems, setUnlockedItems] = useState([]); 234 | const [tasks, setTasks] = useState([]); 235 | 236 | useEffect(() => { 237 | const fetchTasks = async () => { 238 | const tasks = await DotoService.getTasks(); 239 | setTasks(tasks); 240 | }; 241 | const fetchUserInfo = async () => { 242 | const userInfo = await DotoService.getUserInfo(); 243 | setTheme(userInfo.themePreference); 244 | setProfilePic(userInfo.picture); 245 | setName(userInfo.name); 246 | setEmail(userInfo.email); 247 | setStartTime(userInfo.startTime); 248 | setEndTime(userInfo.endTime); 249 | setUserPoints(userInfo.points); 250 | setUnlockedItems(userInfo.unlockedItems || []); 251 | }; 252 | fetchUserInfo(); 253 | fetchTasks(); 254 | }, [setTheme, setStartTime, setEndTime]); 255 | 256 | const changeTheme = newTheme => { 257 | DotoService.updateUserInfo({ themePreference: newTheme, startTime, endTime }).then(setTheme(newTheme)); 258 | }; 259 | 260 | const changeStartTime = newTime => { 261 | setStartTime(newTime); 262 | }; 263 | 264 | const changeEndTime = newTime => { 265 | setEndTime(newTime); 266 | }; 267 | 268 | const saveChanges = async (newStartTime, newEndTime) => { 269 | await DotoService.updateUserInfo({ startTime: newStartTime, endTime: newEndTime }); 270 | const { shiftedTasks } = shiftTasks(tasks, new Date(newStartTime), new Date(newEndTime)); 271 | for (let i = 0; i < shiftedTasks.length; i++) { 272 | await DotoService.updateTask(shiftedTasks[i]); 273 | } 274 | }; 275 | 276 | const handleBuyItem = async (cost, item) => { 277 | const newPointsBalance = userPoints - cost; 278 | 279 | // prevent user from purchasing items they cannot afford 280 | if (newPointsBalance >= 0) { 281 | const newUnlockedItems = [...unlockedItems, item]; 282 | await DotoService.updateUserInfo({ points: newPointsBalance, unlockedItems: newUnlockedItems }); 283 | setUserPoints(newPointsBalance); 284 | setUnlockedItems(newUnlockedItems); 285 | } 286 | // TODO: Display error message explaining why user cannot buy the item 287 | }; 288 | 289 | return ( 290 |
291 |
297 | 298 |
299 |
305 | 306 | 307 | 308 | 309 | 315 | 322 |
323 | 324 |
325 | ); 326 | }; 327 | 328 | ThemePicker.propTypes = { 329 | changeTheme: PropTypes.func.isRequired, 330 | }; 331 | 332 | ProfilePhoto.propTypes = { 333 | profilePic: PropTypes.string.isRequired, 334 | }; 335 | 336 | InputNameField.propTypes = { 337 | name: PropTypes.string.isRequired, 338 | }; 339 | 340 | InputEmailField.propTypes = { 341 | email: PropTypes.string.isRequired, 342 | }; 343 | 344 | export default SettingsPage; 345 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doto 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | ![](https://github.com/se701g2/Doto/workflows/doto-CI/badge.svg?event=push) 7 | ![](https://github.com/se701g2/Doto/workflows/doto-backend-deploy/badge.svg?event=push) 8 | ![](https://github.com/se701g2/Doto/workflows/doto-frontend-deploy/badge.svg?event=push) 9 | 10 | Welcome to Doto. The open-source software (OSS) project for a smart scheduling calendar app. Doto is an online calendar and to-do app. It has all of the basic functionality of any calendar app and can be used to make to-do lists. It also has smart scheduling capabilities, meaning that if the user wants to do a task, it can input it into our app and the app will allocate this task in a suitable time in the user’s calendar. Doto uses Google to sign up, meaning that to use this app, the user must have a google account. The development of Doto is done using the M.E.R.N (MongoDB, Express, React, Node) tech stack (more info about the tech stack can be found in the Wiki). 11 | 12 | ## Why is this project Useful? 13 | This project is useful as it has the basic functionalities of any normal calendar app, but also has the added functionality of smart scheduling. This is particularly useful for people who like to plan their day but find it difficult to plan it well, as the app finds the most suitable time for whatever task the user wants to accomplish. It also encourages people who don’t usually like to plan their day to start planning their day as all they need to do is input the task and the app will choose the perfect time, no effort required. 14 | 15 | 16 | ## Development Setup 17 | It is recommended that you use **VsCode** when contributing to this project. Please install the **Eslint** and **Prettier** extensions so that the code style matches what has already been done before. 18 | 19 | ## Setting up the environment variables 20 | 21 | There are a number of application secrets and credentials which are needed before you begin development. These secrets will be given to you by the Repo maintainer when you start contributing. To set up these envrionment variables you will need to make a `.env` file sitting in the doto-backend folder. An example of this file can be found in the repository `.env.example` contains all the variables that will need to be set (just make a copy of the file and rename to `.env` then copy and paste all the secrets given by the repo maintainer) 22 | 23 | The VAPID keys can be generated by running: 24 | ``` 25 | npx web-push generate-vapid-keys 26 | ``` 27 | Copy the public and private keys to the backend `.env` then copy the public key to the frontend `.env`. 28 | 29 | ## Running the code 30 | We are using [lerna](https://lerna.js.org/) and [yarn](https://yarnpkg.com/) for this project. 31 | 32 | ### Installing dependencies 33 | Run `yarn install --frozen-lockfile` in the project root. 34 | 35 | ### Starting the frontend and backend 36 | To get the project running locally, run `yarn start` in the project root. 37 | 38 | By default, the react-app is hosted on port 3000 and the local server is hosted on port 3001. Do not change these numbers as we have added the addresses as authorized redirect uri's in our google credentials. 39 | 40 | Please check the front end [readme](https://github.com/se701g2/Doto/blob/master/doto-frontend/README.md) for more information on running the front end of the code. 41 | 42 | ### Link to the website 43 | [doto.azurewebsites.net](https://doto.azurewebsites.net) 44 | 45 | ### Link to the API 46 | [doto-backend.azurewebsites.net](https://doto-backend.azurewebsites.net) 47 | 48 | ## Contributing to this Project 49 | Please refer to our [wiki](https://github.com/se701g2/Doto/wiki) for an outline of our coding conventions and git workflow 50 | 51 | ## Meta 52 | Se701 Group2 - Se701group2@gmail.com 53 | Distributed under the MIT license. Check the [wiki](https://github.com/se701g2/Doto/wiki/license) for more details 54 | 55 | ## Where to Get More Help 56 | If you have run into any issues, you can contact our lecturer Kelly Blincoe and she will be able to point you in the right direction. 57 | 58 | ## Contributors ✨ 59 | 60 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 61 | 62 | If you're a developer in the project and would like to add yourself here, please follow the instructions [here](https://github.com/se701g2/Doto/wiki/All-Contributors-Bot). 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |

Kimberley

💻 👀 🎨 🤔

Alex Monk

💻 ⚠️ 📖

Matt Eden

💻 👀 🎨 📖 ⚠️ 🐛

Jordan Sim-Smith

💻 👀 🎨 📖

Xiaoji Sun

💻 🎨

Preet Patel

💻 👀 🎨 📖 📹 🐛

Eric Pedrido

💻 👀 🎨 ⚠️

Harman Lamba

💻 👀 🎨 ⚠️

salma-s

💻 👀 🎨 ⚠️

Tony Liu

💻 👀 🎨 📖 ⚠️

Harrison Leach

💻 👀 🎨 ⚠️

Chinmay Deshmukh

💻 👀 📖

TCHE614

💻 👀 📖

brianzhang310

💻 👀 📖

nikotj1

🐛 🎨

Finn

🐛 💻

utri092

💻 🎨

rmoradc

💻 👀 🎨
93 | 94 | 95 | 96 | 97 | 98 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! --------------------------------------------------------------------------------