├── .editorconfig ├── Makefile ├── Procfile ├── README.md ├── api ├── .gitignore ├── Dockerfile ├── config │ └── config.json ├── database │ └── create_database.sql ├── jest.config.js ├── migrations │ ├── 20210407041030-create-user.js │ ├── 20210407202910-create-team.js │ ├── 20210407205329-create-space.js │ ├── 20210408002327-create-member.js │ ├── 20210408004821-create-board.js │ ├── 20210408014455-create-team-invite.js │ ├── 20210408034749-create-project.js │ ├── 20210408035925-create-group.js │ ├── 20210408130119-create-assignment.js │ └── 20210409010232-create-comment.js ├── package.json ├── scripts │ └── mongo_to_sequelize.js ├── src │ ├── app │ │ ├── controllers │ │ │ ├── assignments.ts │ │ │ ├── boards.ts │ │ │ ├── comments.ts │ │ │ ├── groups.ts │ │ │ ├── projects.ts │ │ │ ├── spaces.ts │ │ │ ├── team-invites.ts │ │ │ ├── team.ts │ │ │ └── user.ts │ │ ├── models │ │ │ ├── mongo │ │ │ │ ├── assignment.js │ │ │ │ ├── board.js │ │ │ │ ├── comment.js │ │ │ │ ├── group.js │ │ │ │ ├── project.js │ │ │ │ ├── space.js │ │ │ │ ├── team-invite.js │ │ │ │ ├── team.js │ │ │ │ └── user.js │ │ │ └── sequelize │ │ │ │ ├── assignment.js │ │ │ │ ├── board.js │ │ │ │ ├── comment.js │ │ │ │ ├── group.js │ │ │ │ ├── index.js │ │ │ │ ├── member.js │ │ │ │ ├── project.js │ │ │ │ ├── space.js │ │ │ │ ├── team-invite.js │ │ │ │ ├── team.js │ │ │ │ └── user.js │ │ ├── repositories │ │ │ ├── assignment-repository-interface.ts │ │ │ ├── board-repository-interface.ts │ │ │ ├── comment-repository-interface.ts │ │ │ ├── group-repository-interface.ts │ │ │ ├── member-repository-interface.ts │ │ │ ├── mongo │ │ │ │ ├── assignment-repository.ts │ │ │ │ ├── board-repository.ts │ │ │ │ ├── comment-repository.ts │ │ │ │ ├── group-repository.ts │ │ │ │ ├── project-repository.ts │ │ │ │ ├── space-repository.ts │ │ │ │ ├── team-invite-repository.ts │ │ │ │ ├── team-repository.ts │ │ │ │ └── user-repository.ts │ │ │ ├── project-repository-interface.ts │ │ │ ├── sequelize │ │ │ │ ├── assignment-repository.ts │ │ │ │ ├── board-repository.ts │ │ │ │ ├── comment-repository.ts │ │ │ │ ├── group-repository.ts │ │ │ │ ├── member-repository.ts │ │ │ │ ├── project-repository.ts │ │ │ │ ├── space-repository.ts │ │ │ │ ├── team-invite-repository.ts │ │ │ │ ├── team-repository.ts │ │ │ │ └── user-repository.ts │ │ │ ├── space-repository-interface.ts │ │ │ ├── team-invite-repository-interface.ts │ │ │ ├── team-repository-interface.ts │ │ │ └── user-repository-interface.ts │ │ ├── services │ │ │ ├── assignment-service.ts │ │ │ ├── board-service.ts │ │ │ ├── comment-service.ts │ │ │ ├── group-service.ts │ │ │ ├── project-service.ts │ │ │ ├── space-service.ts │ │ │ ├── team-invite-service.ts │ │ │ ├── team-service.ts │ │ │ └── user-service.ts │ │ └── util │ │ │ ├── create-auth0-user.ts │ │ │ └── reset-password.ts │ └── index.ts ├── tests │ └── board-service.test.ts ├── tsconfig.json └── webpack.config.js ├── data └── .gitkeep ├── docker-compose.yml ├── kanception ├── .babelrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── package.json ├── public │ ├── _redirects │ ├── favicon.png │ ├── index.html │ ├── logo.icns │ ├── manifest.json │ └── robots.txt ├── sample.env └── src │ ├── App.css │ ├── App.js │ ├── app │ └── store.js │ ├── assignment-cell.css │ ├── assignment-cell.js │ ├── assignment-list.css │ ├── assignment-list.js │ ├── auth_config.json │ ├── constants.js │ ├── electron.js │ ├── features │ ├── assignments │ │ └── assignmentsSlice.js │ ├── comments │ │ └── commentsSlice.js │ ├── gantt │ │ ├── gantt-canvas.js │ │ ├── gantt.css │ │ └── gantt.js │ ├── kanban │ │ ├── arrow.png │ │ ├── card-member-view.css │ │ ├── card-member-view.js │ │ ├── card.css │ │ ├── card.js │ │ ├── comment-box-submit.css │ │ ├── comment-box-submit.js │ │ ├── comment-box.css │ │ ├── comment-box.js │ │ ├── comments-view.css │ │ ├── comments-view.js │ │ ├── context-menu.js │ │ ├── group.js │ │ ├── icon-comment.png │ │ ├── kanban-container.js │ │ ├── kanban.css │ │ ├── kanban.js │ │ ├── kanbanSlice.js │ │ ├── new-card-popup.css │ │ └── new-card-popup.js │ ├── list-view │ │ ├── list-view.css │ │ └── list-view.js │ ├── projects │ │ └── projectsSlice.js │ ├── spaces │ │ └── spacesSlice.js │ └── teams │ │ └── teamsSlice.js │ ├── index.css │ ├── index.js │ ├── invite-table-view.js │ ├── login.js │ ├── logout.js │ ├── members-view.css │ ├── members-view.js │ ├── menu.js │ ├── mode-dark.svg │ ├── mode-light.svg │ ├── new-cards-table-view.js │ ├── private-route.js │ ├── react-auth0-spa.js │ ├── router.js │ ├── side-menu.css │ ├── side-menu.js │ ├── space.css │ ├── space.js │ ├── sub-toolbar.css │ ├── sub-toolbar.js │ ├── table-view.css │ ├── team-table-view.js │ ├── theme-picker.css │ ├── theme-picker.js │ ├── theme.js │ ├── toolbar.css │ ├── toolbar.js │ ├── tree-view.js │ └── utils │ └── history.js ├── license.txt ├── marketing ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json └── src │ ├── components │ ├── header.js │ ├── image.js │ ├── layout.css │ ├── layout.js │ └── seo.js │ ├── images │ ├── gatsby-icon.png │ ├── kanception.png │ └── logo.png │ └── pages │ ├── 404.js │ ├── demo.mp4 │ ├── gantt.mp4 │ ├── gantt.png │ ├── graph.png │ ├── index.css │ ├── index.js │ ├── page-2.js │ ├── roadmap.png │ └── using-typescript.tsx └── mock ├── assignment-repository.ts ├── board-repository.ts ├── comment-repository.ts ├── group-repository.ts └── user-repository.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Match all files 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | electron: 2 | cd kanception && electron-packager . --overwrite --icon=public/logo.icns 3 | test: 4 | docker exec -it kanception-api npm run test 5 | clean: 6 | docker-compose stop kanception-api kanception && docker-compose rm kanception-api kanception && rm -rf api/node_modules kanception/node_modules && rm api/package-lock.json kanception/package-lock.json 7 | migrate: 8 | docker exec -it kanception-api npx sequelize-cli db:migrate 9 | heroku-migrate: 10 | heroku run npx sequelize-cli db:migrate --env production --app kanception -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/Procfile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kanception (Now Open Source) 2 | ![Logo](https://github.com/hpennington/kanception/blob/master/marketing/src/images/logo.png) 3 | 4 | www.kanception.io 5 | 6 | ![Logo](https://github.com/hpennington/kanception/blob/master/marketing/src/images/kanception.png) 7 | 8 | ## Contributing 9 | 10 | Come work on this project with us! It needs security hardening, performance optimizations, and lots of features added. Don't hesitate to make a PR! 11 | 12 | The tech stack is React, NodeJS, & Postgres. AKA the PERN stack. 13 | 14 | ### Roadmap 15 | 16 | - Add tests 17 | - Add realtime features 18 | 19 | ### Getting started 20 | 21 | - Create an account @ https://kanception.io 22 | - git clone https://github.com/hpennington/kanception.git 23 | - cd kanception 24 | - cp kanception/sample.env kanception/.env 25 | - docker-compose up 26 | - goto localhost:3000 27 | 28 | ### Electron 29 | - make electron 30 | -------------------------------------------------------------------------------- /api/.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 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update && apt-get install -y npm nodejs 5 | 6 | RUN groupadd -r ubuntu && useradd -r -g ubuntu ubuntu && \ 7 | mkdir /home/ubuntu && chown ubuntu:ubuntu /home/ubuntu 8 | 9 | USER ubuntu 10 | RUN mkdir -p /home/ubuntu/api 11 | WORKDIR /home/ubuntu/api 12 | COPY package*.json ./ 13 | RUN npm install 14 | 15 | COPY --chown=ubuntu:ubuntu ./ /home/ubuntu/api 16 | CMD ["npm", "run", "dev"] 17 | -------------------------------------------------------------------------------- /api/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "postgres", 4 | "password": "password", 5 | "database": "database_development", 6 | "host": "postgres", 7 | "dialect": "postgres" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": null, 12 | "database": "database_test", 13 | "host": "127.0.0.1", 14 | "dialect": "postgres" 15 | }, 16 | "production": { 17 | "use_env_variable": "DATABASE_URL", 18 | "dialect": "postgres", 19 | "dialectOptions": { 20 | "ssl": { 21 | "require": true, 22 | "rejectUnauthorized": false 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/database/create_database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE database_development; -------------------------------------------------------------------------------- /api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /api/migrations/20210407041030-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Users', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING 11 | }, 12 | firstName: { 13 | allowNull: true, 14 | type: Sequelize.STRING 15 | }, 16 | lastName: { 17 | allowNull: true, 18 | type: Sequelize.STRING 19 | }, 20 | email: { 21 | allowNull: true, 22 | type: Sequelize.STRING 23 | }, 24 | active: { 25 | allowNull: false, 26 | type: Sequelize.BOOLEAN 27 | }, 28 | sub: { 29 | allowNull: true, 30 | type: Sequelize.STRING 31 | }, 32 | createdAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE 35 | }, 36 | updatedAt: { 37 | allowNull: false, 38 | type: Sequelize.DATE 39 | } 40 | }); 41 | }, 42 | down: async (queryInterface, Sequelize) => { 43 | await queryInterface.dropTable('Users'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /api/migrations/20210407202910-create-team.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Teams', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING 11 | }, 12 | createdAt: { 13 | allowNull: false, 14 | type: Sequelize.DATE 15 | }, 16 | updatedAt: { 17 | allowNull: false, 18 | type: Sequelize.DATE 19 | }, 20 | }); 21 | 22 | }, 23 | down: async (queryInterface, Sequelize) => { 24 | await queryInterface.dropTable('Teams'); 25 | } 26 | }; -------------------------------------------------------------------------------- /api/migrations/20210407205329-create-space.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Spaces', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING 11 | }, 12 | title: { 13 | allowNull: true, 14 | type: Sequelize.STRING 15 | }, 16 | team: { 17 | allowNull: false, 18 | type: Sequelize.STRING 19 | }, 20 | owner: { 21 | allowNull: false, 22 | type: Sequelize.STRING 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | } 32 | }); 33 | }, 34 | down: async (queryInterface, Sequelize) => { 35 | await queryInterface.dropTable('Spaces'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /api/migrations/20210408002327-create-member.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Members', { 5 | _id: { 6 | allowNull: false, 7 | autoIncrement: false, 8 | primaryKey: true, 9 | type: Sequelize.STRING, 10 | }, 11 | team: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | user: { 16 | allowNull: false, 17 | type: Sequelize.STRING 18 | }, 19 | createdAt: { 20 | allowNull: false, 21 | type: Sequelize.DATE 22 | }, 23 | updatedAt: { 24 | allowNull: false, 25 | type: Sequelize.DATE 26 | } 27 | }); 28 | }, 29 | down: async (queryInterface, Sequelize) => { 30 | await queryInterface.dropTable('Members'); 31 | } 32 | }; -------------------------------------------------------------------------------- /api/migrations/20210408004821-create-board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Boards', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING, 11 | }, 12 | title: { 13 | type: Sequelize.STRING, 14 | allowNull: true 15 | }, 16 | description: { 17 | type: Sequelize.STRING, 18 | allowNull: true 19 | }, 20 | project: { 21 | type: Sequelize.STRING, 22 | allowNull: false 23 | }, 24 | owner: { 25 | type: Sequelize.STRING, 26 | allowNull: false 27 | }, 28 | parent: { 29 | type: Sequelize.STRING, 30 | allowNull: true 31 | }, 32 | group: { 33 | type: Sequelize.STRING, 34 | allowNull: true 35 | }, 36 | order: { 37 | type: Sequelize.INTEGER, 38 | allowNull: false 39 | }, 40 | start: { 41 | type: Sequelize.INTEGER, 42 | allowNull: true 43 | }, 44 | end: { 45 | type: Sequelize.INTEGER, 46 | allowNull: true 47 | }, 48 | count: { 49 | type: Sequelize.INTEGER, 50 | allowNull: false 51 | }, 52 | comments: { 53 | type: Sequelize.BOOLEAN, 54 | allowNull: false 55 | }, 56 | createdAt: { 57 | allowNull: false, 58 | type: Sequelize.DATE 59 | }, 60 | updatedAt: { 61 | allowNull: false, 62 | type: Sequelize.DATE 63 | } 64 | }); 65 | }, 66 | down: async (queryInterface, Sequelize) => { 67 | await queryInterface.dropTable('Boards'); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /api/migrations/20210408014455-create-team-invite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('TeamInvites', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING, 11 | }, 12 | team: { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }, 16 | invitee: { 17 | type: Sequelize.STRING, 18 | allowNull: false 19 | }, 20 | createdAt: { 21 | allowNull: false, 22 | type: Sequelize.DATE 23 | }, 24 | updatedAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE 27 | } 28 | }); 29 | }, 30 | down: async (queryInterface, Sequelize) => { 31 | await queryInterface.dropTable('TeamInvites'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /api/migrations/20210408034749-create-project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Projects', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING, 11 | }, 12 | title: { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }, 16 | space: { 17 | type: Sequelize.STRING, 18 | allowNull: false 19 | }, 20 | owner: { 21 | type: Sequelize.STRING, 22 | allowNull: false 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | } 32 | }); 33 | }, 34 | down: async (queryInterface, Sequelize) => { 35 | await queryInterface.dropTable('Projects'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /api/migrations/20210408035925-create-group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Groups', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING, 11 | }, 12 | title: { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }, 16 | board: { 17 | type: Sequelize.STRING, 18 | allowNull: true 19 | }, 20 | owner: { 21 | type: Sequelize.STRING, 22 | allowNull: true 23 | }, 24 | order: { 25 | type: Sequelize.INTEGER, 26 | allowNull: false 27 | }, 28 | createdAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE 35 | } 36 | }); 37 | }, 38 | down: async (queryInterface, Sequelize) => { 39 | await queryInterface.dropTable('Groups'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /api/migrations/20210408130119-create-assignment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Assignments', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING, 11 | }, 12 | assignee: { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }, 16 | assigner: { 17 | type: Sequelize.STRING, 18 | allowNull: false 19 | }, 20 | board: { 21 | type: Sequelize.STRING, 22 | allowNull: false 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | } 32 | }); 33 | }, 34 | down: async (queryInterface, Sequelize) => { 35 | await queryInterface.dropTable('Assignments'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /api/migrations/20210409010232-create-comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('Comments', { 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: Sequelize.STRING, 11 | }, 12 | text: { 13 | type: Sequelize.STRING, 14 | allowNull: true 15 | }, 16 | owner: { 17 | type: Sequelize.STRING, 18 | allowNull: false 19 | }, 20 | board: { 21 | type: Sequelize.STRING, 22 | allowNull: false 23 | }, 24 | timestamp: { 25 | type: Sequelize.STRING, 26 | allowNull: false 27 | }, 28 | createdAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE 35 | } 36 | }); 37 | }, 38 | down: async (queryInterface, Sequelize) => { 39 | await queryInterface.dropTable('Comments'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/index.ts", 6 | "scripts": { 7 | "build": "webpack .", 8 | "start": "node ./dist/bundle.js", 9 | "dev": "nodemon ./src/index.ts", 10 | "test": "jest", 11 | "heroku-postbuild": "webpack --config ./webpack.config.js --progress" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@types/express": "^4.17.11", 18 | "@types/node": "^14.14.37", 19 | "@types/sequelize": "^4.28.9", 20 | "aws-sdk": "^2.691.0", 21 | "babel-loader": "^8.1.0", 22 | "body-parser": "^1.19.0", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.1", 25 | "express-jwt": "^5.3.3", 26 | "jwks-rsa": "^2.0.2", 27 | "jwt-decode": "^3.1.2", 28 | "mongoose": "^5.9.14", 29 | "net": "^1.0.2", 30 | "node-fetch": "^2.6.0", 31 | "path": "^0.12.7", 32 | "pg": "^8.5.1", 33 | "pg-hstore": "^2.3.3", 34 | "sendmail": "^1.6.1", 35 | "sequelize": "^6.6.2", 36 | "socket.io": "^4.0.1", 37 | "socketio-jwt": "^4.6.2", 38 | "stream": "^0.0.2", 39 | "stream-http": "^3.1.1", 40 | "tls": "^0.0.1", 41 | "ts-jest": "^26.5.4", 42 | "ts-node": "^9.1.1", 43 | "util": "^0.12.3", 44 | "uuid": "^8.1.0", 45 | "uuidv4": "^6.1.0" 46 | }, 47 | "devDependencies": { 48 | "@types/cors": "^2.8.10", 49 | "@types/jest": "^26.0.22", 50 | "fs": "^0.0.1-security", 51 | "jest": "^26.6.3", 52 | "nodemon": "^2.0.7", 53 | "ts-jest": "^26.5.4", 54 | "ts-loader": "^8.1.0", 55 | "typescript": "^4.2.3", 56 | "webpack": "^5.30.0", 57 | "webpack-cli": "^4.6.0", 58 | "webpack-node-externals": "^2.5.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /api/src/app/controllers/assignments.ts: -------------------------------------------------------------------------------- 1 | import AssignmentService from '../services/assignment-service' 2 | 3 | class AssignmentController { 4 | private assignmentService: AssignmentService 5 | 6 | constructor(assignmentService: AssignmentService) { 7 | this.assignmentService = assignmentService 8 | 9 | this.createAssignment = this.createAssignment.bind(this) 10 | this.readAssignments = this.readAssignments.bind(this) 11 | this.deleteAssignment = this.deleteAssignment.bind(this) 12 | } 13 | 14 | public async createAssignment(req, res) { 15 | 16 | const assignee = req.query.assignee 17 | const board = req.query.board 18 | const sub = req.user.sub 19 | 20 | try { 21 | 22 | const assignment = await this.assignmentService.createAssignment(sub, assignee, board) 23 | res.send(assignment) 24 | 25 | } catch(error) { 26 | console.log(error) 27 | res.sendStatus(500) 28 | } 29 | } 30 | 31 | public async readAssignments(req, res) { 32 | try { 33 | const sub = req.user.sub 34 | const assignments = await this.assignmentService.readAssignments(sub) 35 | res.send(assignments) 36 | } catch(error) { 37 | console.log(error) 38 | res.sendStatus(500) 39 | } 40 | } 41 | 42 | public async deleteAssignment(req, res) { 43 | const board = req.query.board 44 | const assignee = req.query.assignee 45 | 46 | try { 47 | 48 | const result = await this.assignmentService.deleteAssignment(board, assignee) 49 | 50 | res.sendStatus(204) 51 | 52 | } catch(error) { 53 | console.log(error) 54 | res.sendStatus(500) 55 | } 56 | } 57 | } 58 | 59 | export default AssignmentController -------------------------------------------------------------------------------- /api/src/app/controllers/boards.ts: -------------------------------------------------------------------------------- 1 | import BoardService from '../services/board-service' 2 | 3 | class BoardController { 4 | private boardService: BoardService 5 | 6 | constructor(boardService: BoardService) { 7 | this.boardService = boardService 8 | 9 | this.createBoard = this.createBoard.bind(this) 10 | this.readTree = this.readTree.bind(this) 11 | this.updateBoard = this.updateBoard.bind(this) 12 | this.deleteBoard = this.deleteBoard.bind(this) 13 | } 14 | 15 | public async createBoard(req, res) { 16 | try { 17 | 18 | const project = req.query.project 19 | const group = req.query.group 20 | const parent = req.query.parent 21 | const sub = req.user.sub 22 | 23 | const board = await this.boardService.createBoard(project, group, parent, sub) 24 | 25 | res.send(board) 26 | 27 | } catch(error) { 28 | console.log(error) 29 | res.sendStatus(500) 30 | } 31 | } 32 | 33 | public async readTree(req, res) { 34 | try { 35 | 36 | const project = req.query.project 37 | const sub = req.user.sub 38 | 39 | const nodes = await this.boardService.readTree(sub, project) 40 | 41 | res.send(nodes) 42 | 43 | } catch(error) { 44 | console.log(error) 45 | res.sendStatus(500) 46 | } 47 | } 48 | 49 | public async updateBoard(req, res) { 50 | 51 | try { 52 | const boardId = req.query.id 53 | const body = req.body 54 | 55 | await this.boardService.updateBoard(boardId, body) 56 | 57 | res.sendStatus(201) 58 | 59 | } catch(error) { 60 | console.log(error) 61 | res.sendStatus(500) 62 | } 63 | 64 | } 65 | 66 | public async deleteBoard(req, res) { 67 | const id = req.query.id 68 | const sub = req.user.sub 69 | 70 | try { 71 | 72 | const success = await this.boardService.deleteBoard(id, sub) 73 | 74 | if (success === true) { 75 | res.sendStatus(201) 76 | } else { 77 | res.sendStatus(403) 78 | } 79 | 80 | } catch(error) { 81 | console.log(error) 82 | res.sendStatus(500) 83 | } 84 | } 85 | } 86 | 87 | export default BoardController -------------------------------------------------------------------------------- /api/src/app/controllers/comments.ts: -------------------------------------------------------------------------------- 1 | import CommentService from '../services/comment-service' 2 | import SocketIO from 'socket.io' 3 | 4 | class CommentController { 5 | private commentService: CommentService 6 | private io: any 7 | 8 | constructor(commentService: CommentService, io: any) { 9 | this.commentService = commentService 10 | this.io = io 11 | 12 | this.createComment = this.createComment.bind(this) 13 | this.readComments = this.readComments.bind(this) 14 | } 15 | 16 | public async createComment(req, res) { 17 | try { 18 | const text = req.query.text 19 | const boardId = req.query.board 20 | const sub = req.user.sub 21 | 22 | const comment = await this.commentService.createComment(text, boardId, sub) 23 | this.io.to(boardId + '_comments').emit('create_comment', {comment}) 24 | res.send(comment) 25 | 26 | } catch(error) { 27 | console.log(error) 28 | res.sendStatus(500) 29 | } 30 | } 31 | 32 | public async readComments(req, res) { 33 | try { 34 | const boardId = req.query.board 35 | const sub = req.user.sub 36 | 37 | const comments = await this.commentService.readComments(sub, boardId) 38 | 39 | if (comments != null) { 40 | res.send(comments) 41 | } else { 42 | res.sendStatus(500) 43 | } 44 | 45 | } catch(error) { 46 | console.log(error) 47 | res.sendStatus(500) 48 | } 49 | } 50 | } 51 | 52 | export default CommentController 53 | -------------------------------------------------------------------------------- /api/src/app/controllers/groups.ts: -------------------------------------------------------------------------------- 1 | import GroupService from '../services/group-service' 2 | 3 | class GroupController { 4 | private groupService: GroupService 5 | 6 | constructor(groupService: GroupService) { 7 | this.groupService = groupService 8 | 9 | this.createGroup = this.createGroup.bind(this) 10 | this.readGroups = this.readGroups.bind(this) 11 | this.updateGroup = this.updateGroup.bind(this) 12 | this.deleteGroup = this.deleteGroup.bind(this) 13 | } 14 | 15 | public async createGroup(req, res) { 16 | try { 17 | const boardId = req.query.board 18 | const sub = req.user.sub 19 | 20 | const group = await this.groupService.createGroup(sub, boardId) 21 | 22 | res.send(group) 23 | } catch(error) { 24 | console.log(error) 25 | } 26 | } 27 | 28 | public async readGroups(req, res) { 29 | try { 30 | const boardId = req.query.board_id 31 | const groups = await this.groupService.readGroups(boardId) 32 | res.send(groups) 33 | } catch(error) { 34 | console.log(error) 35 | res.sendStatus(500) 36 | } 37 | } 38 | 39 | public async updateGroup(req, res) { 40 | try { 41 | const groupId = req.query.id 42 | const body = req.body 43 | const group = await this.groupService.updateGroup(groupId, body) 44 | res.sendStatus(201) 45 | 46 | } catch(error) { 47 | res.sendStatus(500) 48 | } 49 | 50 | } 51 | 52 | public async deleteGroup(req, res) { 53 | const id = req.query.id 54 | const sub = req.user.sub 55 | 56 | try { 57 | 58 | const result = await this.groupService.deleteGroup(sub, id) 59 | 60 | if (result === true) { 61 | res.sendStatus(201) 62 | } else { 63 | res.sendStatus(403) 64 | } 65 | 66 | } catch(error) { 67 | console.log(error) 68 | res.sendStatus(500) 69 | } 70 | } 71 | } 72 | 73 | export default GroupController -------------------------------------------------------------------------------- /api/src/app/controllers/projects.ts: -------------------------------------------------------------------------------- 1 | import ProjectService from '../services/project-service' 2 | 3 | class ProjectController { 4 | private projectService: ProjectService 5 | 6 | constructor(projectService: ProjectService) { 7 | this.projectService = projectService 8 | 9 | this.createProject = this.createProject.bind(this) 10 | this.readProjects = this.readProjects.bind(this) 11 | this.deleteProject = this.deleteProject.bind(this) 12 | } 13 | 14 | public async createProject(req, res) { 15 | try { 16 | 17 | const title = req.query.title 18 | const space = req.query.space 19 | const sub = req.user.sub 20 | 21 | const project = await this.projectService.createProject(sub, title, space) 22 | res.send(project) 23 | 24 | } catch(error) { 25 | console.log(error) 26 | res.send(500) 27 | } 28 | } 29 | 30 | public async readProjects(req, res) { 31 | try { 32 | const sub = req.user.sub 33 | const projects = await this.projectService.readProjects(sub) 34 | 35 | res.send(projects) 36 | 37 | } catch(error) { 38 | console.log(error) 39 | res.send(500) 40 | } 41 | } 42 | 43 | public async deleteProject(req, res) { 44 | try { 45 | const id = req.query.id 46 | const sub = req.user.sub 47 | const result = await this.projectService.deleteProject(sub, id) 48 | if (result === true) { 49 | res.sendStatus(200) 50 | } else { 51 | res.sendStatus(401) 52 | } 53 | 54 | } catch(error) { 55 | console.log(error) 56 | res.send(500) 57 | } 58 | } 59 | } 60 | 61 | export default ProjectController -------------------------------------------------------------------------------- /api/src/app/controllers/spaces.ts: -------------------------------------------------------------------------------- 1 | import SpaceService from '../services/space-service' 2 | 3 | class SpaceController { 4 | private spaceService: SpaceService 5 | 6 | constructor(spaceService: SpaceService) { 7 | this.spaceService = spaceService 8 | 9 | this.createSpace = this.createSpace.bind(this) 10 | this.readSpaces = this.readSpaces.bind(this) 11 | this.deleteSpace = this.deleteSpace.bind(this) 12 | } 13 | 14 | public async createSpace(req, res) { 15 | try { 16 | 17 | const title = req.query.title 18 | const sub = req.user.sub 19 | 20 | const space = await this.spaceService.createSpace(sub, title) 21 | res.send(space) 22 | 23 | } catch(error) { 24 | console.log(error) 25 | res.sendStatus(500) 26 | } 27 | } 28 | 29 | public async readSpaces(req, res) { 30 | try { 31 | const sub = req.user.sub 32 | const spaces = await this.spaceService.readSpaces(sub) 33 | 34 | res.send(spaces) 35 | 36 | } catch(error) { 37 | console.log(error) 38 | res.sendStatus(500) 39 | } 40 | } 41 | 42 | public async deleteSpace(req, res) { 43 | const id = req.query.id 44 | const sub = req.user.sub 45 | try { 46 | 47 | const result = await this.spaceService.deleteSpace(sub, id) 48 | 49 | 50 | if (result === false) { 51 | res.sendStatus(401) 52 | return 53 | } 54 | 55 | // res.sendState('201') 56 | 57 | } catch(error) { 58 | console.log(error) 59 | res.sendStatus(500) 60 | } 61 | } 62 | } 63 | 64 | export default SpaceController -------------------------------------------------------------------------------- /api/src/app/controllers/team-invites.ts: -------------------------------------------------------------------------------- 1 | import TeamInviteService from '../services/team-invite-service' 2 | 3 | class TeamInviteController { 4 | private teamInviteService: TeamInviteService 5 | 6 | constructor(teamInviteService: TeamInviteService) { 7 | this.teamInviteService = teamInviteService 8 | 9 | this.readTeamInvites = this.readTeamInvites.bind(this) 10 | this.createTeamInvite = this.createTeamInvite.bind(this) 11 | this.updateTeamInviteAccept = this.updateTeamInviteAccept.bind(this) 12 | this.deleteInvite = this.deleteInvite.bind(this) 13 | } 14 | 15 | public async readTeamInvites(req, res) { 16 | try { 17 | const sub = req.user.sub 18 | 19 | const teams = await this.teamInviteService.readTeamInvites(sub) 20 | 21 | res.send(teams) 22 | 23 | } catch(error) { 24 | console.log(error) 25 | res.sendStatus(500) 26 | } 27 | 28 | } 29 | 30 | public async createTeamInvite(req, res) { 31 | const first = req.query.first 32 | const last = req.query.last 33 | const email = req.query.email 34 | const team = req.query.team 35 | const token = req.headers.authorization 36 | 37 | try { 38 | 39 | const result = await this.teamInviteService.createTeamInvite(first, last, email, team, token) 40 | 41 | if (result === true) { 42 | res.sendStatus(201) 43 | } else { 44 | res.sendStatus(409) 45 | } 46 | 47 | } catch(error) { 48 | console.log(error) 49 | res.sendStatus(500) 50 | } 51 | } 52 | 53 | public async updateTeamInviteAccept(req, res) { 54 | try { 55 | const sub = req.user.sub 56 | const team = req.query.team 57 | 58 | const result = await this.teamInviteService.updateTeamInviteAccept(sub, team) 59 | res.sendStatus(result) 60 | 61 | } catch(error) { 62 | console.log(error) 63 | res.sendStatus(501) 64 | } 65 | } 66 | 67 | public async deleteInvite(req, res) { 68 | try { 69 | const sub = req.user.sub 70 | const team = req.query.team 71 | const result = await this.teamInviteService.deleteInvite(sub, team) 72 | 73 | if (result === true) { 74 | res.sendStatus(200) 75 | } else { 76 | res.sendStatus(500) 77 | } 78 | 79 | } catch(error) { 80 | console.log(error) 81 | res.sendStatus(500) 82 | } 83 | } 84 | } 85 | 86 | export default TeamInviteController -------------------------------------------------------------------------------- /api/src/app/controllers/team.ts: -------------------------------------------------------------------------------- 1 | import TeamService from '../services/team-service' 2 | 3 | class TeamController { 4 | private teamService: TeamService 5 | 6 | constructor(teamService: TeamService) { 7 | this.teamService = teamService 8 | 9 | this.createTeam = this.createTeam.bind(this) 10 | this.readTeamRootsChildren = this.readTeamRootsChildren.bind(this) 11 | this.readTeam = this.readTeam.bind(this) 12 | } 13 | 14 | public async createTeam(req, res) { 15 | try { 16 | const sub = req.user.sub 17 | const title = req.query.title 18 | const team = await this.teamService.createTeam(sub, title) 19 | res.send(team) 20 | 21 | } catch(error) { 22 | console.log(error) 23 | res.sendStatus(500) 24 | } 25 | } 26 | 27 | public async readTeamRootsChildren(req, res) { 28 | const team = req.query.team 29 | 30 | try { 31 | 32 | const root = await this.teamService.readTeamRootsChildren(team) 33 | 34 | if (root !== null) { 35 | res.send(root) 36 | } else { 37 | res.sendStatus(500) 38 | } 39 | 40 | } catch(error) { 41 | console.log(error) 42 | res.sendStatus(500) 43 | } 44 | } 45 | 46 | public async readTeam(req, res) { 47 | try { 48 | const sub = req.user.sub 49 | const teamId = req.query.team 50 | const team = await this.teamService.readTeam(sub, teamId) 51 | if (team !== null) { 52 | res.send(JSON.stringify(team)) 53 | } else { 54 | res.sendStatus(500) 55 | } 56 | 57 | } catch (error) { 58 | console.log(error) 59 | res.sendStatus(503) 60 | } 61 | } 62 | } 63 | 64 | export default TeamController -------------------------------------------------------------------------------- /api/src/app/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import UserService from '../services/user-service' 2 | 3 | class UserController { 4 | private userService: UserService 5 | 6 | constructor(userService: UserService) { 7 | this.userService = userService 8 | 9 | this.createUser = this.createUser.bind(this) 10 | this.readUser = this.readUser.bind(this) 11 | this.readProfiles = this.readProfiles.bind(this) 12 | this.updateName = this.updateName.bind(this) 13 | } 14 | 15 | public async createUser(req, res) { 16 | const sub = req.user.sub 17 | const token = req.headers.authorization 18 | 19 | const user = await this.userService.createUser(sub, token) 20 | res.send(user) 21 | 22 | } 23 | 24 | public async readUser(req, res) { 25 | try { 26 | const sub = req.user.sub 27 | console.log({sub}) 28 | const spaces = await this.userService.readUser(sub) 29 | res.send(spaces) 30 | } catch (error) { 31 | console.log(error) 32 | res.sendStatus(500) 33 | } 34 | } 35 | 36 | public async readProfiles(req, res) { 37 | const sub = req.user.sub 38 | const team = req.query.team 39 | 40 | if (team === null || team === undefined || team === 'undefined') { 41 | res.sendStatus(400) 42 | return 43 | } 44 | 45 | try { 46 | 47 | const profiles = await this.userService.readProfiles(sub, team) 48 | if (profiles !== null) { 49 | res.send(JSON.stringify(profiles)) 50 | } else { 51 | res.sendStatus(500) 52 | } 53 | 54 | } catch(error) { 55 | console.log(error) 56 | res.sendStatus(500) 57 | } 58 | } 59 | 60 | public async updateName(req, res) { 61 | try { 62 | 63 | const sub = req.user.sub 64 | const first = req.query.first 65 | const last = req.query.last 66 | const token = req.headers.authorization 67 | 68 | const user = await this.userService.updateName(sub, first, last, token) 69 | 70 | res.sendStatus(201) 71 | 72 | } catch(error) { 73 | console.log(error) 74 | res.sendStatus(500) 75 | } 76 | } 77 | } 78 | 79 | export default UserController -------------------------------------------------------------------------------- /api/src/app/models/mongo/assignment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const assignmentSchema = new mongoose.Schema({ 4 | assignee: String, 5 | assigner: String, 6 | board: String, 7 | }) 8 | 9 | const Assignment = new mongoose.model('Assignment', assignmentSchema) 10 | 11 | module.exports = Assignment -------------------------------------------------------------------------------- /api/src/app/models/mongo/board.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const boardSchema = new mongoose.Schema({ 4 | title: String, 5 | description: String, 6 | project: String, 7 | owner: String, 8 | parent: String, 9 | group: String, 10 | order: Number, 11 | start: Number, 12 | end: Number, 13 | count: Number, 14 | comments: Boolean, 15 | assignees: [String], 16 | }) 17 | 18 | const Board = new mongoose.model('Board', boardSchema) 19 | 20 | module.exports = Board -------------------------------------------------------------------------------- /api/src/app/models/mongo/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const commentSchema = new mongoose.Schema({ 4 | owner: String, 5 | board: String, 6 | text: String, 7 | timestamp: String, 8 | }) 9 | 10 | const Comment = new mongoose.model('Comment', commentSchema) 11 | 12 | module.exports = Comment -------------------------------------------------------------------------------- /api/src/app/models/mongo/group.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const groupSchema = new mongoose.Schema({ 4 | title: String, 5 | board: String, 6 | owner: String, 7 | order: Number, 8 | }) 9 | 10 | const Group = new mongoose.model('Group', groupSchema) 11 | 12 | module.exports = Group -------------------------------------------------------------------------------- /api/src/app/models/mongo/project.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const projectSchema = mongoose.Schema({ 4 | title: String, 5 | space: String, 6 | owner: String, 7 | }) 8 | 9 | const Project = new mongoose.model('Project', projectSchema) 10 | 11 | module.exports = Project -------------------------------------------------------------------------------- /api/src/app/models/mongo/space.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const spaceSchema = new mongoose.Schema({ 4 | title: String, 5 | team: String, 6 | owner: String, 7 | }) 8 | 9 | const Space = new mongoose.model('Space', spaceSchema) 10 | 11 | module.exports = Space -------------------------------------------------------------------------------- /api/src/app/models/mongo/team-invite.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const teamInviteSchema = new mongoose.Schema({ 4 | team: String, 5 | invitee: String, 6 | }) 7 | 8 | const TeamInvite = new mongoose.model('TeamInvite', teamInviteSchema) 9 | 10 | module.exports = TeamInvite -------------------------------------------------------------------------------- /api/src/app/models/mongo/team.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const teamSchema = new mongoose.Schema({ 4 | members: [String], 5 | }) 6 | 7 | const Team = new mongoose.model('Team', teamSchema) 8 | 9 | module.exports = Team -------------------------------------------------------------------------------- /api/src/app/models/mongo/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const userSchema = new mongoose.Schema({ 4 | email: String, 5 | sub: String, 6 | name: { 7 | first: String, 8 | last: String, 9 | }, 10 | spaces: [String], 11 | active: Boolean, 12 | }) 13 | 14 | const User = new mongoose.model('User', userSchema) 15 | 16 | module.exports = User -------------------------------------------------------------------------------- /api/src/app/models/sequelize/assignment.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class Assignment extends Model {} 5 | 6 | Assignment.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | primaryKey: true, 11 | type: DataTypes.STRING, 12 | }, 13 | assignee: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | assigner: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | board: { 22 | type: DataTypes.STRING, 23 | allowNull: false 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: DataTypes.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | } 33 | }, { 34 | sequelize, 35 | modelName: 'Assignment' 36 | }) 37 | 38 | return Assignment 39 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/board.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class Board extends Model {} 5 | 6 | Board.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | primaryKey: true, 11 | type: DataTypes.STRING, 12 | }, 13 | title: { 14 | type: DataTypes.STRING, 15 | allowNull: true 16 | }, 17 | description: { 18 | type: DataTypes.STRING, 19 | allowNull: true 20 | }, 21 | project: { 22 | type: DataTypes.STRING, 23 | allowNull: false 24 | }, 25 | owner: { 26 | type: DataTypes.STRING, 27 | allowNull: false 28 | }, 29 | parent: { 30 | type: DataTypes.STRING, 31 | allowNull: true 32 | }, 33 | group: { 34 | type: DataTypes.STRING, 35 | allowNull: true 36 | }, 37 | order: { 38 | type: DataTypes.INTEGER, 39 | allowNull: false 40 | }, 41 | start: { 42 | type: DataTypes.INTEGER, 43 | allowNull: true 44 | }, 45 | end: { 46 | type: DataTypes.INTEGER, 47 | allowNull: true 48 | }, 49 | count: { 50 | type: DataTypes.INTEGER, 51 | allowNull: false 52 | }, 53 | comments: { 54 | type: DataTypes.BOOLEAN, 55 | allowNull: false 56 | }, 57 | createdAt: { 58 | allowNull: false, 59 | type: DataTypes.DATE 60 | }, 61 | updatedAt: { 62 | allowNull: false, 63 | type: DataTypes.DATE 64 | } 65 | }, { 66 | sequelize, 67 | 68 | modelName: 'Board' 69 | }) 70 | 71 | Board.assignees = [] 72 | 73 | return Board 74 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/comment.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | module.exports = (sequelize, DataTypes) => { 3 | class Comment extends Model {} 4 | 5 | Comment.init({ 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: DataTypes.STRING, 11 | }, 12 | owner: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | }, 16 | board: { 17 | type: DataTypes.STRING, 18 | allowNull: false 19 | }, 20 | text: { 21 | type: DataTypes.STRING, 22 | allowNull: true 23 | }, 24 | timestamp: { 25 | type: DataTypes.STRING, 26 | allowNull: false 27 | }, 28 | createdAt: { 29 | allowNull: false, 30 | type: DataTypes.DATE 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: DataTypes.DATE 35 | } 36 | }, { 37 | sequelize, 38 | modelName: 'Comment' 39 | }) 40 | 41 | return Comment 42 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/group.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | module.exports = (sequelize, DataTypes) => { 3 | class Group extends Model {} 4 | 5 | Group.init({ 6 | _id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: DataTypes.STRING, 11 | }, 12 | title: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | }, 16 | board: { 17 | type: DataTypes.STRING, 18 | allowNull: true 19 | }, 20 | owner: { 21 | type: DataTypes.STRING, 22 | allowNull: true 23 | }, 24 | order: { 25 | type: DataTypes.INTEGER, 26 | allowNull: false 27 | }, 28 | createdAt: { 29 | allowNull: false, 30 | type: DataTypes.DATE 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: DataTypes.DATE 35 | } 36 | }, { 37 | sequelize, 38 | modelName: 'Group' 39 | }) 40 | 41 | return Group 42 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = require(__dirname + '/../../../../config/config.json')[env]; 9 | const db = {}; 10 | 11 | let sequelize; 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } 17 | 18 | const Assignment = require('./assignment')(sequelize, Sequelize.DataTypes) 19 | const Board = require('./board')(sequelize, Sequelize.DataTypes) 20 | const Comment = require('./comment')(sequelize, Sequelize.DataTypes) 21 | const Group = require('./group')(sequelize, Sequelize.DataTypes) 22 | const Member = require('./member')(sequelize, Sequelize.DataTypes) 23 | const Project = require('./project')(sequelize, Sequelize.DataTypes) 24 | const Space = require('./space')(sequelize, Sequelize.DataTypes) 25 | const TeamInvite = require('./team-invite')(sequelize, Sequelize.DataTypes) 26 | const Team = require('./team')(sequelize, Sequelize.DataTypes) 27 | const User = require('./user')(sequelize, Sequelize.DataTypes) 28 | 29 | db['Assignment'] = Assignment 30 | db['Board'] = Board 31 | db['Comment'] = Comment 32 | db['Group'] = Group 33 | db['Member'] = Member 34 | db['Project'] = Project 35 | db['Space'] = Space 36 | db['TeamInvite'] = TeamInvite 37 | db['Team'] = Team 38 | db['User'] = User 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /api/src/app/models/sequelize/member.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class Member extends Model {} 5 | 6 | Member.init({ 7 | _id: { 8 | allowNull: false, 9 | type: DataTypes.STRING, 10 | autoIncrement: false, 11 | primaryKey: true, 12 | }, 13 | team:{ 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | user:{ 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | }, 21 | }, { 22 | sequelize, 23 | modelName: 'Member', 24 | }) 25 | 26 | return Member 27 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/project.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class Project extends Model {} 5 | 6 | Project.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | primaryKey: true, 11 | type: DataTypes.STRING, 12 | }, 13 | title: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | space: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | owner: { 22 | type: DataTypes.STRING, 23 | allowNull: false 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: DataTypes.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | } 33 | }, { 34 | sequelize, 35 | modelName: 'Project' 36 | }) 37 | 38 | return Project 39 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/space.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class Space extends Model {} 5 | 6 | Space.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | primaryKey: true, 11 | type: DataTypes.STRING 12 | }, 13 | title: { 14 | type: DataTypes.STRING, 15 | allowNull: true 16 | }, 17 | team: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | owner: { 22 | type: DataTypes.STRING, 23 | allowNull: false 24 | }, 25 | }, { 26 | sequelize, 27 | modelName: 'Space' 28 | }) 29 | 30 | return Space 31 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/team-invite.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | module.exports = (sequelize, DataTypes) => { 3 | 4 | class TeamInvite extends Model {} 5 | 6 | TeamInvite.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | primaryKey: true, 11 | type: DataTypes.STRING, 12 | }, 13 | team: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | invitee: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | createdAt: { 22 | allowNull: false, 23 | type: DataTypes.DATE 24 | }, 25 | updatedAt: { 26 | allowNull: false, 27 | type: DataTypes.DATE 28 | } 29 | }, { 30 | sequelize, 31 | modelName: 'TeamInvite' 32 | }) 33 | 34 | return TeamInvite 35 | } 36 | -------------------------------------------------------------------------------- /api/src/app/models/sequelize/team.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class Team extends Model {} 5 | 6 | Team.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | primaryKey: true, 11 | type: DataTypes.STRING, 12 | }, 13 | }, { 14 | sequelize, 15 | modelName: 'Team' 16 | }) 17 | 18 | return Team 19 | } -------------------------------------------------------------------------------- /api/src/app/models/sequelize/user.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('sequelize') 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | class User extends Model {} 5 | 6 | User.init({ 7 | _id: { 8 | allowNull: false, 9 | autoIncrement: false, 10 | type: DataTypes.STRING, 11 | primaryKey: true 12 | }, 13 | firstName: { 14 | type: DataTypes.STRING, 15 | allowNull: true, 16 | }, 17 | lastName: { 18 | type: DataTypes.STRING, 19 | allowNull: true, 20 | }, 21 | email: { 22 | type: DataTypes.STRING, 23 | allowNull: true, 24 | }, 25 | active: DataTypes.BOOLEAN, 26 | sub: { 27 | type: DataTypes.STRING, 28 | allowNull: true, 29 | } 30 | }, { 31 | sequelize, 32 | modelName: 'User', 33 | }) 34 | 35 | return User 36 | } -------------------------------------------------------------------------------- /api/src/app/repositories/assignment-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Assignment } from '../models/sequelize' 2 | import { Board } from '../models/sequelize' 3 | 4 | interface AssignmentRepositoryInterface { 5 | findAll(criteria): Promise>; 6 | findOne(criteria): Promise; 7 | create(assignee, assigner, board): Promise; 8 | deleteMany(criteria); 9 | deleteOne(criteria); 10 | } 11 | 12 | export default AssignmentRepositoryInterface 13 | -------------------------------------------------------------------------------- /api/src/app/repositories/board-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Board } from '../models/sequelize' 2 | 3 | interface BoardRepositoryInterface { 4 | find(id: string): Promise; 5 | findAll(criteria): Promise>; 6 | findByParent(id: string): Promise>; 7 | create(properties): Promise; 8 | incrementCount(board, amount); 9 | merge(board, body); 10 | delete(id: string); 11 | } 12 | 13 | export default BoardRepositoryInterface 14 | -------------------------------------------------------------------------------- /api/src/app/repositories/comment-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from '../models/sequelize' 2 | 3 | interface CommentRepositoryInterface { 4 | create(owner, board, timestamp, text): Promise; 5 | findAll(criteria): Promise>; 6 | deleteMany(criteria); 7 | } 8 | 9 | export default CommentRepositoryInterface 10 | -------------------------------------------------------------------------------- /api/src/app/repositories/group-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Group } from '../models/sequelize' 2 | 3 | interface GroupRepositoryInterface { 4 | create(title, owner, order, board): Promise; 5 | find(id: string): Promise; 6 | findAll(criteria): Promise>; 7 | delete(id: string); 8 | } 9 | 10 | export default GroupRepositoryInterface 11 | -------------------------------------------------------------------------------- /api/src/app/repositories/member-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Member } from '../models/sequelize' 2 | 3 | interface MemberRepositoryInterface { 4 | create(properties): Promise; 5 | findAll(criteria): Promise>; 6 | } 7 | 8 | export default MemberRepositoryInterface 9 | -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/assignment-repository.ts: -------------------------------------------------------------------------------- 1 | import AssignmentRepositoryInterface from '../assignment-repository-interface' 2 | import Board = require('../../models/mongo/board') 3 | import Assignment = require('../../models/mongo/assignment') 4 | 5 | class AssignmentRepository implements AssignmentRepositoryInterface { 6 | async findAll(criteria): Promise> { 7 | const assignments = await Assignment.find(criteria) 8 | return assignments 9 | } 10 | 11 | async findOne(criteria): Promise { 12 | const assignment = await Assignment.findOne(criteria) 13 | return assignment 14 | } 15 | 16 | async create(assignee, assigner, board): Promise { 17 | const assignment = await Assignment.create({assignee, assigner, board}) 18 | return assignment 19 | } 20 | 21 | async deleteMany(criteria) { 22 | await Assignment.deleteMany(criteria) 23 | } 24 | 25 | async deleteOne(criteria) { 26 | await Assignment.deleteOne(criteria) 27 | } 28 | } 29 | 30 | export default AssignmentRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/board-repository.ts: -------------------------------------------------------------------------------- 1 | import BoardRepositoryInterface from '../board-repository-interface' 2 | import Board = require('../../models/mongo/board') 3 | import mongoose = require('mongoose') 4 | 5 | const ObjectId = mongoose.Types.ObjectId 6 | 7 | class BoardRepository implements BoardRepositoryInterface { 8 | async find(id: string): Promise { 9 | const board = await Board.findById(new ObjectId(id)) 10 | return board 11 | } 12 | 13 | async findAll(criteria): Promise> { 14 | const boards = await Board.find(criteria) 15 | return boards 16 | } 17 | 18 | async findByParent(id: string): Promise> { 19 | const children = await Board.find({parent: id}) 20 | return children 21 | } 22 | 23 | async create(properties): Promise { 24 | const board = await Board.create(properties) 25 | return board 26 | } 27 | 28 | async incrementCount(board, amount) { 29 | await board.update({$inc: {count: amount}}) 30 | board.save() 31 | } 32 | 33 | async merge(board, body) { 34 | const updatedBoard = Object.assign(board, body) 35 | updatedBoard.save() 36 | return updatedBoard 37 | } 38 | 39 | async delete(id: string) { 40 | const deleteResult = await Board.deleteOne({_id: new ObjectId(id)}) 41 | } 42 | } 43 | 44 | export default BoardRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/comment-repository.ts: -------------------------------------------------------------------------------- 1 | import CommentRepositoryInterface from '../comment-repository-interface' 2 | import Comment = require('../../models/mongo/comment') 3 | import Board = require('../../models/mongo/board') 4 | 5 | class CommentRepository implements CommentRepositoryInterface { 6 | async create(owner, board, timestamp, text): Promise { 7 | const comment = await Comment.create({ 8 | text: text, 9 | owner: owner, 10 | board: board, 11 | timestamp: timestamp, 12 | }) 13 | 14 | return comment 15 | } 16 | 17 | async findAll(criteria): Promise> { 18 | const comments = await Comment.find(criteria) 19 | return comments 20 | } 21 | 22 | async deleteMany(criteria) { 23 | await Comment.deleteMany(criteria) 24 | } 25 | } 26 | 27 | export default CommentRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/group-repository.ts: -------------------------------------------------------------------------------- 1 | import GroupRepositoryInterface from '../group-repository-interface' 2 | import Group = require('../../models/mongo/group') 3 | import mongoose = require('mongoose') 4 | 5 | const ObjectId = mongoose.Types.ObjectId 6 | 7 | class GroupRepository implements GroupRepositoryInterface { 8 | async create(title, owner, order, board): Promise { 9 | const group = await Group.create({ 10 | title: title, 11 | owner: owner._id, 12 | order: order, 13 | board: board._id, 14 | }) 15 | 16 | return group 17 | } 18 | 19 | async find(id: string): Promise { 20 | const group = await Group.findById(new ObjectId(id)) 21 | return group 22 | } 23 | 24 | async findAll(criteria): Promise> { 25 | const groups = await Group.find(criteria) 26 | return groups 27 | } 28 | 29 | async delete(id: string) { 30 | await Group.deleteOne({_id: id}) 31 | } 32 | } 33 | 34 | export default GroupRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/project-repository.ts: -------------------------------------------------------------------------------- 1 | import ProjectRepositoryInterface from '../project-repository-interface' 2 | import Project = require('../../models/mongo/project') 3 | import mongoose = require('mongoose') 4 | 5 | const ObjectId = mongoose.Types.ObjectId 6 | 7 | class ProjectRepository implements ProjectRepositoryInterface { 8 | async create(title, space, owner) { 9 | const project = await Project.create({ 10 | title: title, 11 | space: space, 12 | owner: owner, 13 | }) 14 | return project 15 | } 16 | 17 | async find(id: string): Promise { 18 | const project = await Project.findById(new ObjectId(id)) 19 | return project 20 | } 21 | 22 | async findAll(criteria): Promise> { 23 | const projects = await Project.find(criteria) 24 | return projects 25 | } 26 | 27 | async delete(id) { 28 | const deleteResult = await Project.deleteOne({_id: id}) 29 | return deleteResult 30 | } 31 | } 32 | 33 | export default ProjectRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/space-repository.ts: -------------------------------------------------------------------------------- 1 | import SpaceRepositoryInterface from '../space-repository-interface' 2 | import Space = require('../../models/mongo/space') 3 | import mongoose = require('mongoose') 4 | 5 | const ObjectId = mongoose.Types.ObjectId 6 | 7 | class SpaceRepository implements SpaceRepositoryInterface { 8 | async create(title, team, owner) { 9 | const space = await Space.create({title: title, team: team, owner: owner}) 10 | return space 11 | } 12 | 13 | async find(id: string): Promise { 14 | const space = await Space.findById(new ObjectId(id)) 15 | return space 16 | } 17 | 18 | async findOne(criteria): Promise { 19 | const space = await Space.findOne(criteria) 20 | return space 21 | } 22 | 23 | async findAll(criteria): Promise> { 24 | const spaces = await Space.find(criteria) 25 | return spaces 26 | } 27 | 28 | async delete(id: string) { 29 | await Space.deleteOne({_id: id}) 30 | } 31 | } 32 | 33 | export default SpaceRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/team-invite-repository.ts: -------------------------------------------------------------------------------- 1 | import TeamInviteRepositoryInterface from '../team-invite-repository-interface' 2 | import TeamInvite = require('../../models/mongo/team-invite') 3 | 4 | class TeamInviteRepository implements TeamInviteRepositoryInterface { 5 | async create(team, invitee): Promise { 6 | const invite = await TeamInvite.create({ 7 | team: team, 8 | invitee: invitee 9 | }) 10 | return invite 11 | } 12 | 13 | async findAll(criteria): Promise> { 14 | const invites = await TeamInvite.find(criteria) 15 | return invites 16 | } 17 | 18 | async findOne(criteria): Promise { 19 | const invitation = await TeamInvite.findOne(criteria) 20 | return invitation 21 | } 22 | 23 | async deleteOne(criteria) { 24 | await TeamInvite.deleteOne(criteria) 25 | } 26 | } 27 | 28 | export default TeamInviteRepository -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/team-repository.ts: -------------------------------------------------------------------------------- 1 | import TeamRepositoryInterface from '../team-repository-interface' 2 | import Team = require('../../models/mongo/team') 3 | import mongoose = require('mongoose') 4 | 5 | const ObjectId = mongoose.Types.ObjectId 6 | 7 | class TeamRepository implements TeamRepositoryInterface { 8 | async create(members, owner, title): Promise { 9 | if (owner === null && title === null) { 10 | const team = await Team.create({ 11 | members: members 12 | }) 13 | return team 14 | } else { 15 | const team = await Team.create({ 16 | members: members, 17 | owner: owner, 18 | title: title, 19 | }) 20 | return team 21 | } 22 | 23 | } 24 | 25 | async find(id: string): Promise { 26 | const team = await Team.findById(new ObjectId(id)) 27 | return team 28 | } 29 | 30 | async findAll(criteria): Promise> { 31 | const teams = await Team.find(criteria) 32 | return teams 33 | } 34 | 35 | async findOne(criteria): Promise { 36 | const team = await Team.findOne(criteria) 37 | return team 38 | } 39 | 40 | async delete(id: string) { 41 | await Team.deleteOne({_id: id}) 42 | } 43 | 44 | async deleteOne(criteria) { 45 | await Team.deleteOne(criteria) 46 | } 47 | } 48 | 49 | export default TeamRepository 50 | -------------------------------------------------------------------------------- /api/src/app/repositories/mongo/user-repository.ts: -------------------------------------------------------------------------------- 1 | import UserRepositoryInterface from '../user-repository-interface' 2 | import User = require('../../models/mongo/user') 3 | import mongoose = require('mongoose') 4 | 5 | const ObjectId = mongoose.Types.ObjectId 6 | 7 | class UserRepository implements UserRepositoryInterface { 8 | async create(properties): Promise { 9 | const user = await User.create(properties) 10 | return user 11 | } 12 | 13 | async findOne(criteria): Promise { 14 | const user = await User.findOne(criteria) 15 | return user 16 | } 17 | 18 | async findAll(criteria): Promise> { 19 | const users = await User.find(criteria) 20 | return users 21 | } 22 | 23 | async find(id: string): Promise { 24 | const user = await User.findById(new ObjectId(id)) 25 | return user 26 | } 27 | } 28 | 29 | export default UserRepository -------------------------------------------------------------------------------- /api/src/app/repositories/project-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '../models/sequelize' 2 | 3 | interface ProjectRepositoryInterface { 4 | create(title, space, owner); 5 | find(id: string): Promise; 6 | findAll(criteria): Promise>; 7 | delete(id); 8 | } 9 | 10 | export default ProjectRepositoryInterface 11 | -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/assignment-repository.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from 'uuidv4' 2 | import AssignmentRepositoryInterface from '../assignment-repository-interface' 3 | import { Board } from '../../models/sequelize' 4 | import { Assignment } from '../../models/sequelize' 5 | 6 | class AssignmentRepository implements AssignmentRepositoryInterface { 7 | async findAll(criteria): Promise> { 8 | const assignments = await Assignment.findAll({where: criteria}) 9 | return assignments 10 | } 11 | 12 | async findOne(criteria): Promise { 13 | const assignment = await Assignment.findOne({where: criteria}) 14 | return assignment 15 | } 16 | 17 | async create(assignee, assigner, board): Promise { 18 | const _id = uuid() 19 | const assignment = await Assignment.create({_id, assignee, assigner, board}) 20 | return assignment 21 | } 22 | 23 | async deleteMany(criteria) { 24 | await Assignment.destroy({where: criteria}) 25 | } 26 | 27 | async deleteOne(criteria) { 28 | await Assignment.destroy({where: criteria}) 29 | } 30 | } 31 | 32 | export default AssignmentRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/board-repository.ts: -------------------------------------------------------------------------------- 1 | import BoardRepositoryInterface from '../board-repository-interface' 2 | import { Board } from '../../models/sequelize' 3 | 4 | class BoardRepository implements BoardRepositoryInterface { 5 | async find(id: string): Promise { 6 | const board = await Board.findOne({where: {_id: id}}) 7 | return board 8 | } 9 | 10 | async findAll(criteria): Promise> { 11 | const boards = await Board.findAll({where: criteria}) 12 | return boards 13 | } 14 | 15 | async findByParent(id: string): Promise> { 16 | const children = await Board.findAll({where: {parent: id}}) 17 | return children 18 | } 19 | 20 | async create(properties): Promise { 21 | const board = await Board.create(properties) 22 | return board 23 | } 24 | 25 | async incrementCount(board, amount) { 26 | await board.increment('count', {by: amount}) 27 | await board.save() 28 | } 29 | 30 | async merge(board, body) { 31 | const updatedBoard = Object.assign(board, body) 32 | await updatedBoard.save() 33 | return updatedBoard 34 | } 35 | 36 | async delete(id: string) { 37 | const deleteResult = await Board.destroy({where: {_id: id}}) 38 | } 39 | } 40 | 41 | export default BoardRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/comment-repository.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from 'uuidv4' 2 | import CommentRepositoryInterface from '../comment-repository-interface' 3 | import { Comment } from '../../models/sequelize' 4 | import { Board } from '../../models/sequelize' 5 | 6 | class CommentRepository implements CommentRepositoryInterface { 7 | async create(owner, board, timestamp, text): Promise { 8 | const _id = uuid() 9 | const comment = await Comment.create({ 10 | _id: _id, 11 | text: text, 12 | owner: owner, 13 | board: board, 14 | timestamp: timestamp, 15 | }) 16 | 17 | return comment 18 | } 19 | 20 | async findAll(criteria): Promise> { 21 | const comments = await Comment.findAll({where: criteria}) 22 | return comments 23 | } 24 | 25 | async deleteMany(criteria) { 26 | await Comment.destroy({where: criteria}) 27 | } 28 | } 29 | 30 | export default CommentRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/group-repository.ts: -------------------------------------------------------------------------------- 1 | import GroupRepositoryInterface from '../group-repository-interface' 2 | import { Group } from '../../models/sequelize' 3 | import { uuid } from 'uuidv4' 4 | 5 | class GroupRepository implements GroupRepositoryInterface { 6 | async create(title, owner, order, board): Promise { 7 | const group = await Group.create({ 8 | _id: uuid(), 9 | title: title, 10 | owner: owner, 11 | order: order, 12 | board: board, 13 | }) 14 | 15 | return group 16 | } 17 | 18 | async find(id: string): Promise { 19 | const group = await Group.findOne({where: {_id: id}}) 20 | return group 21 | } 22 | 23 | async findAll(criteria): Promise> { 24 | const groups = await Group.findAll({where: criteria}) 25 | return groups 26 | } 27 | 28 | async delete(id: string) { 29 | await Group.destroy({where: {_id: id}}) 30 | } 31 | } 32 | 33 | export default GroupRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/member-repository.ts: -------------------------------------------------------------------------------- 1 | import MemberRepositoryInterface from '../member-repository-interface' 2 | import { Member } from '../../models/sequelize' 3 | 4 | class MemberRepository implements MemberRepositoryInterface { 5 | async create(properties): Promise { 6 | const member = await Member.create(properties) 7 | return member 8 | } 9 | 10 | async findAll(criteria): Promise> { 11 | const members = await Member.findAll({where: criteria}) 12 | return members 13 | } 14 | } 15 | 16 | export default MemberRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/project-repository.ts: -------------------------------------------------------------------------------- 1 | import ProjectRepositoryInterface from '../project-repository-interface' 2 | import { Project }from '../../models/sequelize' 3 | import { uuid } from 'uuidv4' 4 | 5 | class ProjectRepository implements ProjectRepositoryInterface { 6 | async create(title, space, owner) { 7 | const project = await Project.create({ 8 | _id: uuid(), 9 | title: title, 10 | space: space, 11 | owner: owner, 12 | }) 13 | return project 14 | } 15 | 16 | async find(id: string): Promise { 17 | const project = await Project.findOne({where: {_id: id}}) 18 | return project 19 | } 20 | 21 | async findAll(criteria): Promise> { 22 | const projects = await Project.findAll({where: criteria}) 23 | return projects 24 | } 25 | 26 | async delete(id) { 27 | const deleteResult = await Project.destroy({where: {_id: id}}) 28 | return deleteResult 29 | } 30 | } 31 | 32 | export default ProjectRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/space-repository.ts: -------------------------------------------------------------------------------- 1 | import SpaceRepositoryInterface from '../space-repository-interface' 2 | import { Space } from '../../models/sequelize' 3 | import { uuid } from 'uuidv4' 4 | 5 | class SpaceRepository implements SpaceRepositoryInterface { 6 | async create(title, team, owner) { 7 | const space = await Space.create({_id: uuid(), title: title, team: team, owner: owner}) 8 | return space 9 | } 10 | 11 | async find(id: string): Promise { 12 | const space = await Space.findOne({where: {_id: id}}) 13 | return space 14 | } 15 | 16 | async findOne(criteria): Promise { 17 | const space = await Space.findOne({where: criteria}) 18 | return space 19 | } 20 | 21 | async findAll(criteria): Promise> { 22 | const spaces = await Space.findAll({where: criteria}) 23 | return spaces 24 | } 25 | 26 | async delete(id: string) { 27 | await Space.destroy({where: {_id: id}}) 28 | } 29 | } 30 | 31 | export default SpaceRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/team-invite-repository.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from 'uuidv4' 2 | import TeamInviteRepositoryInterface from '../team-invite-repository-interface' 3 | import { TeamInvite } from '../../models/sequelize' 4 | 5 | class TeamInviteRepository implements TeamInviteRepositoryInterface { 6 | async create(team, invitee): Promise { 7 | const invite = await TeamInvite.create({ 8 | _id: uuid(), 9 | team: team, 10 | invitee: invitee 11 | }) 12 | return invite 13 | } 14 | 15 | async findAll(criteria): Promise> { 16 | const invites = await TeamInvite.findAll({where: criteria}) 17 | return invites 18 | } 19 | 20 | async findOne(criteria): Promise { 21 | const invitation = await TeamInvite.findOne({where: criteria}) 22 | return invitation 23 | } 24 | 25 | async deleteOne(criteria) { 26 | await TeamInvite.destroy({where: criteria}) 27 | } 28 | } 29 | 30 | export default TeamInviteRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/team-repository.ts: -------------------------------------------------------------------------------- 1 | import TeamRepositoryInterface from '../team-repository-interface' 2 | import { Team } from '../../models/sequelize' 3 | 4 | class TeamRepository implements TeamRepositoryInterface { 5 | async create(id, members, owner, title): Promise { 6 | if (owner === null && title === null) { 7 | const team = await Team.create({ 8 | _id: id, 9 | members: members 10 | }) 11 | return team 12 | } else { 13 | const team = await Team.create({ 14 | _id: id, 15 | members: members, 16 | owner: owner, 17 | title: title, 18 | }) 19 | return team 20 | } 21 | 22 | } 23 | 24 | async find(id: string): Promise { 25 | const team = await Team.findOne({where: {_id: id}}) 26 | return team 27 | } 28 | 29 | async findOne(criteria): Promise { 30 | const team = await Team.findOne({where: criteria}) 31 | return team 32 | } 33 | 34 | async findAll(criteria): Promise> { 35 | const teams = await Team.findAll({where: criteria}) 36 | return teams 37 | } 38 | 39 | async delete(id: string) { 40 | await Team.destroy({where: {_id: id}}) 41 | } 42 | 43 | async deleteOne(criteria) { 44 | await Team.destroy({where: criteria}) 45 | } 46 | } 47 | 48 | export default TeamRepository -------------------------------------------------------------------------------- /api/src/app/repositories/sequelize/user-repository.ts: -------------------------------------------------------------------------------- 1 | import UserRepositoryInterface from '../user-repository-interface' 2 | import { User } from '../../models/sequelize' 3 | 4 | class UserRepository implements UserRepositoryInterface { 5 | async create(properties): Promise { 6 | const user = await User.create(properties) 7 | return user 8 | } 9 | 10 | async findOne(criteria): Promise { 11 | const user = await User.findOne({where: criteria}) 12 | return user 13 | } 14 | 15 | async findAll(criteria): Promise> { 16 | const users = await User.findAll({where: criteria}) 17 | return users 18 | } 19 | 20 | async find(id: string): Promise { 21 | const user = await User.findOne({where: {_id: id}}) 22 | return user 23 | } 24 | } 25 | 26 | export default UserRepository -------------------------------------------------------------------------------- /api/src/app/repositories/space-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Space } from '../models/sequelize' 2 | 3 | interface SpaceRepositoryInterface { 4 | create(title, team, owner): Promise; 5 | find(id: string): Promise; 6 | findAll(criteria): Promise>; 7 | findOne(criteria): Promise; 8 | delete(id); 9 | } 10 | 11 | export default SpaceRepositoryInterface 12 | -------------------------------------------------------------------------------- /api/src/app/repositories/team-invite-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { TeamInvite } from '../models/sequelize' 2 | 3 | interface TeamInviteRepositoryInterface { 4 | create(team, invitee): Promise; 5 | findAll(criteria): Promise>; 6 | findOne(criteria): Promise; 7 | deleteOne(criteria); 8 | } 9 | 10 | export default TeamInviteRepositoryInterface 11 | -------------------------------------------------------------------------------- /api/src/app/repositories/team-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { Team } from '../models/sequelize' 2 | 3 | interface TeamRepositoryInterface { 4 | create(id, members, owner, title): Promise; 5 | find(id: string): Promise; 6 | findOne(criteria): Promise; 7 | findAll(criteria): Promise>; 8 | delete(id); 9 | deleteOne(criteria); 10 | } 11 | 12 | export default TeamRepositoryInterface 13 | -------------------------------------------------------------------------------- /api/src/app/repositories/user-repository-interface.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../models/sequelize' 2 | 3 | interface UserRepositoryInterface { 4 | create(properties): Promise; 5 | findOne(criteria): Promise; 6 | findAll(criteria): Promise>; 7 | find(id: string): Promise; 8 | } 9 | 10 | export default UserRepositoryInterface 11 | -------------------------------------------------------------------------------- /api/src/app/services/assignment-service.ts: -------------------------------------------------------------------------------- 1 | import UserRepositoryInterface from '../repositories/user-repository-interface' 2 | import AssignmentRepositoryInterface from '../repositories/assignment-repository-interface' 3 | 4 | class AssignmentService { 5 | private userRepository: UserRepositoryInterface 6 | private assignmentRepository: AssignmentRepositoryInterface 7 | 8 | constructor( 9 | userRepository: UserRepositoryInterface, 10 | assignmentRepository: AssignmentRepositoryInterface, 11 | ) { 12 | this.userRepository = userRepository 13 | this.assignmentRepository = assignmentRepository 14 | } 15 | 16 | public async createAssignment(sub, assignee, board) { 17 | try { 18 | const user = await this.userRepository.findOne({sub: sub}) 19 | const assigner = user._id 20 | const currentAssignment = await this.assignmentRepository.findOne({ 21 | board: board, 22 | assignee: assignee, 23 | }) 24 | 25 | if (currentAssignment != null) { 26 | return currentAssignment 27 | } 28 | 29 | const assignment = await this.assignmentRepository.create(assignee, assigner, board) 30 | 31 | return assignment 32 | 33 | } catch(error) { 34 | throw error 35 | } 36 | } 37 | 38 | public async readAssignments(sub) { 39 | try { 40 | const user = await this.userRepository.findOne({sub: sub}) 41 | const assignments = await this.assignmentRepository.findAll({assignee: user._id}) 42 | return assignments 43 | } catch(error) { 44 | throw error 45 | } 46 | } 47 | 48 | public async deleteAssignment(board, assignee) { 49 | try { 50 | 51 | const result = await this.assignmentRepository.deleteOne({ 52 | board: board, 53 | assignee: assignee 54 | }) 55 | 56 | return result 57 | 58 | } catch(error) { 59 | throw error 60 | } 61 | } 62 | } 63 | 64 | export default AssignmentService -------------------------------------------------------------------------------- /api/src/app/services/comment-service.ts: -------------------------------------------------------------------------------- 1 | import UserRepositoryInterface from '../repositories/user-repository-interface' 2 | import CommentRepositoryInterface from '../repositories/comment-repository-interface' 3 | import BoardRepositoryInterface from '../repositories/board-repository-interface' 4 | import ProjectRepositoryInterface from '../repositories/project-repository-interface' 5 | import SpaceRepositoryInterface from '../repositories/space-repository-interface' 6 | import TeamRepositoryInterface from '../repositories/team-repository-interface' 7 | import MemberRepositoryInterface from '../repositories/member-repository-interface' 8 | 9 | class CommentService { 10 | private userRepository: UserRepositoryInterface 11 | private commentRepository: CommentRepositoryInterface 12 | private boardRepository: BoardRepositoryInterface 13 | private projectRepository: ProjectRepositoryInterface 14 | private spaceRepository: SpaceRepositoryInterface 15 | private teamRepository: TeamRepositoryInterface 16 | private memberRepository: MemberRepositoryInterface 17 | 18 | constructor( 19 | userRepository: UserRepositoryInterface, 20 | commentRepository: CommentRepositoryInterface, 21 | boardRepository: BoardRepositoryInterface, 22 | projectRepository: ProjectRepositoryInterface, 23 | spaceRepository: SpaceRepositoryInterface, 24 | teamRepository: TeamRepositoryInterface, 25 | memberRepository: MemberRepositoryInterface, 26 | ) { 27 | this.userRepository = userRepository 28 | this.commentRepository = commentRepository 29 | this.boardRepository = boardRepository 30 | this.projectRepository = projectRepository 31 | this.spaceRepository = spaceRepository 32 | this.teamRepository = teamRepository 33 | this.memberRepository = memberRepository 34 | } 35 | 36 | public async createComment(text, boardId, sub) { 37 | try { 38 | const timestamp = new Date().getTime() 39 | 40 | const user = await this.userRepository.findOne({sub: sub}) 41 | 42 | const comment = await this.commentRepository.create(user._id, boardId, timestamp ,text) 43 | 44 | const board = await this.boardRepository.find(boardId) 45 | if (board.comments === false || board.comments === undefined) { 46 | board.comments = true 47 | board.save() 48 | } 49 | 50 | return comment 51 | } catch(error) { 52 | throw error 53 | } 54 | } 55 | 56 | public async readComments(sub, boardId) { 57 | try { 58 | const user = await this.userRepository.findOne({sub: sub}) 59 | const board = await this.boardRepository.find(boardId) 60 | const project = await this.projectRepository.find(board.project) 61 | const space = await this.spaceRepository.find(project.space) 62 | // const team = await this.teamRepository.find(space.team) 63 | const members = (await this.memberRepository.findAll({team: space.team})) 64 | .map(member => member.user) 65 | 66 | if (members.includes(user._id)) { 67 | const comments = await this.commentRepository.findAll({board: boardId}) 68 | return comments.sort((a, b) => +(parseInt(a.timestamp) < parseInt(b.timestamp))) 69 | } 70 | 71 | return null 72 | } catch(error) { 73 | throw error 74 | } 75 | } 76 | } 77 | 78 | export default CommentService -------------------------------------------------------------------------------- /api/src/app/services/group-service.ts: -------------------------------------------------------------------------------- 1 | import BoardService from '../services/board-service' 2 | import BoardRepositoryInterface from '../repositories/board-repository-interface' 3 | import UserRepositoryInterface from '../repositories/user-repository-interface' 4 | import GroupRepositoryInterface from '../repositories/group-repository-interface' 5 | import AssignmentRepositoryInterface from '../repositories/assignment-repository-interface' 6 | import CommentRepositoryInterface from '../repositories/comment-repository-interface' 7 | 8 | class GroupService { 9 | private boardRepository: BoardRepositoryInterface 10 | private userRepository: UserRepositoryInterface 11 | private groupRepository: GroupRepositoryInterface 12 | private assignmentRepository: AssignmentRepositoryInterface 13 | private commentRepository: CommentRepositoryInterface 14 | private boardService: BoardService 15 | 16 | constructor( 17 | boardRepository: BoardRepositoryInterface, 18 | userRepository: UserRepositoryInterface, 19 | groupRepository: GroupRepositoryInterface, 20 | assignmentRepository: AssignmentRepositoryInterface, 21 | commentRepository: CommentRepositoryInterface, 22 | boardService: BoardService, 23 | ) { 24 | this.boardRepository = boardRepository 25 | this.userRepository = userRepository 26 | this.groupRepository = groupRepository 27 | this.assignmentRepository = assignmentRepository 28 | this.commentRepository = commentRepository 29 | this.boardService = boardService 30 | } 31 | 32 | public async createGroup(sub, boardId) { 33 | try { 34 | const ownerObject = await this.userRepository.findOne({sub: sub}) 35 | const owner = ownerObject._id 36 | const board = await this.boardRepository.find(boardId) 37 | const currentGroups = await this.groupRepository.findAll({board: boardId}) 38 | const order = Math.max(...[-1, ...currentGroups.map(group => group.order)]) + 1 39 | const group = await this.groupRepository.create('', owner, order, boardId) 40 | return group 41 | 42 | } catch(error) { 43 | throw error 44 | } 45 | } 46 | 47 | public async readGroups(boardId) { 48 | try { 49 | const groups = await this.groupRepository.findAll({board: boardId}) 50 | return groups 51 | } catch(error) { 52 | throw error 53 | } 54 | } 55 | 56 | public async updateGroup(groupId, body) { 57 | try { 58 | const group = await this.groupRepository.find(groupId) 59 | const updatedGroup = Object.assign(group, body) 60 | updatedGroup.save() 61 | return updatedGroup 62 | 63 | } catch(error) { 64 | throw error 65 | } 66 | 67 | } 68 | 69 | public async deleteGroup(sub, id) { 70 | try { 71 | 72 | const user = await this.userRepository.findOne({sub: sub}) 73 | const group = await this.groupRepository.find(id) 74 | 75 | if (group.owner == user._id) { 76 | const deleteResult = await this.groupRepository.delete(id) 77 | const boards = await this.boardRepository.findAll({group: id}) 78 | if (boards.length > 0) { 79 | await this.boardService.recursiveDelete(boards.map(board => board._id)) 80 | } 81 | return true 82 | } else { 83 | return false 84 | } 85 | 86 | } catch(error) { 87 | throw error 88 | } 89 | } 90 | } 91 | 92 | export default GroupService -------------------------------------------------------------------------------- /api/src/app/services/project-service.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from 'uuidv4' 2 | import BoardService from '../services/board-service' 3 | import BoardRepositoryInterface from '../repositories/board-repository-interface' 4 | import UserRepositoryInterface from '../repositories/user-repository-interface' 5 | import GroupRepositoryInterface from '../repositories/group-repository-interface' 6 | import AssignmentRepositoryInterface from '../repositories/assignment-repository-interface' 7 | import CommentRepositoryInterface from '../repositories/comment-repository-interface' 8 | import ProjectRepositoryInterface from '../repositories/project-repository-interface' 9 | import SpaceRepositoryInterface from '../repositories/space-repository-interface' 10 | import MemberRepositoryInterface from '../repositories/member-repository-interface' 11 | 12 | class ProjectService { 13 | private boardRepository: BoardRepositoryInterface 14 | private userRepository: UserRepositoryInterface 15 | private groupRepository: GroupRepositoryInterface 16 | private assignmentRepository: AssignmentRepositoryInterface 17 | private commentRepository: CommentRepositoryInterface 18 | private projectRepository: ProjectRepositoryInterface 19 | private spaceRepository: SpaceRepositoryInterface 20 | private memberRepository: MemberRepositoryInterface 21 | private boardService: BoardService 22 | 23 | constructor( 24 | boardRepository: BoardRepositoryInterface, 25 | userRepository: UserRepositoryInterface, 26 | groupRepository: GroupRepositoryInterface, 27 | assignmentRepository: AssignmentRepositoryInterface, 28 | commentRepository: CommentRepositoryInterface, 29 | projectRepository: ProjectRepositoryInterface, 30 | spaceRepository: SpaceRepositoryInterface, 31 | memberRepository: MemberRepositoryInterface, 32 | boardService: BoardService 33 | ) { 34 | this.boardRepository = boardRepository 35 | this.userRepository = userRepository 36 | this.groupRepository = groupRepository 37 | this.assignmentRepository = assignmentRepository 38 | this.commentRepository = commentRepository 39 | this.projectRepository = projectRepository 40 | this.spaceRepository = spaceRepository 41 | this.memberRepository = memberRepository 42 | this.boardService = boardService 43 | } 44 | 45 | public async createProject(sub, title, space) { 46 | try { 47 | const owner = await this.userRepository.findOne({sub: sub}) 48 | const project = await this.projectRepository.create(title, space, owner._id) 49 | const projectRoot = await this.boardRepository.create({ 50 | _id: uuid(), 51 | title: title, 52 | description: "", 53 | owner: owner._id, 54 | order: 0, 55 | project: project._id, 56 | parent: null, 57 | group: null, 58 | count: 0, 59 | comments: false, 60 | }) 61 | 62 | const groupBacklog = await this.groupRepository.create("Backlog", owner._id, 0, projectRoot._id) 63 | const groupTodo = await this.groupRepository.create("To-do", owner._id, 1, projectRoot._id) 64 | const groupInProgress = await this.groupRepository.create("In progress", owner._id, 2, projectRoot._id) 65 | const groupReview = await this.groupRepository.create("Review", owner._id, 3, projectRoot._id) 66 | const groupDone = await this.groupRepository.create("Done", owner._id, 4, projectRoot._id) 67 | 68 | return project 69 | 70 | } catch(error) { 71 | throw error 72 | } 73 | } 74 | 75 | public async readProjects(sub) { 76 | try { 77 | 78 | const owner = await this.userRepository.findOne({sub: sub}) 79 | 80 | const teams = (await this.memberRepository.findAll({user: owner._id})) 81 | .map(member => member.team) 82 | 83 | const projects = [] 84 | 85 | for (const team of teams) { 86 | const space = await this.spaceRepository.findOne({team: team}) 87 | const projectsResult = await this.projectRepository.findAll({space: space._id}) 88 | projects.push(...projectsResult) 89 | } 90 | 91 | return projects 92 | 93 | } catch(error) { 94 | throw error 95 | } 96 | } 97 | 98 | public async deleteProject(sub, id) { 99 | try { 100 | 101 | const owner = await this.userRepository.findOne({sub: sub}) 102 | const project = await this.projectRepository.find(id) 103 | if (owner._id == project.owner) { 104 | const boards = await this.boardRepository.findAll({project: id}) 105 | await this.boardService.recursiveDelete(boards.map(board => board._id)) 106 | const deleteResult = await this.projectRepository.delete(id) 107 | return true 108 | } else { 109 | return false 110 | } 111 | 112 | } catch(error) { 113 | throw error 114 | } 115 | } 116 | } 117 | 118 | export default ProjectService -------------------------------------------------------------------------------- /api/src/app/services/space-service.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from 'uuidv4' 2 | import BoardService from '../services/board-service' 3 | import BoardRepositoryInterface from '../repositories/board-repository-interface' 4 | import UserRepositoryInterface from '../repositories/user-repository-interface' 5 | import GroupRepositoryInterface from '../repositories/group-repository-interface' 6 | import AssignmentRepositoryInterface from '../repositories/assignment-repository-interface' 7 | import CommentRepositoryInterface from '../repositories/comment-repository-interface' 8 | import TeamRepositoryInterface from '../repositories/team-repository-interface' 9 | import SpaceRepositoryInterface from '../repositories/space-repository-interface' 10 | import ProjectRepositoryInterface from '../repositories/project-repository-interface' 11 | import MemberRepositoryInterface from '../repositories/member-repository-interface' 12 | 13 | class SpaceService { 14 | private boardService: BoardService 15 | private boardRepository: BoardRepositoryInterface 16 | private userRepository: UserRepositoryInterface 17 | private groupRepository: GroupRepositoryInterface 18 | private assignmentRepository: AssignmentRepositoryInterface 19 | private commentRepository: CommentRepositoryInterface 20 | private teamRepository: TeamRepositoryInterface 21 | private spaceRepository: SpaceRepositoryInterface 22 | private projectRepository: ProjectRepositoryInterface 23 | private memberRepository: MemberRepositoryInterface 24 | 25 | constructor( 26 | boardRepository: BoardRepositoryInterface, 27 | userRepository: UserRepositoryInterface, 28 | groupRepository: GroupRepositoryInterface, 29 | assignmentRepository: AssignmentRepositoryInterface, 30 | commentRepository: CommentRepositoryInterface, 31 | teamRepository: TeamRepositoryInterface, 32 | spaceRepository: SpaceRepositoryInterface, 33 | projectRepository: ProjectRepositoryInterface, 34 | memberRepository: MemberRepositoryInterface, 35 | boardService: BoardService 36 | ) { 37 | this.boardRepository = boardRepository 38 | this.userRepository = userRepository 39 | this.groupRepository = groupRepository 40 | this.assignmentRepository = assignmentRepository 41 | this.commentRepository = commentRepository 42 | this.teamRepository = teamRepository 43 | this.spaceRepository = spaceRepository 44 | this.projectRepository = projectRepository 45 | this.memberRepository = memberRepository 46 | this.boardService = boardService 47 | } 48 | 49 | public async createSpace(sub, title) { 50 | try { 51 | const owner = await this.userRepository.findOne({sub: sub}) 52 | const team = await this.teamRepository.create(uuid(), [owner._id], null, null) 53 | const space = await this.spaceRepository.create(title, team._id, owner._id) 54 | await this.memberRepository.create({_id: uuid(), team: team._id, user: owner._id}) 55 | 56 | return space 57 | 58 | } catch(error) { 59 | throw error 60 | } 61 | } 62 | 63 | public async readSpaces(sub) { 64 | try { 65 | 66 | const owner = await this.userRepository.findOne({sub: sub}) 67 | const teams = (await this.memberRepository.findAll({user: owner._id})) 68 | .map(member => member.team) 69 | 70 | console.log({teams}) 71 | 72 | const spaces = [] 73 | 74 | for (const team of teams) { 75 | const space = await this.spaceRepository.findOne({team: team}) 76 | spaces.push(space) 77 | } 78 | 79 | return spaces 80 | 81 | } catch(error) { 82 | throw error 83 | } 84 | } 85 | 86 | public async deleteSpace(sub, id) { 87 | try { 88 | 89 | const user = await this.userRepository.findOne({sub: sub}) 90 | const space = await this.spaceRepository.find(id) 91 | const team = await this.teamRepository.find(space.team) 92 | 93 | if (user._id != space.owner) { 94 | 95 | return false 96 | } 97 | 98 | const projects = await this.projectRepository.findAll({space: id}) 99 | 100 | for (const project of projects) { 101 | 102 | const boards = await this.boardRepository.findAll({project: project._id}) 103 | 104 | await this.boardService.recursiveDelete(boards.map(board => board._id)) 105 | 106 | const result = await this.projectRepository.delete(project._id) 107 | } 108 | 109 | for (const member of team.members) { 110 | const teamMember = await this.userRepository.find(member) 111 | teamMember.spaces = teamMember.spaces.filter(s => s !== id) 112 | teamMember.save() 113 | } 114 | 115 | const result = await this.spaceRepository.delete(id) 116 | const result2 = await this.teamRepository.delete(space.team) 117 | 118 | return true 119 | } catch(error) { 120 | throw error 121 | } 122 | } 123 | } 124 | 125 | export default SpaceService -------------------------------------------------------------------------------- /api/src/app/services/team-service.ts: -------------------------------------------------------------------------------- 1 | import { uuid }from 'uuidv4' 2 | import BoardRepositoryInterface from '../repositories/board-repository-interface' 3 | import UserRepositoryInterface from '../repositories/user-repository-interface' 4 | import GroupRepositoryInterface from '../repositories/group-repository-interface' 5 | import SpaceRepositoryInterface from '../repositories/space-repository-interface' 6 | import TeamRepositoryInterface from '../repositories/team-repository-interface' 7 | import MemberRepositoryInterface from '../repositories/member-repository-interface' 8 | 9 | class TeamService { 10 | private boardRepository: BoardRepositoryInterface 11 | private userRepository: UserRepositoryInterface 12 | private groupRepository: GroupRepositoryInterface 13 | private spaceRepository: SpaceRepositoryInterface 14 | private teamRepository: TeamRepositoryInterface 15 | private memberRepository: MemberRepositoryInterface 16 | 17 | constructor( 18 | boardRepository: BoardRepositoryInterface, 19 | userRepository: UserRepositoryInterface, 20 | groupRepository: GroupRepositoryInterface, 21 | spaceRepository: SpaceRepositoryInterface, 22 | teamRepository: TeamRepositoryInterface, 23 | memberRepository: MemberRepositoryInterface 24 | ) { 25 | this.boardRepository = boardRepository 26 | this.userRepository = userRepository 27 | this.groupRepository = groupRepository 28 | this.spaceRepository = spaceRepository 29 | this.teamRepository = teamRepository 30 | this.memberRepository = memberRepository 31 | } 32 | 33 | public async createTeam(sub, title) { 34 | try { 35 | const owner = await this.userRepository.findOne({sub: sub}) 36 | if (owner === null) { 37 | throw new Error("User is null") 38 | } 39 | 40 | const team = await this.teamRepository.create(uuid(), [owner._id], owner._id, title) 41 | await this.memberRepository.create({_id: uuid(), team: team._id, user: owner._id}) 42 | // owner.spaces.push(team._id) 43 | // owner.save() 44 | 45 | const groupBacklog = await this.groupRepository.create("Backlog", team._id, 0, undefined) 46 | const groupTodo = await this.groupRepository.create("To-do", team._id, 1, undefined) 47 | const groupInProgress = await this.groupRepository.create("In progress", team._id, 2, undefined) 48 | const groupReview = await this.groupRepository.create("Review", team._id, 3, undefined) 49 | const groupDone = await this.groupRepository.create("Done", team._id, 4, undefined) 50 | 51 | const groups = [groupBacklog, groupTodo, groupInProgress, groupReview, groupDone] 52 | 53 | const teamRoot = await this.boardRepository.create({ 54 | _id: uuid(), 55 | title: title, 56 | owner: team._id, 57 | groups: groups.map(group => group._id), 58 | order: 0, 59 | parent: null, 60 | team: team._id, 61 | group: null, 62 | isTeamRoot: true, 63 | isUserRoot: false, 64 | count: 0, 65 | comments: false, 66 | }) 67 | 68 | return team 69 | 70 | } catch(error) { 71 | throw error 72 | } 73 | } 74 | 75 | public async readTeamRootsChildren(team) { 76 | try { 77 | 78 | const root = await this.boardRepository.findAll({team: team, group: null}) 79 | 80 | if (root.length === 0) { 81 | return null 82 | } 83 | 84 | return root 85 | 86 | } catch(error) { 87 | throw error 88 | } 89 | } 90 | 91 | public async readTeam(sub, teamId) { 92 | try { 93 | const owner = await this.userRepository.findOne({sub: sub}) 94 | if (owner === null) { 95 | return null 96 | } 97 | 98 | const space = await this.spaceRepository.findOne({team: teamId}) 99 | const members = await this.memberRepository.findAll({user: owner._id}) 100 | const teams = members.map(member => member.team) 101 | console.log({teams}) 102 | if (teams.includes(teamId) === false) { 103 | return null 104 | } 105 | 106 | const team = await this.teamRepository.find(teamId) 107 | 108 | if (team === null) { 109 | return null 110 | } 111 | 112 | return team 113 | 114 | } catch (error) { 115 | throw error 116 | } 117 | } 118 | } 119 | 120 | export default TeamService -------------------------------------------------------------------------------- /api/src/app/services/user-service.ts: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | import { uuid } from 'uuidv4' 3 | import UserRepositoryInterface from '../repositories/user-repository-interface' 4 | import SpaceRepositoryInterface from '../repositories/space-repository-interface' 5 | import TeamRepositoryInterface from '../repositories/team-repository-interface' 6 | import MemberRepositoryInterface from '../repositories/member-repository-interface' 7 | 8 | class UserService { 9 | private userRepository: UserRepositoryInterface 10 | private spaceRepository: SpaceRepositoryInterface 11 | private teamRepository: TeamRepositoryInterface 12 | private memberRepository: MemberRepositoryInterface 13 | 14 | constructor( 15 | userRepository: UserRepositoryInterface, 16 | spaceRepository: SpaceRepositoryInterface, 17 | teamRepository: TeamRepositoryInterface, 18 | memberRepository: MemberRepositoryInterface 19 | ) { 20 | this.userRepository = userRepository 21 | this.spaceRepository = spaceRepository 22 | this.teamRepository = teamRepository 23 | this.memberRepository = memberRepository 24 | } 25 | 26 | public async createUser(sub, token) { 27 | const user = await this.userRepository.findAll({sub: sub}) 28 | 29 | const userEmail = await fetch('https://kanception.auth0.com/userinfo', { 30 | headers: { 31 | Authorization: token, 32 | } 33 | }) 34 | const email = await userEmail.json() 35 | 36 | const userFromEmail = await this.userRepository.findAll({email: email.email}) 37 | 38 | if (user.length === 0 && userFromEmail.length === 0) { 39 | 40 | const user = await this.userRepository.create({_id: uuid(), sub: sub, spaces: [], active: true}) 41 | 42 | return user 43 | 44 | } else if (user.length === 0 && userFromEmail.length > 0) { 45 | userFromEmail[0].sub = sub 46 | userFromEmail[0].active = true 47 | userFromEmail[0].save() 48 | 49 | return userFromEmail[0] 50 | 51 | } else { 52 | return user[0] 53 | } 54 | 55 | } 56 | 57 | public async readUser(sub) { 58 | try { 59 | console.log("++++++++++++++++++++++++++++ REPOS ********************") 60 | console.log("++++++++++++++++++++++++++++ REPOS ********************") 61 | console.log("++++++++++++++++++++++++++++ REPOS ********************") 62 | console.log("++++++++++++++++++++++++++++ REPOS ********************") 63 | console.log("++++++++++++++++++++++++++++ REPOS ********************") 64 | console.log(this.userRepository) 65 | console.log(this.memberRepository) 66 | const user = await this.userRepository.findOne({sub: sub}) 67 | const members = await this.memberRepository.findAll({user: user._id}) 68 | const teams = members.map(member => member.team) 69 | return teams 70 | } catch (error) { 71 | throw error 72 | } 73 | } 74 | 75 | public async readProfiles(sub, team) { 76 | try { 77 | 78 | const user = await this.userRepository.findAll({sub: sub}) 79 | if (user.length === 0) { 80 | // res.sendStatus(503) 81 | return null 82 | } 83 | 84 | console.log({team}) 85 | const space = await this.spaceRepository.findOne({_id: team}) 86 | const members = (await this.memberRepository.findAll({team: space.team})) 87 | .map(member => member.user) 88 | 89 | console.log({members}) 90 | console.log(user[0]._id) 91 | 92 | if (members.includes(user[0]._id) === false) { 93 | // res.sendStatus(501) 94 | console.log('members does not include') 95 | return null 96 | } 97 | 98 | const profiles = [] 99 | 100 | for (const member of members) { 101 | const userObject = await this.userRepository.find(member) 102 | 103 | if (userObject === null || userObject === undefined) { 104 | // res.sendStatus(500) 105 | return null 106 | } 107 | 108 | profiles.push({ 109 | _id: userObject._id, 110 | email: userObject.email, 111 | firstName: userObject.firstName, 112 | lastName: userObject.lastName, 113 | }) 114 | } 115 | 116 | return profiles 117 | // res.send(JSON.stringify(profiles)) 118 | 119 | } catch(error) { 120 | throw error 121 | } 122 | } 123 | 124 | public async updateName(sub, first, last, token) { 125 | try { 126 | 127 | const users = await this.userRepository.findAll({sub: sub}) 128 | const user = users[0] 129 | 130 | const userEmail = await fetch('https://kanception.auth0.com/userinfo', { 131 | headers: { 132 | Authorization: token, 133 | } 134 | }) 135 | 136 | const email = await userEmail.json() 137 | 138 | user.firstName = first 139 | user.lastName = last 140 | user.email = email.email 141 | 142 | user.save() 143 | 144 | return user 145 | 146 | } catch(error) { 147 | throw error 148 | } 149 | } 150 | } 151 | 152 | export default UserService -------------------------------------------------------------------------------- /api/src/app/util/create-auth0-user.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import { uuid } from 'uuidv4' 3 | 4 | const createAuth0User = async (email, first, last) => { 5 | const tokenResult = await fetch( 6 | 'https://kanception.auth0.com/oauth/token', { 7 | method: 'POST', 8 | headers: { 'content-type': 'application/json' }, 9 | body: JSON.stringify({ 10 | "client_id": process.env.AUTH0_CLIENT_ID, 11 | "client_secret": process.env.AUTH0_CLIENT_SECRET, 12 | "audience": "https://kanception.auth0.com/api/v2/", 13 | "grant_type": "client_credentials" 14 | }) 15 | }) 16 | 17 | const accessToken = await tokenResult.json() 18 | const token = accessToken.access_token 19 | 20 | const auth0UserResult = await fetch('https://kanception.auth0.com/api/v2/users', { 21 | method: 'POST', 22 | headers: { 23 | Authorization: `Bearer ${token}`, 24 | "Content-Type": "application/json" 25 | }, 26 | body: JSON.stringify({ 27 | connection: 'Username-Password-Authentication', 28 | email: email, 29 | email_verified: true, 30 | given_name: first, 31 | family_name: last, 32 | password: uuid() 33 | }), 34 | }) 35 | console.log(auth0UserResult) 36 | const auth0User = await auth0UserResult.json() 37 | console.log(auth0User) 38 | 39 | return auth0User 40 | } 41 | 42 | export { createAuth0User } -------------------------------------------------------------------------------- /api/src/app/util/reset-password.ts: -------------------------------------------------------------------------------- 1 | import fetch = require('node-fetch') 2 | import aws = require('aws-sdk') 3 | 4 | const sendPasswordResetEmail = (recipient, url) => { 5 | // Replace sender@example.com with your "From" address. 6 | // This address must be verified with Amazon SES. 7 | const sender = "Kanception.io " 8 | 9 | // The subject line for the email. 10 | const subject = "Kanception.io: Verify account and set password" 11 | 12 | // The email body for recipients with non-HTML email clients. 13 | const body_text = url 14 | 15 | // The HTML body of the email. 16 | const body_html = ` 17 | 18 | 19 | Verify account 20 | 21 | `; 22 | 23 | // The character encoding for the email. 24 | const charset = "UTF-8"; 25 | 26 | // Create a new SES object. 27 | var ses = new aws.SES(); 28 | 29 | // Specify the parameters to pass to the API. 30 | var params = { 31 | Source: sender, 32 | Destination: { 33 | ToAddresses: [ 34 | recipient 35 | ], 36 | }, 37 | Message: { 38 | Subject: { 39 | Data: subject, 40 | Charset: charset 41 | }, 42 | Body: { 43 | Text: { 44 | Data: body_text, 45 | Charset: charset 46 | }, 47 | Html: { 48 | Data: body_html, 49 | Charset: charset 50 | } 51 | } 52 | }, 53 | } 54 | 55 | //Try to send the email. 56 | ses.sendEmail(params, function(err, data) { 57 | // If something goes wrong, print an error message. 58 | if(err) { 59 | console.log(err.message); 60 | } else { 61 | console.log("Email sent! Message ID: ", data.MessageId); 62 | } 63 | }); 64 | } 65 | 66 | const resetPassword = async (user_id, email) => { 67 | console.log({email}) 68 | console.log({user_id}) 69 | const tokenResult = await fetch( 70 | 'https://kanception.auth0.com/oauth/token', { 71 | method: 'POST', 72 | headers: { 'content-type': 'application/json' }, 73 | body: JSON.stringify({ 74 | "client_id": process.env.AUTH0_CLIENT_ID, 75 | "client_secret": process.env.AUTH0_CLIENT_SECRET, 76 | "audience": "https://kanception.auth0.com/api/v2/", 77 | "grant_type": "client_credentials" 78 | }) 79 | }) 80 | 81 | const accessToken = await tokenResult.json() 82 | const token = accessToken.access_token 83 | 84 | const resetPasswordResult = 85 | await fetch('https://kanception.auth0.com/api/v2/tickets/password-change?email=' + email, { 86 | method: 'POST', 87 | headers: { 88 | Authorization: `Bearer ${token}`, 89 | "Content-Type": "application/json" 90 | }, 91 | body: JSON.stringify({ 92 | user_id: user_id, 93 | }), 94 | }) 95 | 96 | console.log('sent reset password email') 97 | const resetPassword = await resetPasswordResult.json() 98 | console.log({resetPassword}) 99 | 100 | sendPasswordResetEmail(email, resetPassword.ticket) 101 | } 102 | 103 | export { sendPasswordResetEmail, resetPassword } -------------------------------------------------------------------------------- /api/tests/board-service.test.ts: -------------------------------------------------------------------------------- 1 | import BoardService from '../src/app/services/board-service' 2 | import BoardRepository from '../src/app/repositories/mock/board-repository' 3 | import UserRepository from '../src/app/repositories/mock/user-repository' 4 | import GroupRepository from '../src/app/repositories/mock/group-repository' 5 | import AssignmentRepository from '../src/app/repositories/mock/assignment-repository' 6 | import CommentRepository from '../src/app/repositories/mock/comment-repository' 7 | 8 | // const project = '0f0d514cf6a4dbf1f5d74b7152f440d1' 9 | // const group = '0f0d514cf6a4dbf1f5d74b7152f440d2' 10 | // const parent = '0f0d514cf6a4dbf1f5d74b7152f440d3' 11 | // const sub = '0f0d514cf6a4dbf1f5d74b7152f440d4' 12 | 13 | // const boardRepository = new BoardRepository() 14 | // const userRepository = new UserRepository() 15 | // const groupRepository = new GroupRepository() 16 | // const assignmentRepository = new AssignmentRepository() 17 | // const commentRepository = new CommentRepository() 18 | // const boardService = new BoardService(boardRepository, userRepository, groupRepository, assignmentRepository, commentRepository) 19 | 20 | // test('createBoard', async () => { 21 | // const board = await boardService.createBoard({project, group, parent, sub}) 22 | 23 | // expect(board.project).toBe(project) 24 | // expect(board.group).toBe(group) 25 | // expect(board.parent).toBe(parent) 26 | // }) 27 | 28 | // test('readTree', async () => { 29 | // const tree = await boardService.readTree(sub, project) 30 | // expect(tree[0]._id).toBe('0f0d514cf6a4dbf1f5d74b7152f440d0') 31 | // }) 32 | 33 | // test('updateBoard', async () => { 34 | // const board = await boardService.createBoard({project, group, parent, sub}) 35 | 36 | // expect(board.project).toBe(project) 37 | // expect(board.group).toBe(group) 38 | // expect(board.parent).toBe(parent) 39 | 40 | // const newBoard = await boardService.updateBoard(board._id, {group: 'OTHER'}) 41 | // expect(newBoard.group).toBe('OTHER') 42 | // }) 43 | -------------------------------------------------------------------------------- /api/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | target: 'node', 6 | entry: './src/index.ts', 7 | devtool: 'source-map', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: 'ts-loader', 13 | exclude: /node_modules/, 14 | }, 15 | ], 16 | }, 17 | optimization: { 18 | minimize: false 19 | }, 20 | resolve: { 21 | extensions: ['.tsx', '.ts', '.js'], 22 | }, 23 | output: { 24 | filename: 'bundle.js', 25 | path: path.resolve(__dirname, 'dist'), 26 | }, 27 | externals: [ 28 | nodeExternals(), 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/data/.gitkeep -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | postgres: 4 | container_name: 'postgres' 5 | image: postgres 6 | volumes: 7 | - ./api/database/create_database.sql:/docker-entrypoint-initdb.d/init.sql 8 | environment: 9 | POSTGRES_PASSWORD: password 10 | POSTGRES_USER: postgres 11 | kanception-api: 12 | container_name: 'kanception-api' 13 | build: ./api 14 | ports: 15 | - "4000:4000" 16 | volumes: 17 | - type: "bind" 18 | source: ./api 19 | target: /home/ubuntu/api 20 | - /home/ubuntu/api/node_modules/ 21 | depends_on: 22 | - postgres 23 | kanception: 24 | container_name: 'kanception' 25 | build: ./kanception 26 | stdin_open: true 27 | ports: 28 | - "3000:3000" 29 | depends_on: 30 | - kanception-api 31 | volumes: 32 | - type: "bind" 33 | source: ./kanception 34 | target: /home/ubuntu/kanception 35 | - /home/ubuntu/kanception/node_modules/ 36 | -------------------------------------------------------------------------------- /kanception/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-optional-chaining"] 3 | } 4 | -------------------------------------------------------------------------------- /kanception/.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 | -------------------------------------------------------------------------------- /kanception/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update && apt-get install -y npm nodejs 5 | 6 | RUN groupadd -r ubuntu && useradd -r -g ubuntu ubuntu && \ 7 | mkdir /home/ubuntu && chown ubuntu:ubuntu /home/ubuntu 8 | 9 | USER ubuntu 10 | RUN mkdir -p /home/ubuntu/kanception 11 | WORKDIR /home/ubuntu/kanception 12 | COPY package*.json ./ 13 | RUN npm install 14 | 15 | COPY --chown=ubuntu:ubuntu . /home/ubuntu/kanception 16 | 17 | CMD ["npm", "run", "start"] 18 | 19 | -------------------------------------------------------------------------------- /kanception/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 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 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /kanception/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kanception", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@auth0/auth0-spa-js": "^1.8.2", 7 | "@material-ui/core": "^4.11.0", 8 | "@material-ui/lab": "^4.0.0-alpha.56", 9 | "@reduxjs/toolkit": "^1.1.0", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.3.2", 12 | "@testing-library/user-event": "^7.1.2", 13 | "bootstrap": "^4.5.0", 14 | "gatsby-plugin-canonical-urls": "^2.3.10", 15 | "react": "^16.13.1", 16 | "react-beautiful-dnd": "^13.0.0", 17 | "react-bootstrap": "^1.0.1", 18 | "react-dom": "^16.13.1", 19 | "react-hamburger-menu": "^1.2.1", 20 | "react-redux": "^7.1.3", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "^4.0.3", 23 | "react-spring": "^8.0.27", 24 | "react-textarea-autosize": "^7.1.2", 25 | "socket.io-client": "^4.0.1" 26 | }, 27 | "scripts": { 28 | "electron": "electron .", 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "main": "src/electron.js", 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/plugin-proposal-optional-chaining": "^7.10.4", 52 | "electron": "^12.0.2", 53 | "electron-packager": "^15.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /kanception/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /kanception/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/kanception/public/favicon.png -------------------------------------------------------------------------------- /kanception/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 29 | Kanban | Project Management Tool 30 | 31 | 32 | 39 | 40 | 50 | 51 | 52 | 53 |
54 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /kanception/public/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/kanception/public/logo.icns -------------------------------------------------------------------------------- /kanception/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 | -------------------------------------------------------------------------------- /kanception/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /kanception/sample.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API=http://localhost:4000 2 | -------------------------------------------------------------------------------- /kanception/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | height: 100%; 4 | width: fit-content; 5 | overflow-y: hidden; 6 | } 7 | 8 | .App .coming-soon { 9 | color: var(--text-color); 10 | } 11 | 12 | body { 13 | scrollbar-width: none; /* Firefox */ 14 | } 15 | 16 | body::-webkit-scrollbar { 17 | display: none; /* Safari and Chrome */ 18 | } 19 | 20 | .page-link { 21 | color: black!important; 22 | } -------------------------------------------------------------------------------- /kanception/src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import kanbanReducer from '../features/kanban/kanbanSlice' 3 | import teamsReducer from '../features/teams/teamsSlice' 4 | import spacesReducer from '../features/spaces/spacesSlice' 5 | import projectsReducer from '../features/projects/projectsSlice' 6 | import assignmentsReducer from '../features/assignments/assignmentsSlice' 7 | import commentsReducer from '../features/comments/commentsSlice' 8 | 9 | export default configureStore({ 10 | reducer: { 11 | assignments: assignmentsReducer, 12 | kanban: kanbanReducer, 13 | teams: teamsReducer, 14 | spaces: spacesReducer, 15 | projects: projectsReducer, 16 | comments: commentsReducer, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /kanception/src/assignment-cell.css: -------------------------------------------------------------------------------- 1 | .assignment-cell { 2 | background: var(--background-color); 3 | margin: 10px; 4 | border-radius: 5px; 5 | padding: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /kanception/src/assignment-cell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './assignment-cell.css' 3 | 4 | const AssignmentCell = props => ( 5 |
6 |

{props.title}

7 |

Assigned by {props.assigner.firstName + ' ' + props.assigner.lastName}

8 |
9 | ) 10 | 11 | export default AssignmentCell 12 | -------------------------------------------------------------------------------- /kanception/src/assignment-list.css: -------------------------------------------------------------------------------- 1 | .assignment-list-overlay { 2 | display: flex; 3 | z-index: 1000; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100vw; 7 | height: 100vh; 8 | top: 0; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | position: fixed; 13 | background: rgba(128, 128, 128, 0.5); 14 | } 15 | 16 | .assignment-list { 17 | position: absolute; 18 | z-index: 1001; 19 | border-radius: 5px; 20 | padding: 10px; 21 | box-shadow: 1px 1px 3px black; 22 | background: white; 23 | color: black; 24 | text-align: center; 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | .assignment-list h5 { 30 | margin: 10px; 31 | } 32 | 33 | .assignment-list > table { 34 | margin: auto; 35 | } 36 | 37 | .assignment-list > table > tbody { 38 | display: block; 39 | border: solid 1px #ddd; 40 | border-radius: 5px; 41 | overflow-y: scroll; 42 | height: calc(300px - 1.5rem); 43 | } 44 | 45 | .assignment-list-cell { 46 | display: flex; 47 | justify-content: start; 48 | align-items: center; 49 | padding: 5px 2px; 50 | border-top: 1px solid #eee; 51 | border-bottom: 1px solid #eee; 52 | } 53 | 54 | .assignment-list-cell > input { 55 | margin: 0 10px; 56 | } 57 | 58 | .assignment-list .close-assignment-list { 59 | background: #00ffc4!important; 60 | width: 80px; 61 | font-size: 1em!important; 62 | margin: 20px auto 10px auto; 63 | border-radius: 80px; 64 | color: white!important; 65 | } 66 | 67 | .card-member-view { 68 | margin: 0 10px; 69 | } 70 | 71 | .name-container { 72 | text-align: start; 73 | font-size: 12px; 74 | margin: 0 10px; 75 | } 76 | 77 | .name-container > p { 78 | margin-bottom: 0; 79 | } 80 | -------------------------------------------------------------------------------- /kanception/src/assignment-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CardMemberView from './features/kanban/card-member-view' 3 | import './assignment-list.css' 4 | 5 | const AssignmentList = props => ( 6 |
7 |
8 |
Assign to
9 | 10 | 11 | { 12 | props.members.map(member => 13 | 14 | 22 | 23 | ) 24 | } 25 | 26 |
27 | 28 |
29 |
30 | ) 31 | 32 | const AssignmentListCell = props => { 33 | const onChange = e => { 34 | if (e.target.checked === true) { 35 | props.onAddAssignment(e.target.dataset.userId) 36 | } else { 37 | props.onDeleteAssignment(e.target.dataset.userId) 38 | } 39 | } 40 | 41 | return ( 42 |
43 | 49 | 50 |
51 |

{props.name.first + " " + props.name.last}

52 |

{props.email}

53 |
54 |
55 | ) 56 | } 57 | 58 | export default AssignmentList 59 | -------------------------------------------------------------------------------- /kanception/src/auth_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "kanception.auth0.com", 3 | "clientId": "0Fq4bCIH2I5q0j4jrNNOIKrIeYbKwn8s", 4 | "audience": "https://kanception.auth0.com/api/v2/" 5 | } 6 | -------------------------------------------------------------------------------- /kanception/src/constants.js: -------------------------------------------------------------------------------- 1 | export const SPACES = 'SPACES' 2 | export const ASSIGNMENTS = 'ASSIGNMENTS' 3 | -------------------------------------------------------------------------------- /kanception/src/electron.js: -------------------------------------------------------------------------------- 1 | 2 | const { app, BrowserWindow } = require('electron'); 3 | const path = require('path'); 4 | const url = require('url'); 5 | let mainWindow; 6 | 7 | function createWindow () { 8 | const startUrl = process.env.ELECTRON_START_URL || url.format({ 9 | pathname: path.join(__dirname, '../public/index.html'), 10 | protocol: 'file:', 11 | slashes: true, 12 | }); 13 | mainWindow = new BrowserWindow({width: 1280, height: 980, titleBarStyle: 'hidden'}); 14 | mainWindow.loadURL('https://app.kanception.io/'); 15 | // mainWindow.loadURL(startUrl); 16 | mainWindow.on('closed', function () { 17 | mainWindow = null; 18 | }); 19 | } 20 | 21 | app.on('ready', createWindow); 22 | 23 | app.on('window-all-closed', function () { 24 | if (process.platform !== 'darwin') { 25 | app.quit(); 26 | } 27 | }); 28 | 29 | app.on('activate', function () { 30 | if (mainWindow === null) { 31 | createWindow(); 32 | } 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /kanception/src/features/assignments/assignmentsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const assignmentsSlice = createSlice({ 4 | name: 'assignments', 5 | initialState: { 6 | entries: [], 7 | }, 8 | reducers: { 9 | setEntries: (state, action) => { 10 | state.entries = action.payload.entries 11 | }, 12 | addAssignment2: (state, action) => { 13 | state.entries.push(action.payload.entries) 14 | }, 15 | deleteAssignment2: (state, action) => { 16 | state.entries = state.entries 17 | .filter(entry => entry.board === action.payload.board 18 | && entry.assignee === action.payload.assignee 19 | ) 20 | }, 21 | }, 22 | }); 23 | 24 | export const { 25 | setEntries, 26 | addAssignment2, 27 | deleteAssignment2, 28 | } = assignmentsSlice.actions 29 | 30 | export default assignmentsSlice.reducer 31 | -------------------------------------------------------------------------------- /kanception/src/features/comments/commentsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const commentsSlice = createSlice({ 4 | name: 'comments', 5 | initialState: { 6 | comments: [], 7 | }, 8 | reducers: { 9 | setComments: (state, action) => { 10 | state.comments = action.payload.comments 11 | }, 12 | addComment: (state, action) => { 13 | state.comments.unshift(action.payload.comment) 14 | }, 15 | }, 16 | }); 17 | 18 | export const { 19 | setComments, 20 | addComment, 21 | } = commentsSlice.actions 22 | 23 | export default commentsSlice.reducer 24 | -------------------------------------------------------------------------------- /kanception/src/features/gantt/gantt.css: -------------------------------------------------------------------------------- 1 | .gantt { 2 | height: 65vh; 3 | margin: 5px; 4 | margin-top: 40px; 5 | background: rgb(22, 22, 46); 6 | color: white; 7 | border-radius: 5px; 8 | } 9 | 10 | .gantt-channel { 11 | height: calc(65vh / 8); 12 | } 13 | 14 | .todo-panel { 15 | position: absolute; 16 | background: rgb(22, 22, 22); 17 | height: calc(100vh - 50px); 18 | width: 200px!important; 19 | } 20 | 21 | .todo-inner-panel { 22 | overflow-y: auto; 23 | height: calc(100% - 150px); 24 | scrollbar-width: none; /* Firefox */ 25 | } 26 | 27 | .todo-inner-panel::-webkit-scrollbar { 28 | display: none; /* Safari and Chrome */ 29 | } 30 | 31 | .todo-cell { 32 | height: 50px; 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | } 38 | 39 | .todo-cell > p { 40 | margin-bottom: 0; 41 | color: white; 42 | } 43 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/kanception/src/features/kanban/arrow.png -------------------------------------------------------------------------------- /kanception/src/features/kanban/card-member-view.css: -------------------------------------------------------------------------------- 1 | .card-member-container { 2 | display: flex; 3 | width: calc(100% - 20px); 4 | flex-wrap: wrap; 5 | margin: 10px; 6 | } 7 | 8 | .card-member-view { 9 | border-radius: 100%; 10 | background: rebeccapurple; 11 | height: 37px; 12 | width: 37px; 13 | line-height: 37px; 14 | margin: 2px!important; 15 | text-align: center; 16 | } 17 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/card-member-view.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import './card-member-view.css' 3 | 4 | const CardMemberView = props => ( 5 |
{ 6 | e.stopPropagation() 7 | e.preventDefault() 8 | }}> 9 | {props.text} 10 |
11 | ) 12 | 13 | export default CardMemberView 14 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/card.css: -------------------------------------------------------------------------------- 1 | .kanception-card { 2 | margin: 10px; 3 | background: #382d5a; 4 | min-height: 70px; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: flex-start; 8 | justify-content: flex-start; 9 | border-radius: 5px; 10 | background: #382d5a; 11 | box-shadow: 0 0 10px 3px var(--shadow-color); 12 | } 13 | 14 | .share-select { 15 | visibility: hidden; 16 | margin: 10px; 17 | opacity: 0.75; 18 | } 19 | 20 | .kanception-card:hover .share-select { 21 | visibility: visible; 22 | } 23 | 24 | .kanception-card textarea { 25 | outline: none; 26 | overflow: visible; 27 | scrollbar-width: none; /* Firefox */ 28 | width: 80%; 29 | margin-top: 5px; 30 | margin-bottom: 25px; 31 | margin-left: 10px; 32 | text-align: start; 33 | } 34 | 35 | textarea::-webkit-scrollbar { 36 | display: none; /* Safari and Chrome */ 37 | } 38 | 39 | .icon-cards { 40 | border: solid 2px white; 41 | border-radius: 4px; 42 | min-width: 32px; 43 | text-align: center; 44 | font-size: 12px; 45 | } 46 | 47 | .go-into-btn { 48 | width: 25%; 49 | } 50 | 51 | .kanception-card .go-into-btn { 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | transform: rotate(-90deg); 56 | } 57 | 58 | .kanception-card:hover .go-into-btn > div { 59 | width: 30px; 60 | height: 30px; 61 | background: url(./arrow.png) no-repeat; 62 | filter: invert(1); 63 | object-fit: scale-down; 64 | } 65 | 66 | .go-into-btn:hover { 67 | cursor: pointer; 68 | } 69 | 70 | .go-into-btn:hover div { 71 | filter: none!important; 72 | } 73 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/comment-box-submit.css: -------------------------------------------------------------------------------- 1 | .comment-box-submit { 2 | min-height: fit-content; 3 | } 4 | 5 | .comment-submit-btn { 6 | background: #420E97!important; 7 | color: white!important; 8 | font-size: 1em!important; 9 | border-radius: 5px!important; 10 | margin: 10px; 11 | display: inherit; 12 | } 13 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/comment-box-submit.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react' 2 | import TextAreaAutoSize from 'react-textarea-autosize' 3 | import './comment-box-submit.css' 4 | 5 | const CommentBoxSubmit = props => { 6 | const [text, setText] = useState('') 7 | const textArea = useRef(null) 8 | 9 | return ( 10 |
11 | { 24 | if (e.target.value.length < 1000) { 25 | setText(e.target.value) 26 | } 27 | }} 28 | /> 29 | 42 |
43 | ) 44 | } 45 | 46 | export default CommentBoxSubmit 47 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/comment-box.css: -------------------------------------------------------------------------------- 1 | .comment-box { 2 | width: 100%; 3 | min-height: fit-content; 4 | background: whitesmoke; 5 | border-radius: 5px; 6 | display: block; 7 | text-align: start!important; 8 | margin: 10px auto; 9 | } 10 | 11 | .comment-box p { 12 | color: black; 13 | margin: 5px; 14 | } 15 | 16 | .comment-box > p { 17 | color: black; 18 | margin: 10px; 19 | margin-left: 65px; 20 | white-space: pre-wrap; 21 | } 22 | 23 | .user-box { 24 | display: flex; 25 | align-items: center; 26 | } 27 | 28 | .user-box > .card-member-view { 29 | margin: 10px; 30 | } 31 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/comment-box.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CardMemberView from './card-member-view' 3 | import './comment-box.css' 4 | 5 | const nameToInitials = name => { 6 | return name.first.slice(0, 1).toUpperCase() 7 | + name.last.slice(0, 1).toUpperCase() 8 | } 9 | 10 | const displayName = ({first, last}) => { 11 | return first[0].toUpperCase() + first.slice(1) 12 | + ' ' 13 | + last[0].toUpperCase() + last.slice(1) 14 | } 15 | 16 | const displayDate = timestamp => { 17 | const date = new Date(Math.floor(timestamp)) 18 | return date.toLocaleString(undefined, 19 | {dateStyle: 'full', timeStyle: 'short'}) 20 | } 21 | 22 | const CommentBox = props => ( 23 |
24 |
25 |
26 | 29 |
30 |
31 |

32 | {displayName(props.name)} 33 |

34 |

35 | {displayDate(props.timestamp)} 36 |

37 |
38 |
39 |

{props.text}

40 |
41 | ) 42 | 43 | export default CommentBox 44 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/comments-view.css: -------------------------------------------------------------------------------- 1 | .comments-view-overlay { 2 | position: fixed; 3 | z-index: 100; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | background: rgba(127, 127, 127, 0.5); 12 | } 13 | 14 | .comments-view { 15 | display: flex; 16 | overflow: auto; 17 | width: 100%; 18 | max-width: 1200px; 19 | height: 100%; 20 | max-height: 900px; 21 | background: #fff; 22 | border-radius: 15px; 23 | z-index: 101; 24 | } 25 | 26 | .comments-view-left { 27 | width: 45%; 28 | margin: 40px 25px 40px 50px; 29 | } 30 | 31 | .comments-view-right { 32 | display: flex; 33 | flex-direction: column; 34 | align-items: center; 35 | width: 45%; 36 | margin: 40px 0px 40px 25px; 37 | } 38 | 39 | .comments-view-right-bar { 40 | width: fit-content; 41 | } 42 | 43 | .comments-view h3 { 44 | color: black; 45 | margin: 20px; 46 | font-size: 2em; 47 | text-align: center; 48 | 49 | } 50 | .comments-view-right > h3 { 51 | font-weight: lighter; 52 | } 53 | 54 | .comments-view h4 { 55 | color: black; 56 | font-size: 1em; 57 | text-align: center; 58 | } 59 | 60 | .comments-box { 61 | overflow: auto; 62 | width: 100%; 63 | max-height: 500px; 64 | display: flex; 65 | flex-direction: column; 66 | align-items: center; 67 | } 68 | 69 | .close-comments-btn { 70 | color: #420E97!important; 71 | font-size: 1em!important; 72 | display: inherit; 73 | border: solid 2px #420E97!important; 74 | width: 30px; 75 | height: 30px; 76 | display: flex; 77 | align-items: center; 78 | justify-content: center; 79 | border-radius: 30px; 80 | margin: 10px; 81 | } 82 | 83 | .close-comments-btn-mobile { 84 | display: none; 85 | } 86 | 87 | .close-comments-btn > svg { 88 | transform: scale(2.0); 89 | } 90 | 91 | .close-comments-btn:hover svg { 92 | transform: rotate(90deg) scale(2.0); 93 | transition: 0.5s; 94 | } 95 | 96 | @media screen and (max-width: 1300px) { 97 | .comments-view { 98 | width: 100%; 99 | height: 100%; 100 | max-width: 95vw; 101 | max-height: 95vh; 102 | } 103 | } 104 | @media screen and (max-width: 1024px) { 105 | .close-comments-btn:hover svg { 106 | transform: rotate(90deg) scale(1.0); 107 | transition: 0.5s; 108 | } 109 | 110 | .close-comments-btn-mobile { 111 | display: flex; 112 | } 113 | 114 | .comments-view { 115 | flex-direction: column; 116 | } 117 | 118 | .comments-view-left { 119 | width: calc(100% - 20px); 120 | margin: 10px; 121 | } 122 | 123 | .comments-view-right { 124 | display: flex; 125 | flex-direction: column; 126 | align-items: center; 127 | width: calc(100% - 20px); 128 | margin: 10px; 129 | } 130 | 131 | .comments-view-right-bar { 132 | display: none; 133 | } 134 | 135 | } 136 | 137 | @media screen and (max-width: 600px) { 138 | .comments-view { 139 | width: 100%; 140 | height: 100%; 141 | max-width: 100%; 142 | max-height: 100%; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/comments-view.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { connect } from 'react-redux' 3 | import CommentBox from './comment-box' 4 | import CommentBoxSubmit from './comment-box-submit' 5 | import TextAreaAutoSize from 'react-textarea-autosize' 6 | import { setComments } from '../comments/commentsSlice' 7 | import { useAuth0 } from '../../react-auth0-spa' 8 | import { io } from 'socket.io-client' 9 | import './comments-view.css' 10 | 11 | const CommentsView = props => { 12 | const { getTokenSilently } = useAuth0() 13 | 14 | useEffect(() => { 15 | getTokenSilently() 16 | .then(token => { 17 | 18 | const socket = io.connect(process.env.REACT_APP_API, { 19 | "transports": ['websocket'], 20 | }) 21 | 22 | socket.on('connect_error', (error) => { 23 | console.log(error) 24 | }) 25 | 26 | socket.on('connect', () => { 27 | console.log('on connect') 28 | socket.emit('authenticate_comments', { token: token, board: props.board }) 29 | }) 30 | 31 | socket.on('send_comments', async (data) => { 32 | console.log('send_comments') 33 | console.log(data.comments) 34 | await props.dispatch(setComments({comments: data.comments})) 35 | }) 36 | 37 | socket.on('create_comment', (data) => { 38 | const currentCommentIds = props.comments.map(comment => comment._id) 39 | if (currentCommentIds.includes(data.comment._id) === false) { 40 | props.onSubmitComment(data.comment, props.board) 41 | } 42 | }) 43 | }) 44 | 45 | }, [props.board]) 46 | 47 | const onSubmitComment = async text => { 48 | const token = await getTokenSilently() 49 | const url = process.env.REACT_APP_API + '/comments' 50 | + '?text=' + text.replace(/\n/g, '%0A') 51 | + '&board=' + props.board 52 | const result = await fetch(url, { 53 | method: 'POST', 54 | headers: { 55 | Authorization: `Bearer ${token}`, 56 | }, 57 | }) 58 | 59 | const comment = await result.json() 60 | // await props.onSubmitComment(comment, props.board) 61 | } 62 | 63 | return ( 64 |
65 |
66 | 71 |
72 |

{props.title}

73 | { 90 | if (e.target.value.length < 10000) { 91 | props.setDescription(e.target.value) 92 | } 93 | }}> 94 | 95 |
96 |
97 |

