├── 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 |
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 |

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