Comments

98 | 99 | 100 |
101 | { 102 | props.comments && props.comments.map( 103 | comment => 104 | m._id === comment.owner)?.firstName, 108 | last: props.members.find(m => m._id === comment.owner)?.lastName 109 | }} 110 | text={comment.text} 111 | /> 112 | ) 113 | } 114 |
115 |
116 |
117 | 122 |
123 |
124 |
125 | ) 126 | } 127 | 128 | const mapStateToProps = state => { 129 | return { 130 | comments: state.comments.comments, 131 | } 132 | } 133 | 134 | export default connect(mapStateToProps)(CommentsView) 135 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/context-menu.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | forwardRef, 5 | useRef, 6 | useImperativeHandle 7 | } from 'react' 8 | 9 | import './kanban.css' 10 | 11 | const CardContextMenu = props => { 12 | const style = { 13 | visibility: props.isOpen === true ? 'visible' : 'hidden', 14 | top: props.position.y, 15 | left: props.position.x, 16 | } 17 | 18 | const onContextMenu = e => { 19 | e.preventDefault() 20 | props.onClose(e) 21 | } 22 | 23 | return ( 24 |
30 |
Delete Card
31 |
Assignment
32 |
Close
33 |
34 | ) 35 | } 36 | 37 | const GroupContextMenu = props => { 38 | const style = { 39 | visibility: props.isOpen === true ? 'visible' : 'hidden', 40 | top: props.position.y, 41 | left: props.position.x, 42 | } 43 | 44 | const onContextMenu = e => { 45 | e.preventDefault() 46 | props.onClose(e) 47 | } 48 | 49 | return ( 50 |
56 |
Delete Group
57 |
Close
58 |
59 | ) 60 | } 61 | 62 | export { CardContextMenu, GroupContextMenu } 63 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/icon-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/kanception/src/features/kanban/icon-comment.png -------------------------------------------------------------------------------- /kanception/src/features/kanban/kanban.css: -------------------------------------------------------------------------------- 1 | .kanban { 2 | overflow: visible; 3 | height: calc(100% - 100px); 4 | margin-top: 100px; 5 | } 6 | 7 | #kanbanRoot { 8 | width: fit-content; 9 | display: flex; 10 | color: white; 11 | text-align: center; 12 | height: 100%; 13 | } 14 | 15 | #kanbanRoot button { 16 | background: inherit; 17 | border: none; 18 | color: var(--text-color); 19 | font-size: 2em; 20 | outline: none; 21 | } 22 | 23 | .column { 24 | width: 300px; 25 | min-height: 50px; 26 | max-height: calc(100vh - 225px); 27 | background: var(--foreground-color); 28 | margin: 10px; 29 | border: solid var(--foreground-color) 10px; 30 | border-radius: 5px; 31 | overflow-y: auto; 32 | box-shadow: 0 0 5px 1px var(--shadow-color); 33 | scrollbar-width: none; /* Firefox */ 34 | } 35 | 36 | .column::-webkit-scrollbar { 37 | display: none; /* Safari and Chrome */ 38 | } 39 | 40 | .pointer:hover { 41 | cursor: pointer; 42 | } 43 | 44 | #kanbanRoot .add-group-btn { 45 | font-size: 1em; 46 | background: var(--foreground-color); 47 | } 48 | 49 | .group-title-input { 50 | font-size: 1.5em; 51 | width: 70%; 52 | background: inherit; 53 | color: var(--text-color); 54 | border: none; 55 | text-align: center; 56 | margin-top: 20px; 57 | margin-bottom: 20px; 58 | white-space: nowrap; 59 | } 60 | 61 | .hide { 62 | visibility: hidden; 63 | } 64 | 65 | .context-menu { 66 | position: fixed; 67 | width: 200px; 68 | background: white; 69 | border-radius: 5px; 70 | color: black; 71 | box-shadow: 0 2px 2px #444; 72 | display: flex; 73 | flex-direction: column; 74 | justify-content: space-between; 75 | } 76 | 77 | .context-menu h6 { 78 | padding: 10px; 79 | } 80 | 81 | .context-menu .border-bottom { 82 | border-bottom: 1px solid #aaa; 83 | } 84 | 85 | .context-menu .border-top { 86 | border-top: 1px solid #aaa; 87 | } 88 | 89 | .group { 90 | width: 320px; 91 | height: fit-content; 92 | } 93 | 94 | .group-control { 95 | display: flex; 96 | justify-content: center; 97 | } 98 | 99 | .rbd-droppable-context { 100 | min-height: 40px; 101 | overflow-x: hidden; 102 | } 103 | 104 | @media screen and (max-width: 768px) { 105 | .column { 106 | max-height: calc(100vh - 275px); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/kanbanSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const kanbanSlice = createSlice({ 4 | name: 'kanban', 5 | initialState: { 6 | groups: [], 7 | boards: [], 8 | tree: [], 9 | }, 10 | reducers: { 11 | addGroup: (state, action) => { 12 | state.groups.push(action.payload.group) 13 | }, 14 | setGroups: (state, action) => { 15 | state.groups = action.payload.groups 16 | }, 17 | setBoards: (state, action) => { 18 | state.boards = action.payload.boards 19 | }, 20 | addBoard: (state, action) => { 21 | state.tree.push(action.payload.board) 22 | }, 23 | setBoardTeam: (state, action) => { 24 | const boardRef = state.tree.find(node => node.board === action.payload.board) 25 | if (action.payload.team === 'Private') { 26 | boardRef.team = 'Private' 27 | } else { 28 | boardRef.team = action.payload.team 29 | } 30 | }, 31 | updateBoard: (state, action) => { 32 | console.log(action.payload) 33 | let board = state.tree.find(board => board._id === action.payload.id) 34 | board = Object.assign(board, action.payload.object) 35 | }, 36 | updateGroup: (state, action) => { 37 | let group = state.groups.find(group => group._id === action.payload.id) 38 | group = Object.assign(group, action.payload.object) 39 | state.groups = [group, ...state.groups.filter(group => group._id !== action.payload.id)] 40 | state.groups.sort((a, b) => a.order - b.order) 41 | }, 42 | setTree: (state, action) => { 43 | state.tree = action.payload.tree 44 | }, 45 | cardDelete: (state, action) => { 46 | state.tree = state.tree.filter(node => node._id !== action.payload.card) 47 | }, 48 | groupDelete: (state, action) => { 49 | state.groups = state.groups.filter(group => group._id !== action.payload.group) 50 | }, 51 | addAssignment: (state, action) => { 52 | if (state.tree.find(board => board._id === action.payload.board).assignees?.includes(action.payload.assignee) === false) { 53 | state.tree.find(board => board._id === action.payload.board).assignees.push(action.payload.assignee) 54 | } 55 | }, 56 | deleteAssignment: (state, action) => { 57 | state.tree.find(board => board._id === action.payload.board).assignees = 58 | state.tree.find(board => board._id === action.payload.board).assignees?.filter(assignee => assignee !== action.payload.assignee) 59 | }, 60 | addCommentIcon: (state, action) => { 61 | state.tree.find(board => board._id === action.payload.board).comments = true 62 | }, 63 | }, 64 | }); 65 | 66 | export const { 67 | setGroups, 68 | setBoards, 69 | addGroup, 70 | addBoard, 71 | updateBoard, 72 | updateGroup, 73 | setTree, 74 | setBoardTeam, 75 | cardDelete, 76 | groupDelete, 77 | addAssignment, 78 | deleteAssignment, 79 | addCommentIcon, 80 | } = kanbanSlice.actions 81 | 82 | export default kanbanSlice.reducer 83 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/new-card-popup.css: -------------------------------------------------------------------------------- 1 | .new-card-popup { 2 | width: 175px; 3 | height: 90px; 4 | position: absolute; 5 | background: white; 6 | color: black; 7 | border-radius: 5px; 8 | } 9 | 10 | .new-card-popup ul { 11 | list-style: none; 12 | text-align: center; 13 | padding: 0; 14 | } 15 | 16 | .new-card-popup li { 17 | padding: 10px; 18 | } 19 | 20 | .new-card-popup li:nth-child(1) { 21 | border-bottom: 1px solid #aaa; 22 | } 23 | 24 | .new-card-popup li:active { 25 | background: #eee; 26 | } 27 | -------------------------------------------------------------------------------- /kanception/src/features/kanban/new-card-popup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './new-card-popup.css' 3 | 4 | const NewCardPopup = props => { 5 | return ( 6 |
7 |
    8 |
  • Add private card
  • 9 | { 10 | props.showAddTeamCard === true && 11 |
  • Add team card
  • 12 | } 13 |
14 |
15 | ) 16 | } 17 | 18 | export default NewCardPopup 19 | -------------------------------------------------------------------------------- /kanception/src/features/list-view/list-view.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --cell-margin: 10px; 3 | } 4 | 5 | .list-cell { 6 | max-width: 800px; 7 | width: 100%; 8 | padding: 30px; 9 | background: #242628; 10 | color: white; 11 | border-radius: 5px; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | margin-top: var(--cell-margin); 16 | margin-bottom: var(--cell-margin); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /kanception/src/features/list-view/list-view.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Draggable, Droppable, DragDropContext } from 'react-beautiful-dnd' 4 | import './list-view.css' 5 | 6 | const BoardsListView = props => { 7 | return ( 8 | 11 | {provided => 12 |
24 | {props.boards.map((board, index) => 25 | 26 | )} 27 | {provided.placeholder} 28 |
29 | } 30 |
31 | ) 32 | } 33 | 34 | const ListCell = props => { 35 | return ( 36 | 40 | {provided => 41 |
47 | {props.title} 48 |
49 | } 50 |
51 | ) 52 | } 53 | 54 | const mapStateToProps = state => { 55 | return { 56 | boards: state.kanban.boards 57 | } 58 | } 59 | 60 | export default connect(mapStateToProps)(BoardsListView) 61 | 62 | -------------------------------------------------------------------------------- /kanception/src/features/projects/projectsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const projectsSlice = createSlice({ 4 | name: 'projects', 5 | initialState: { 6 | selectedProject: null, 7 | selectedNode: null, 8 | projects: [] 9 | }, 10 | reducers: { 11 | setProjects: (state, action) => { 12 | state.projects = action.payload.projects 13 | }, 14 | addProject: (state, action) => { 15 | state.projects.push(action.payload.project) 16 | }, 17 | deleteProject: (state, action) => { 18 | const project = state.projects.find(p => p._id === action.payload.project) 19 | state.projects = state.projects.filter(p => p._id !== action.payload.project) 20 | if (state.projects.filter(p => p.space === project.space).length > 0) { 21 | state.selectedProject = state.projects 22 | .filter(p => p.space === project.space)[0]._id 23 | } else { 24 | state.selectedProject = null 25 | } 26 | }, 27 | setSelectedProject: (state, action) => { 28 | state.selectedProject = action.payload.project 29 | }, 30 | setSelectedNode: (state, action) => { 31 | state.selectedNode = action.payload.id 32 | }, 33 | }, 34 | }) 35 | 36 | export const { 37 | setProjects, 38 | addProject, 39 | deleteProject, 40 | setSelectedProject, 41 | setSelectedNode, 42 | } = projectsSlice.actions 43 | 44 | export default projectsSlice.reducer 45 | -------------------------------------------------------------------------------- /kanception/src/features/spaces/spacesSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const spacesSlice = createSlice({ 4 | name: 'spaces', 5 | initialState: { 6 | selectedSpace: null, 7 | spaces: [], 8 | }, 9 | reducers: { 10 | setSpaces: (state, action) => { 11 | state.spaces = action.payload.spaces 12 | }, 13 | addSpace: (state, action) => { 14 | state.spaces.push(action.payload.space) 15 | }, 16 | deleteSpace: (state, action) => { 17 | state.spaces = state.spaces 18 | .filter(space => space._id !== action.payload.space) 19 | }, 20 | }, 21 | }); 22 | 23 | export const { 24 | setSpaces, 25 | addSpace, 26 | deleteSpace, 27 | } = spacesSlice.actions 28 | 29 | export default spacesSlice.reducer 30 | -------------------------------------------------------------------------------- /kanception/src/features/teams/teamsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const teamsSlice = createSlice({ 4 | name: 'teams', 5 | initialState: { 6 | teams: [], 7 | selectedTeam: null, 8 | members: [], 9 | newCards: [], 10 | }, 11 | reducers: { 12 | setTeams: (state, action) => { 13 | state.teams = action.payload.teams 14 | }, 15 | addTeam: (state, action) => { 16 | state.teams.unshift(action.payload.team) 17 | }, 18 | setSelectedTeam: (state, action) => { 19 | state.selectedTeam = action.payload.team 20 | }, 21 | setMembers: (state, action) => { 22 | state.members = action.payload.members 23 | }, 24 | setNewCards: (state, action) => { 25 | state.newCards = action.payload.cards 26 | }, 27 | removeNewCard: (state, action) => { 28 | state.newCards = state.newCards.filter(card => card._id !== action.payload.id) 29 | }, 30 | }, 31 | }); 32 | 33 | export const { 34 | setTeams, 35 | setMembers, 36 | addTeam, 37 | setSelectedTeam, 38 | setNewCards, 39 | removeNewCard, 40 | } = teamsSlice.actions 41 | 42 | export default teamsSlice.reducer 43 | -------------------------------------------------------------------------------- /kanception/src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | width: 100%; 4 | user-select: none; 5 | overscroll-behavior-x: none; 6 | } 7 | 8 | body { 9 | background: #1c1c1c; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | margin: 0; 14 | } 15 | -------------------------------------------------------------------------------- /kanception/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Auth0Provider } from './react-auth0-spa' 4 | import history from './utils/history' 5 | import config from './auth_config.json' 6 | import store from './app/store' 7 | import Router from './router' 8 | import { Provider } from 'react-redux' 9 | 10 | import './index.css' 11 | 12 | const onRedirectCallback = appState => { 13 | history.push( 14 | appState && appState.targetUrl 15 | ? appState.targetUrl 16 | : window.location.pathname 17 | ) 18 | } 19 | 20 | ReactDOM.render( 21 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | , 35 | document.getElementById('root') 36 | ) 37 | -------------------------------------------------------------------------------- /kanception/src/invite-table-view.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import './table-view.css' 3 | 4 | const TableCell = props => { 5 | console.log(props.cellId) 6 | return ( 7 |
15 | 29 |
{props.title}
32 | 46 |
47 | ) 48 | } 49 | 50 | const TableView = props => { 51 | const onCellClick = e => { 52 | props.onSetSelectedTeam(e.target.dataset.cellId) 53 | } 54 | 55 | return ( 56 |
57 | { 58 | props.cells 59 | .map(cell => ( 60 | {_id: cell._id, title: cell.title, 61 | highlighted: cell._id === props.selectedId}) 62 | ) 63 | .map(cell => 64 | 72 | ) 73 | } 74 |
75 | ) 76 | } 77 | 78 | export default TableView 79 | -------------------------------------------------------------------------------- /kanception/src/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAuth0 } from './react-auth0-spa' 3 | 4 | const Login = () => { 5 | const { isAuthenticated, loginWithRedirect, logout } = useAuth0() 6 | 7 | return ( 8 |
9 | {!isAuthenticated && ( 10 | 11 | )} 12 |
13 | ) 14 | } 15 | 16 | export default Login 17 | -------------------------------------------------------------------------------- /kanception/src/logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAuth0 } from './react-auth0-spa' 3 | 4 | const Logout = () => { 5 | const { isAuthenticated, logout } = useAuth0() 6 | 7 | return ( 8 |
12 | Logout 13 | {isAuthenticated && logout()} 14 |
15 | ) 16 | } 17 | 18 | export default Logout 19 | -------------------------------------------------------------------------------- /kanception/src/members-view.css: -------------------------------------------------------------------------------- 1 | .members-container { 2 | overflow: auto; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | flex-wrap: wrap; 7 | background: var(--background-color); 8 | width: 275px; 9 | margin: auto; 10 | border-radius: 5px; 11 | padding: 10px; 12 | } 13 | 14 | .member-view { 15 | background: #4d27cf; 16 | color: white; 17 | margin: 10px; 18 | width: 70px; 19 | height: 70px; 20 | border-radius: 100%; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | .member-view * { 27 | margin: 0; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /kanception/src/menu.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Button } from 'react-bootstrap' 3 | 4 | const TeamTitleMenu = props => { 5 | const [disabled, setDisabled] = useState(true) 6 | const onTitleChange = e => { 7 | if (e.target.value.length > 0) { 8 | setDisabled(false) 9 | } else if (e.target.value.length === 0) { 10 | setDisabled(true) 11 | } 12 | } 13 | 14 | return ( 15 |
27 |
36 | 47 |

Usually a company name or umbrella project title.

48 |
49 | 56 | 64 |
65 |
66 |
67 | ) 68 | } 69 | 70 | const ProjectTitleMenu = props => { 71 | const [disabled, setDisabled] = useState(true) 72 | const onTitleChange = e => { 73 | if (e.target.value.length > 0) { 74 | setDisabled(false) 75 | } else if (e.target.value.length === 0) { 76 | setDisabled(true) 77 | } 78 | } 79 | 80 | return ( 81 |
93 |
102 | 113 |
114 | 121 | 129 |
130 |
131 |
132 | ) 133 | } 134 | 135 | export { ProjectTitleMenu, TeamTitleMenu } 136 | -------------------------------------------------------------------------------- /kanception/src/new-cards-table-view.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react' 2 | import TextAreaAutoSize from 'react-textarea-autosize' 3 | import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' 4 | import './table-view.css' 5 | 6 | const NewCard = props => { 7 | const groupSelect = useRef() 8 | 9 | const onAcceptCard = e => { 10 | const group = groupSelect.current.children[groupSelect.current.selectedIndex].dataset.groupId 11 | props.onAcceptCard(props.id, group) 12 | } 13 | 14 | return ( 15 |
19 |

e.stopPropagation()} 22 | style={{ 23 | color: "white", 24 | background: "inherit", 25 | height: "2rem", 26 | border: "none", 27 | textAlign: "center", 28 | resize: "none", 29 | margin: 0, 30 | marginTop: "10px", 31 | userSelect: "none", 32 | }} 33 | > 34 | {props.title} 35 |

36 | 37 |
38 |

Save to:

39 | 44 | 55 |
56 |
57 | ) 58 | } 59 | 60 | const TableView = props => { 61 | const onCellClick = e => { 62 | props.onSetSelectedTeam(e.target.dataset.cellId) 63 | } 64 | 65 | return ( 66 |
69 | { 70 | props.cells.map((cell, index) => 71 | 73 | ) 74 | } 75 |
76 | ) 77 | } 78 | 79 | export default TableView 80 | -------------------------------------------------------------------------------- /kanception/src/private-route.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Route } from "react-router-dom"; 3 | import { useAuth0 } from "./react-auth0-spa"; 4 | 5 | const PrivateRoute = ({ component: Component, path, ...rest }) => { 6 | const { loading, isAuthenticated, loginWithRedirect } = useAuth0(); 7 | 8 | useEffect(() => { 9 | if (loading || isAuthenticated) { 10 | return; 11 | } 12 | const fn = async () => { 13 | await loginWithRedirect({ 14 | appState: {targetUrl: window.location.pathname} 15 | }); 16 | }; 17 | fn(); 18 | }, [loading, isAuthenticated, loginWithRedirect, path]); 19 | 20 | const render = props => 21 | isAuthenticated === true ? : null; 22 | 23 | return ; 24 | }; 25 | 26 | export default PrivateRoute; 27 | -------------------------------------------------------------------------------- /kanception/src/react-auth0-spa.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import createAuth0Client from "@auth0/auth0-spa-js"; 3 | 4 | const DEFAULT_REDIRECT_CALLBACK = () => 5 | window.history.replaceState({}, document.title, window.location.pathname); 6 | 7 | export const Auth0Context = React.createContext(); 8 | export const useAuth0 = () => useContext(Auth0Context); 9 | export const Auth0Provider = ({ 10 | children, 11 | onRedirectCallback = DEFAULT_REDIRECT_CALLBACK, 12 | ...initOptions 13 | }) => { 14 | const [isAuthenticated, setIsAuthenticated] = useState(); 15 | const [user, setUser] = useState(); 16 | const [auth0Client, setAuth0] = useState(); 17 | const [loading, setLoading] = useState(true); 18 | const [popupOpen, setPopupOpen] = useState(false); 19 | 20 | useEffect(() => { 21 | const initAuth0 = async () => { 22 | const auth0FromHook = await createAuth0Client({ 23 | ...initOptions, 24 | advancedOptions: { 25 | scope: 'email' 26 | } 27 | }); 28 | setAuth0(auth0FromHook); 29 | 30 | if (window.location.search.includes("code=") && 31 | window.location.search.includes("state=")) { 32 | const { appState } = await auth0FromHook.handleRedirectCallback(); 33 | onRedirectCallback(appState); 34 | } 35 | 36 | const isAuthenticated = await auth0FromHook.isAuthenticated(); 37 | 38 | setIsAuthenticated(isAuthenticated); 39 | 40 | if (isAuthenticated) { 41 | const user = await auth0FromHook.getUser(); 42 | setUser(user); 43 | } 44 | 45 | setLoading(false); 46 | }; 47 | initAuth0(); 48 | // eslint-disable-next-line 49 | }, []); 50 | 51 | const loginWithPopup = async (params = {}) => { 52 | setPopupOpen(true); 53 | try { 54 | await auth0Client.loginWithPopup(params); 55 | } catch (error) { 56 | console.error(error); 57 | } finally { 58 | setPopupOpen(false); 59 | } 60 | const user = await auth0Client.getUser(); 61 | setUser(user); 62 | setIsAuthenticated(true); 63 | }; 64 | 65 | const handleRedirectCallback = async () => { 66 | setLoading(true); 67 | await auth0Client.handleRedirectCallback(); 68 | const user = await auth0Client.getUser(); 69 | setLoading(false); 70 | setIsAuthenticated(true); 71 | setUser(user); 72 | }; 73 | return ( 74 | auth0Client.getIdTokenClaims(...p), 83 | loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p), 84 | getTokenSilently: (...p) => auth0Client.getTokenSilently(...p), 85 | getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p), 86 | logout: (...p) => auth0Client.logout(...p) 87 | }} 88 | > 89 | {children} 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /kanception/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' 4 | import PrivateRoute from './private-route' 5 | 6 | import App from './App' 7 | import Login from './login' 8 | import Logout from './logout' 9 | 10 | const AppRouter = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default AppRouter 23 | -------------------------------------------------------------------------------- /kanception/src/side-menu.css: -------------------------------------------------------------------------------- 1 | #side-menu { 2 | overflow: auto; 3 | position: fixed; 4 | width: 300px; 5 | height: 100%; 6 | background: var(--foreground-color); 7 | z-index: 1; 8 | margin-top: 50px; 9 | color: var(--text-color); 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | } 14 | 15 | #side-menu > div > h1 { 16 | margin-top: 20px; 17 | margin-bottom: 20px; 18 | text-align: center; 19 | font-size: 1.5em; 20 | } 21 | 22 | #side-menu > div > div > button, 23 | #side-menu > div > div > div > button { 24 | border: none; 25 | background: #4d27cf; 26 | border-radius: 5px; 27 | padding-left: 10px; 28 | padding-right: 10px; 29 | padding-top: 5px; 30 | padding-bottom: 5px; 31 | color: white; 32 | margin: 20px; 33 | margin-top: 15px; 34 | margin-left: 15px; 35 | } 36 | 37 | .btn-container { 38 | width: 100%; 39 | } 40 | 41 | .btn-container p { 42 | margin-top: 30px; 43 | text-align: center; 44 | } 45 | 46 | 47 | #side-menu h1 { 48 | margin-top: 40px; 49 | } 50 | 51 | #side-menu > div > div > div > p { 52 | margin-top: 40px; 53 | margin-left: 40px; 54 | } 55 | 56 | #side-menu-scroll { 57 | height: 100%; 58 | width: 100%; 59 | overflow: auto; 60 | scrollbar-width: none; /* Firefox */ 61 | } 62 | 63 | #side-menu-scroll::-webkit-scrollbar { 64 | display: none; /* Safari and Chrome */ 65 | } 66 | -------------------------------------------------------------------------------- /kanception/src/space.css: -------------------------------------------------------------------------------- 1 | .space { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .space h1 { 9 | color: var(--text-color); 10 | text-align: flex-start; 11 | margin: 0 auto; 12 | margin-top: 100px; 13 | } 14 | 15 | .space h2 { 16 | color: #bbb; 17 | text-align: center; 18 | margin-top: 30px; 19 | } 20 | 21 | .space button { 22 | width: 150px; 23 | height: 40px; 24 | margin-top: 40px; 25 | color: white; 26 | border: none; 27 | border-radius: 5px; 28 | } 29 | 30 | .space .btn-container { 31 | display: flex; 32 | } 33 | 34 | .space .new-project { 35 | background: black; 36 | margin: 10px; 37 | } 38 | 39 | .space .delete-space { 40 | background: #dc3545; 41 | margin: 10px; 42 | } 43 | -------------------------------------------------------------------------------- /kanception/src/space.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './space.css' 3 | 4 | const Space = props => ( 5 |
6 |
7 | 11 | 15 |
16 |

{props.title}

17 |

Create a new project to get started

18 |
19 | ) 20 | 21 | export default Space 22 | -------------------------------------------------------------------------------- /kanception/src/sub-toolbar.css: -------------------------------------------------------------------------------- 1 | .page-link { 2 | padding: .25rem .5rem!important; 3 | } -------------------------------------------------------------------------------- /kanception/src/sub-toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CommentIcon from './features/kanban/icon-comment.png' 3 | import { Dropdown, Pagination, ToggleButtonGroup, ToggleButton } from 'react-bootstrap' 4 | import HamburgerMenu from 'react-hamburger-menu' 5 | import './sub-toolbar.css' 6 | 7 | const SubToolbar = props => { 8 | return ( 9 |
10 | {/**/} 11 |
19 | { 20 | props.menuOpen === false && 21 |
22 | 30 |
31 | } 32 | 33 | 36 | 37 | 38 |
39 |
{props.title}
40 |
41 | ) 42 | } 43 | 44 | export default SubToolbar -------------------------------------------------------------------------------- /kanception/src/table-view.css: -------------------------------------------------------------------------------- 1 | .table-view { 2 | max-height: 250px; 3 | overflow: auto; 4 | scrollbar-width: none; /* Firefox */ 5 | } 6 | 7 | .table-view::-webkit-scrollbar { 8 | display: none; /* Safari and Chrome */ 9 | } 10 | 11 | .table-cell { 12 | width: 300px; 13 | height: 50px; 14 | text-align: center; 15 | background: #1c1c1c; 16 | border-radius: 5px; 17 | margin: auto; 18 | margin-bottom: 20px; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .table-cell-invite { 25 | margin: auto; 26 | margin-top: 20px; 27 | margin-bottom: 20px; 28 | justify-content: space-between; 29 | } 30 | 31 | .btn-container { 32 | margin: auto; 33 | } 34 | 35 | .MuiTreeItem-iconContainer { 36 | display: none; 37 | } 38 | -------------------------------------------------------------------------------- /kanception/src/team-table-view.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import './table-view.css' 3 | 4 | const TableCell = props => { 5 | console.log(props.cellId) 6 | return ( 7 |
15 |
{props.title}
18 |
19 | ) 20 | } 21 | 22 | const TableView = props => { 23 | const onCellClick = e => { 24 | props.onSetSelectedTeam(e.target.dataset.cellId) 25 | } 26 | 27 | return ( 28 |
29 | { 30 | props.cells 31 | .map(cell => ( 32 | {_id: cell._id, title: cell.title, 33 | highlighted: cell._id === props.selectedId}) 34 | ) 35 | .map(cell => 36 | 42 | ) 43 | } 44 |
45 | ) 46 | } 47 | 48 | export default TableView 49 | -------------------------------------------------------------------------------- /kanception/src/theme-picker.css: -------------------------------------------------------------------------------- 1 | .theme-picker-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | background: rgba(128, 128, 128, 0.5); 10 | z-index: 200; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | 16 | .theme-picker-modal { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: center; 21 | width: 525px; 22 | height: 550px; 23 | border-radius: 15px; 24 | background: #D6D6D6; 25 | text-align: center; 26 | } 27 | 28 | .theme-picker-modal button { 29 | border: none; 30 | height: 60px; 31 | width: 120px; 32 | border-radius: 10px; 33 | color: white; 34 | font-weight: 500; 35 | font-size: 1.1em; 36 | } 37 | 38 | .finish-btn { 39 | background: orange; 40 | } 41 | 42 | .choose-btn { 43 | background: rebeccapurple; 44 | } 45 | 46 | .choice-container { 47 | display: flex; 48 | align-items: space-around; 49 | justify-content: space-around; 50 | } 51 | 52 | .choice { 53 | margin: 20px; 54 | padding: 40px 10px; 55 | display: flex; 56 | flex-direction: column; 57 | align-items: center; 58 | justify-content: space-between; 59 | height: 350px; 60 | border-radius: 15px; 61 | } 62 | 63 | .mode-img { 64 | width: 180px; 65 | } 66 | 67 | @media screen and (max-width: 500px) { 68 | 69 | .theme-picker-modal { 70 | width: 300px; 71 | height: 550px; 72 | } 73 | 74 | .mode-img { 75 | width: 100px; 76 | } 77 | 78 | .theme-picker-modal button { 79 | width: 80px; 80 | } 81 | 82 | .choice { 83 | margin: 10px; 84 | } 85 | 86 | .theme-picker-modal button { 87 | height: 50px; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /kanception/src/theme-picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './theme-picker.css' 3 | import Dark from './mode-dark.svg' 4 | import Light from './mode-light.svg' 5 | 6 | const ThemePicker = props => ( 7 |
8 |
9 |

Choose a Theme

10 |
11 |
17 | Light theme 18 |

Light Mode

19 | 23 |
24 |
30 | Dark theme 31 |

Dark Mode

32 | 36 |
37 |
38 | 39 |
40 |
41 | ) 42 | 43 | export default ThemePicker 44 | -------------------------------------------------------------------------------- /kanception/src/theme.js: -------------------------------------------------------------------------------- 1 | export const themes = { 2 | light: { 3 | variant: 'light', 4 | foreground: '#eeeeee', 5 | background: '#ffffff', 6 | text: '#000000', 7 | shadow: '#aaaaaa', 8 | }, 9 | dark: { 10 | variant: 'dark', 11 | foreground: '#242628', 12 | background: '#1c1c1c', 13 | text: '#ffffff', 14 | shadow: '#111111', 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /kanception/src/toolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | display: flex; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | align-items: center; 8 | justify-content: flex-end; 9 | background: var(--background-color); 10 | height: 50px; 11 | width: 100%; 12 | z-index: 2; 13 | -webkit-app-region: drag; 14 | } 15 | 16 | .page-link { 17 | transform: rotate(90deg); 18 | } 19 | 20 | .toolbar .btn-primary { 21 | background: black; 22 | border: solid 1px white; 23 | box-shadow: 0 0 0 0.2rem #white; 24 | margin: 10px; 25 | } 26 | 27 | .toolbar .btn-primary:hover { 28 | background: black; 29 | border: solid 1px #4043b9; 30 | } 31 | 32 | .toolbar .btn-primary:focus { 33 | border: solid 1px white; 34 | box-shadow: 0 0 0 0.2rem #white; 35 | } 36 | 37 | .toolbar .btn-primary.dropdown-toggle:focus { 38 | background: black; 39 | box-shadow: 0 0 0 0.2rem #white; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /kanception/src/toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Dropdown, Pagination, ToggleButtonGroup, ToggleButton } from 'react-bootstrap' 3 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 4 | import Switch from '@material-ui/core/Switch'; 5 | import HamburgerMenu from 'react-hamburger-menu' 6 | import './toolbar.css' 7 | 8 | const Toolbar = (props) => { 9 | return ( 10 |
11 | { 12 | // 13 | // 26 | // Kanban 27 | // 28 | // 40 | // Gantt 41 | // 42 | // 43 | } 44 | 45 | 46 | 47 | 48 |
49 | 57 | } 58 | label="Dark Mode" 59 | /> 60 |
61 | Sign Out 62 |
63 |
64 |
65 | ) 66 | } 67 | 68 | export default Toolbar 69 | -------------------------------------------------------------------------------- /kanception/src/utils/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history" 2 | export default createBrowserHistory() 3 | -------------------------------------------------------------------------------- /marketing/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /marketing/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /marketing/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /marketing/LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Gatsby Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /marketing/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /marketing/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: `Kanception.io`, 4 | author: `@kanception`, 5 | description: `A team Kanban board. Schudule cards with the Gantt chart. Track time with the calendar.`, 6 | }, 7 | plugins: [ 8 | `gatsby-plugin-react-helmet`, 9 | { 10 | resolve: `gatsby-source-filesystem`, 11 | options: { 12 | name: `images`, 13 | path: `${__dirname}/src/images`, 14 | }, 15 | }, 16 | `gatsby-transformer-sharp`, 17 | `gatsby-plugin-sharp`, 18 | { 19 | resolve: `gatsby-plugin-manifest`, 20 | options: { 21 | name: `gatsby-starter-default`, 22 | short_name: `starter`, 23 | start_url: `/`, 24 | background_color: `#663399`, 25 | theme_color: `#663399`, 26 | display: `minimal-ui`, 27 | icon: `src/images/logo.png`, // This path is relative to the root of the site. 28 | }, 29 | }, 30 | { 31 | resolve: `gatsby-plugin-google-analytics`, 32 | options: { 33 | // replace "UA-XXXXXXXXX-X" with your own Tracking ID 34 | trackingId: "UA-172115728-1", 35 | }, 36 | }, 37 | { 38 | resolve: `gatsby-plugin-canonical-urls`, 39 | options: { 40 | siteUrl: `https://www.kanception.io`, 41 | }, 42 | }, 43 | { 44 | resolve: `gatsby-plugin-hotjar`, 45 | options: { 46 | id: 1912190, 47 | sv: 6 48 | }, 49 | }, 50 | // this (optional) plugin enables Progressive Web App + Offline functionality 51 | // To learn more, visit: https://gatsby.dev/offline 52 | // `gatsby-plugin-offline`, 53 | ], 54 | } 55 | -------------------------------------------------------------------------------- /marketing/gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Node APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/node-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /marketing/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /marketing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kanception.io", 3 | "private": true, 4 | "description": "Kanception.io | A Kanban board for modern workflows", 5 | "version": "0.1.0", 6 | "author": "Hayden Pennington ", 7 | "dependencies": { 8 | "gatsby": "^2.23.12", 9 | "gatsby-cli": "^2.12.58", 10 | "gatsby-image": "^2.4.9", 11 | "gatsby-plugin-canonical-urls": "^2.3.10", 12 | "gatsby-plugin-google-analytics": "^2.3.11", 13 | "gatsby-plugin-hotjar": "^1.0.1", 14 | "gatsby-plugin-manifest": "^2.4.14", 15 | "gatsby-plugin-offline": "^3.2.13", 16 | "gatsby-plugin-react-helmet": "^3.3.6", 17 | "gatsby-plugin-sharp": "^2.6.14", 18 | "gatsby-source-filesystem": "^2.3.14", 19 | "gatsby-transformer-sharp": "^2.5.7", 20 | "prop-types": "^15.7.2", 21 | "react": "^16.12.0", 22 | "react-dom": "^16.12.0", 23 | "react-helmet": "^6.1.0" 24 | }, 25 | "devDependencies": { 26 | "prettier": "2.0.5" 27 | }, 28 | "keywords": [ 29 | "gatsby" 30 | ], 31 | "license": "0BSD", 32 | "scripts": { 33 | "build": "gatsby build", 34 | "develop": "gatsby develop", 35 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 36 | "start": "npm run develop", 37 | "serve": "gatsby serve", 38 | "clean": "gatsby clean", 39 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/gatsbyjs/gatsby/issues" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /marketing/src/components/header.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby" 2 | import PropTypes from "prop-types" 3 | import React from "react" 4 | import Logo from "../images/logo.png" 5 | 6 | const Header = ({ siteTitle }) => ( 7 |
12 |
15 |
16 |
23 |

24 | 25 | 32 | {siteTitle} 33 | 34 |

35 |
36 |
37 | ) 38 | 39 | Header.propTypes = { 40 | siteTitle: PropTypes.string, 41 | } 42 | 43 | Header.defaultProps = { 44 | siteTitle: ``, 45 | } 46 | 47 | export default Header 48 | -------------------------------------------------------------------------------- /marketing/src/components/image.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useStaticQuery, graphql } from "gatsby" 3 | import Img from "gatsby-image" 4 | 5 | /* 6 | * This component is built using `gatsby-image` to automatically serve optimized 7 | * images with lazy loading and reduced file sizes. The image is loaded using a 8 | * `useStaticQuery`, which allows us to load the image from directly within this 9 | * component, rather than having to pass the image data down from pages. 10 | * 11 | * For more information, see the docs: 12 | * - `gatsby-image`: https://gatsby.dev/gatsby-image 13 | * - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/ 14 | */ 15 | 16 | const Image = () => { 17 | const data = useStaticQuery(graphql` 18 | query { 19 | placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) { 20 | childImageSharp { 21 | fluid(maxWidth: 300) { 22 | ...GatsbyImageSharpFluid 23 | } 24 | } 25 | } 26 | } 27 | `) 28 | 29 | return 30 | } 31 | 32 | export default Image 33 | -------------------------------------------------------------------------------- /marketing/src/components/layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout component that queries for data 3 | * with Gatsby's useStaticQuery component 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from "react" 9 | import PropTypes from "prop-types" 10 | import { useStaticQuery, graphql } from "gatsby" 11 | 12 | import Header from "./header" 13 | import "./layout.css" 14 | 15 | const Layout = ({ children }) => { 16 | const data = useStaticQuery(graphql` 17 | query SiteTitleQuery { 18 | site { 19 | siteMetadata { 20 | title 21 | } 22 | } 23 | } 24 | `) 25 | 26 | return ( 27 | <> 28 |
29 |
37 |
{children}
38 |
39 |
40 |
41 | 42 | ) 43 | } 44 | 45 | Layout.propTypes = { 46 | children: PropTypes.node.isRequired, 47 | } 48 | 49 | export default Layout 50 | -------------------------------------------------------------------------------- /marketing/src/components/seo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from "react" 9 | import PropTypes from "prop-types" 10 | import { Helmet } from "react-helmet" 11 | import { useStaticQuery, graphql } from "gatsby" 12 | 13 | function SEO({ description, lang, meta, title }) { 14 | const { site } = useStaticQuery( 15 | graphql` 16 | query { 17 | site { 18 | siteMetadata { 19 | title 20 | description 21 | author 22 | } 23 | } 24 | } 25 | ` 26 | ) 27 | 28 | const metaDescription = description || site.siteMetadata.description 29 | 30 | return ( 31 | 72 | ) 73 | } 74 | 75 | SEO.defaultProps = { 76 | lang: `en`, 77 | meta: [], 78 | description: ``, 79 | } 80 | 81 | SEO.propTypes = { 82 | description: PropTypes.string, 83 | lang: PropTypes.string, 84 | meta: PropTypes.arrayOf(PropTypes.object), 85 | title: PropTypes.string.isRequired, 86 | } 87 | 88 | export default SEO 89 | -------------------------------------------------------------------------------- /marketing/src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /marketing/src/images/kanception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/images/kanception.png -------------------------------------------------------------------------------- /marketing/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/images/logo.png -------------------------------------------------------------------------------- /marketing/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Layout from "../components/layout" 4 | import SEO from "../components/seo" 5 | 6 | const NotFoundPage = () => ( 7 | 8 | 9 |

NOT FOUND

10 |

You just hit a route that doesn't exist... the sadness.

11 |
12 | ) 13 | 14 | export default NotFoundPage 15 | -------------------------------------------------------------------------------- /marketing/src/pages/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/pages/demo.mp4 -------------------------------------------------------------------------------- /marketing/src/pages/gantt.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/pages/gantt.mp4 -------------------------------------------------------------------------------- /marketing/src/pages/gantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/pages/gantt.png -------------------------------------------------------------------------------- /marketing/src/pages/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/pages/graph.png -------------------------------------------------------------------------------- /marketing/src/pages/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: sans-serif; 3 | } 4 | 5 | p { 6 | max-width: 900px; 7 | margin: 20px 10px; 8 | font-size: 1.25em; 9 | text-align: center; 10 | } 11 | 12 | h2 { 13 | margin: 10px; 14 | } 15 | 16 | h4 { 17 | margin: 20px 10px; 18 | } 19 | 20 | .card h4 { 21 | font-size: 1em; 22 | } 23 | 24 | .title { 25 | margin-bottom: 20px; 26 | } 27 | 28 | .top-block { 29 | text-align: center; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | width: 90vw; 35 | border-radius: 24px; 36 | height: fit-content; 37 | background: #1b2448; 38 | box-shadow: 0 20px 44px 0 rgba(27,36,72,.26); 39 | } 40 | 41 | .top-block-inner { 42 | background-image: radial-gradient(circle farthest-corner at 50% 50%,rgba(62,56,127,.45),rgba(62,56,127,0)); 43 | } 44 | 45 | .top-block h1, .top-block p { 46 | text-align: center; 47 | margin: 20px auto; 48 | color: white; 49 | } 50 | 51 | .top-block h1 { 52 | font-size: 50px; 53 | max-width: 1000px; 54 | width: 100%; 55 | margin-top: 60px; 56 | } 57 | 58 | .top-block a { 59 | margin-bottom: 60px; 60 | display: inline-block; 61 | } 62 | 63 | .top-block h2 { 64 | color: #d6dbf0; 65 | width: 90%; 66 | max-width: 700px; 67 | margin: 20px auto; 68 | font-size: 24px; 69 | } 70 | 71 | .gantt-block > p { 72 | margin: 20px auto; 73 | } 74 | 75 | .color-strip { 76 | width: 100%; 77 | height: 12px; 78 | border-radius: 10px; 79 | margin-bottom: 20px; 80 | } 81 | 82 | .red { 83 | background: lightgreen; 84 | } 85 | 86 | .blue { 87 | background: cornflowerblue; 88 | } 89 | 90 | .sign-up-btn { 91 | border: none; 92 | background: orange; 93 | border-radius: 5px; 94 | padding: 5px 10px; 95 | color: black; 96 | text-decoration: none; 97 | margin-bottom: 60px; 98 | font-weight: bold; 99 | } 100 | 101 | .sign-up-btn-bottom { 102 | margin: 60px; 103 | font-weight: bold; 104 | } 105 | 106 | .center-text { 107 | text-align: center; 108 | } 109 | 110 | .mid-width { 111 | max-width: 700px; 112 | } 113 | 114 | .kanban-detail-block { 115 | background: linear-gradient(90deg, #6f27cf, #4d27cf); 116 | width: 100vw; 117 | display: flex; 118 | align-items: center; 119 | justify-content: center; 120 | padding-bottom: 100px; 121 | } 122 | .kanban-detail-text-block { 123 | background: linear-gradient(90deg, #6f27cf, #4d27cf); 124 | text-align: center; 125 | color: white; 126 | font-size: 1.2em; 127 | } 128 | .kanban-detail-text-block > h4 { 129 | margin: 20px 10px; 130 | } 131 | 132 | .main-video { 133 | max-width: 960px; 134 | } 135 | 136 | .card { 137 | background: white; 138 | border-radius: 10px; 139 | width: 400px; 140 | text-align: center; 141 | padding: 30px; 142 | box-shadow: 5px 5px 10px 10px rgb(68 68 68 / 59%); 143 | margin: 20px; 144 | } 145 | 146 | .card progress { 147 | width: 90%; 148 | } 149 | 150 | #gantt-chart { 151 | box-shadow: 0 0 5px 10px #ccc; 152 | border-radius: 5px; 153 | max-width: 960px; 154 | } 155 | 156 | .gantt-block > h4 { 157 | font-size: 1.3em; 158 | margin-bottom: 30px; 159 | } 160 | 161 | .roadmap-block > h1 { 162 | font-size: 3em; 163 | } 164 | 165 | @media screen and (min-height: 900px) { 166 | .top-block { 167 | margin-bottom: 120px; 168 | } 169 | 170 | .top-block h1 { 171 | margin-top: 180px; 172 | } 173 | 174 | .top-block a { 175 | margin-bottom: 180px; 176 | } 177 | } 178 | 179 | @media screen and (max-width: 1024px) { 180 | .top-block h1 { 181 | margin-left: 10px; 182 | margin-right: 10px; 183 | width: auto; 184 | margin-top: 120px; 185 | } 186 | 187 | .card-container { 188 | width: 95%; 189 | max-width: 600px; 190 | } 191 | 192 | .roadmap-block > h1 { 193 | font-size: 2em; 194 | } 195 | 196 | .kanban-detail-block { 197 | flex-direction: column-reverse; 198 | } 199 | 200 | .main-video { 201 | max-width: 95%; 202 | } 203 | 204 | #gantt-chart { 205 | max-width: 95%; 206 | } 207 | 208 | .card { 209 | max-width: 70%; 210 | margin: 20px auto; 211 | width: unset; 212 | } 213 | 214 | .kanban-detail-block img { 215 | margin-right: 0!important; 216 | } 217 | 218 | .gantt-block > p { 219 | margin: 20px 10px; 220 | } 221 | 222 | .top-block a { 223 | margin-bottom: 120px; 224 | } 225 | } 226 | 227 | @media screen and (max-width: 768px) { 228 | .top-block { 229 | margin-bottom: 20px; 230 | } 231 | 232 | .top-block h1 { 233 | font-size: 2em; 234 | } 235 | 236 | .card { 237 | max-width: 70%; 238 | padding: 20px; 239 | width: unset; 240 | margin: 20px auto; 241 | } 242 | } 243 | 244 | @media screen and (max-width: 500px) { 245 | .top-block h1 { 246 | margin-top: 60px!important; 247 | } 248 | .top-block a { 249 | margin-bottom: 60px!important; 250 | } 251 | .card { 252 | width: 100%; 253 | max-width: 90%; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /marketing/src/pages/page-2.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | 4 | import Layout from "../components/layout" 5 | import SEO from "../components/seo" 6 | 7 | const SecondPage = () => ( 8 | 9 | 10 |

Hi from the second page

11 |

Welcome to page 2

12 | Go back to the homepage 13 |
14 | ) 15 | 16 | export default SecondPage 17 | -------------------------------------------------------------------------------- /marketing/src/pages/roadmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpennington/kanception/84c97cd1edd8c19009fbd2585aeb5f53afef560c/marketing/src/pages/roadmap.png -------------------------------------------------------------------------------- /marketing/src/pages/using-typescript.tsx: -------------------------------------------------------------------------------- 1 | // If you don't want to use TypeScript you can delete this file! 2 | import React from "react" 3 | import { PageProps, Link, graphql } from "gatsby" 4 | 5 | import Layout from "../components/layout" 6 | import SEO from "../components/seo" 7 | 8 | type DataProps = { 9 | site: { 10 | buildTime: string 11 | } 12 | } 13 | 14 | const UsingTypescript: React.FC> = ({ data, path }) => ( 15 | 16 | 17 |

Gatsby supports TypeScript by default!

18 |

This means that you can create and write .ts/.tsx files for your pages, components etc. Please note that the gatsby-*.js files (like gatsby-node.js) currently don't support TypeScript yet.

19 |

For type checking you'll want to install typescript via npm and run tsc --init to create a .tsconfig file.

20 |

You're currently on the page "{path}" which was built on {data.site.buildTime}.

21 |

To learn more, head over to our documentation about TypeScript.

22 | Go back to the homepage 23 |
24 | ) 25 | 26 | export default UsingTypescript 27 | 28 | export const query = graphql` 29 | { 30 | site { 31 | buildTime(formatString: "YYYY-MM-DD hh:mm a z") 32 | } 33 | } 34 | ` 35 | -------------------------------------------------------------------------------- /mock/assignment-repository.ts: -------------------------------------------------------------------------------- 1 | import AssignmentRepositoryInterface from '../assignment-repository-interface' 2 | import Assignment = require('../../models/assignment') 3 | import Board = require('../../models/board') 4 | 5 | class AssignmentRepository implements AssignmentRepositoryInterface { 6 | async create(assignee, assigner, board): Promise { 7 | const group = { 8 | assigner: assigner, 9 | assignee: assignee, 10 | board: board._id, 11 | } 12 | 13 | return group 14 | } 15 | 16 | async findAll(criteria): Promise> { 17 | return [] 18 | } 19 | 20 | async findOne(criteria): Promise { 21 | return null 22 | } 23 | 24 | async deleteMany(criteria) { 25 | 26 | } 27 | 28 | async deleteOne(criteria) { 29 | 30 | } 31 | } 32 | 33 | export default AssignmentRepository -------------------------------------------------------------------------------- /mock/board-repository.ts: -------------------------------------------------------------------------------- 1 | import BoardRepositoryInterface from '../board-repository-interface' 2 | import Board from '../../models/user' 3 | 4 | class BoardRepository implements BoardRepositoryInterface { 5 | async find(id: string): Promise { 6 | const board = { 7 | _id: '0f0d514cf6a4dbf1f5d74b7152f440d0', 8 | title: '', 9 | description: '', 10 | owner: '0f0d514cf6a4dbf1f5d74b7152f440d0', 11 | order: '', 12 | project: '', 13 | parent: null, 14 | group: '', 15 | count: 0, 16 | comments: false, 17 | } 18 | 19 | return board 20 | 21 | } 22 | 23 | async findByParent(id: string): Promise> { 24 | const boards = [{ 25 | _id: '0f0d514cf6a4dbf1f5d74b7152f440d0', 26 | title: '', 27 | description: '', 28 | owner: '0f0d514cf6a4dbf1f5d74b7152f440d0', 29 | order: '', 30 | project: '', 31 | parent: id, 32 | group: '', 33 | count: 0, 34 | comments: false, 35 | } 36 | ] 37 | 38 | return boards 39 | } 40 | 41 | async findAll(criteria: Object): Promise> { 42 | const boards = [{ 43 | _id: '0f0d514cf6a4dbf1f5d74b7152f440d0', 44 | title: '', 45 | description: '', 46 | owner: '0f0d514cf6a4dbf1f5d74b7152f440d0', 47 | order: '', 48 | project: '', 49 | parent: null, 50 | group: '', 51 | count: 0, 52 | comments: false, 53 | }] 54 | 55 | return boards 56 | } 57 | 58 | async create(properties): Promise { 59 | properties._id = '0f0d514cf6a4dbf1f5d74b7152f440d0' 60 | return properties 61 | } 62 | 63 | async incrementCount(board, amount) { 64 | } 65 | 66 | async merge(board, body) { 67 | const updatedBoard = Object.assign(board, body) 68 | return updatedBoard 69 | } 70 | 71 | async delete(id: string) { 72 | return true 73 | } 74 | } 75 | 76 | export default BoardRepository -------------------------------------------------------------------------------- /mock/comment-repository.ts: -------------------------------------------------------------------------------- 1 | import CommentRepositoryInterface from '../comment-repository-interface' 2 | 3 | class CommentRepository implements CommentRepositoryInterface { 4 | async create(owner, board, timestamp, text): Promise { 5 | return null 6 | } 7 | 8 | async findAll(criteria): Promise> { 9 | return null 10 | } 11 | 12 | async deleteMany(criteria) { 13 | } 14 | } 15 | 16 | export default CommentRepository -------------------------------------------------------------------------------- /mock/group-repository.ts: -------------------------------------------------------------------------------- 1 | import GroupRepositoryInterface from '../group-repository-interface' 2 | import Group = require('../../models/group') 3 | 4 | class GroupRepository implements GroupRepositoryInterface { 5 | async create(title, owner, order, board): Promise { 6 | const group = { 7 | title: title, 8 | owner: owner._id, 9 | order: order, 10 | board: board._id, 11 | } 12 | 13 | return group 14 | } 15 | } 16 | 17 | export default GroupRepository -------------------------------------------------------------------------------- /mock/user-repository.ts: -------------------------------------------------------------------------------- 1 | import UserRepositoryInterface from '../user-repository-interface' 2 | import User from '../../models/user' 3 | 4 | class UserRepository implements UserRepositoryInterface { 5 | async findBySub(sub: string): Promise { 6 | return { 7 | sub: sub, 8 | _id: '0f0d514cf6a4dbf1f5d74b7152f440d0', 9 | email: '', 10 | name: { 11 | first: '', 12 | last: '', 13 | }, 14 | spaces: [], 15 | active: true, 16 | } 17 | } 18 | async findOne(criteria): Promise { 19 | return criteria 20 | } 21 | } 22 | 23 | export default UserRepository --------------------------------------------------------------------------------