Browse Source

Snapshot fae2fd741d

master
commit
7d74b909ab
69 changed files with 23572 additions and 0 deletions
  1. +1
    -0
      Licence note
  2. +45
    -0
      README.md
  3. BIN
      assets/main.png
  4. BIN
      assets/update.png
  5. +115
    -0
      back/.gitignore
  6. +6
    -0
      back/config/default.json
  7. +9
    -0
      back/config/keys.js
  8. +6
    -0
      back/config/test.json
  9. +15
    -0
      back/controller/MongoController.js
  10. +26
    -0
      back/controller/TaskController.js
  11. +24
    -0
      back/index.js
  12. +22
    -0
      back/model/Task.js
  13. +3565
    -0
      back/package-lock.json
  14. +30
    -0
      back/package.json
  15. +75
    -0
      back/route/TaskRoute.js
  16. +171
    -0
      back/test/controller/TaskController.test.js
  17. +385
    -0
      back/test/route/TaskRoute.test.js
  18. +30
    -0
      docker-compose.yml
  19. +23
    -0
      front/.gitignore
  20. +17413
    -0
      front/package-lock.json
  21. +45
    -0
      front/package.json
  22. BIN
      front/public/favicon.ico
  23. +20
    -0
      front/public/index.html
  24. BIN
      front/public/logo192.png
  25. BIN
      front/public/logo512.png
  26. +25
    -0
      front/public/manifest.json
  27. +3
    -0
      front/public/robots.txt
  28. +19
    -0
      front/src/App.js
  29. +81
    -0
      front/src/action/TaskAction.js
  30. +7
    -0
      front/src/action/TaskType.js
  31. +21
    -0
      front/src/component/generic/DisplayError.js
  32. +49
    -0
      front/src/component/generic/Modal.js
  33. +22
    -0
      front/src/component/task/Task.js
  34. +22
    -0
      front/src/component/task/TaskCreate.js
  35. +137
    -0
      front/src/component/task/TaskForm.js
  36. +35
    -0
      front/src/component/task/TaskIndex.js
  37. +52
    -0
      front/src/component/task/TaskList.js
  38. +34
    -0
      front/src/font/fontello/config.json
  39. +85
    -0
      front/src/font/fontello/css/animation.css
  40. +5
    -0
      front/src/font/fontello/css/fontello-codes.css
  41. +62
    -0
      front/src/font/fontello/css/fontello-embedded.css
  42. +5
    -0
      front/src/font/fontello/css/fontello-ie7-codes.css
  43. +16
    -0
      front/src/font/fontello/css/fontello-ie7.css
  44. +61
    -0
      front/src/font/fontello/css/fontello.css
  45. BIN
      front/src/font/fontello/font/fontello.eot
  46. +18
    -0
      front/src/font/fontello/font/fontello.svg
  47. BIN
      front/src/font/fontello/font/fontello.ttf
  48. BIN
      front/src/font/fontello/font/fontello.woff
  49. BIN
      front/src/font/fontello/font/fontello.woff2
  50. +13
    -0
      front/src/index.css
  51. +17
    -0
      front/src/index.js
  52. +52
    -0
      front/src/reducer/TaskReducer.js
  53. +6
    -0
      front/src/reducer/index.js
  54. +13
    -0
      front/src/reportWebVitals.js
  55. +5
    -0
      front/src/setupTests.js
  56. +16
    -0
      front/src/store.js
  57. +4
    -0
      front/src/style/_base.scss
  58. +26
    -0
      front/src/style/_icon.scss
  59. +5
    -0
      front/src/style/_reset.scss
  60. +34
    -0
      front/src/style/_screen.scss
  61. +17
    -0
      front/src/style/_variable.scss
  62. +8
    -0
      front/src/style/generic/DisplayError.scss
  63. +47
    -0
      front/src/style/generic/Modal.scss
  64. +29
    -0
      front/src/style/task/Task.scss
  65. +7
    -0
      front/src/style/task/TaskCreate.scss
  66. +23
    -0
      front/src/style/task/TaskForm.scss
  67. +19
    -0
      front/src/style/task/TaskList.scss
  68. +429
    -0
      package-lock.json
  69. +17
    -0
      package.json

+ 1
- 0
Licence note View File

@ -0,0 +1 @@
If you find software that doesn’t have a license, that means you have no permission from the creators of the software to use, modify, or share the software. Although a code host such as GitHub may allow you to view and fork the code, this does not imply that you are permitted to use, modify, or share the software for any purpose.

+ 45
- 0
README.md View File

@ -0,0 +1,45 @@
# Kanban
## Description
Proof of concept of a kanban style application.
### Technology stack
+ [MongoDB](https://www.mongodb.com/) for the database with [Mongoose](https://mongoosejs.com/) as the Object Data Manager.
+ [ExpressJS](https://expressjs.com/) as the backend web framework.
+ [React](https://reactjs.org/) for the frontend with [Redux](https://redux.js.org/) to handle the application states.
+ [NodeJS](https://nodejs.org) as the JavaScript runtime.
## Installation
### Using docker
Use the provided `docker-compose.yml` script to launch the application directly.
While being at the root of the application, use:
```bash
docker-compose up
```
### Using npm
If you prefer launching the application directly, you will need to provide a MongoDB instance for the application to connect.
Edit `back/config/keys` with the necessary information.
You can then launch both the back and frontend with ```npm start``` at the root of the application.
## Usage
Navigate to [127.0.0.1:3000](http://127.0.0.1:3000) to see the frontend.
Backend accessible via [127.0.0.1:3001](http://127.0.0.1:3001) (obviously not much to see here)
## Screenshots
**Main screen populated**
![main](assets/main.png)
**Task update screen**
![update](assets/update.png)

BIN
assets/main.png View File

Before After
Width: 1920  |  Height: 1080  |  Size: 58 KiB

BIN
assets/update.png View File

Before After
Width: 1920  |  Height: 1080  |  Size: 57 KiB

+ 115
- 0
back/.gitignore View File

@ -0,0 +1,115 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*

+ 6
- 0
back/config/default.json View File

@ -0,0 +1,6 @@
{
"express":{
"port":3001,
"baseUrl": "/api"
}
}

+ 9
- 0
back/config/keys.js View File

@ -0,0 +1,9 @@
const mongodb_user="test";
const mongodb_password="test";
const mongodb_url="mongo";
const mongodb_database="kanban-mongo"
module.exports = {
//mongoURI: 'mongodb://'+mongodb_user+":"+mongodb_password+"@"+mongodb_url+"/"+mongodb_database
mongoURI: 'mongodb://'+mongodb_url+"/"+mongodb_database
}

+ 6
- 0
back/config/test.json View File

@ -0,0 +1,6 @@
{
"express":{
"port":2999,
"baseUrl": "/api"
}
}

+ 15
- 0
back/controller/MongoController.js View File

@ -0,0 +1,15 @@
const keys=require("../config/keys.js");
const mongoose=require("mongoose");
exports.connectToDatabase=()=>{
return mongoose.connect(
keys.mongoURI,
{
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false
});
}
exports.disconnectFromDatabase=()=>{
return mongoose.disconnect();
}

+ 26
- 0
back/controller/TaskController.js View File

@ -0,0 +1,26 @@
const { findByIdAndUpdate } = require('../model/Task');
const Model=require('../model/Task');
exports.findAllTask=()=>{
return Model
.find()
.sort({name:"asc"})
}
exports.findTaskById=(id)=>{
return Model.findById(id);
}
exports.addTask=(taskData)=>{
let task=new Model(taskData);
let errorValidate=task.validateSync();
if(errorValidate){
throw errorValidate;
}
return task.save();
}
exports.updateTask=(id,update)=>{
return Model.findByIdAndUpdate(id,update,{new:true, runValidators:true});
}
exports.deleteTask=(id)=>{
return Model.findByIdAndDelete(id);
}

+ 24
- 0
back/index.js View File

@ -0,0 +1,24 @@
const express=require("express");
const bodyParser=require("body-parser");
const config=require("config");
const { connectToDatabase } = require("./controller/MongoController.js");
const taskRoute=require("./route/TaskRoute");
const webapp=express();
connectToDatabase()
.then(()=>console.log("Database online"))
.catch(err=>console.error(err));
webapp.use(bodyParser.json());
webapp.use(config.express.baseUrl+"/task",taskRoute);
function startWebServer(){
if(config.util.getEnv('NODE_ENV') !== 'test') {
webapp.listen(
config.express.port,
()=>console.log("Web server started on port "+config.express.port)
);
}
}
startWebServer();
module.exports=webapp;

+ 22
- 0
back/model/Task.js View File

@ -0,0 +1,22 @@
const mongoose=require("mongoose");
const validatorId=require('mongoose-id-validator');
var taskSchema=new mongoose.Schema({
name:{ type: String, required: true },
description:{ type: String, default:"" },
parent:{ type: mongoose.Schema.Types.ObjectId, ref: 'Task' }
});
taskSchema.plugin(validatorId);
taskSchema.pre("findOneAndUpdate",function(next){
if(
this._update
&& this._update.hasOwnProperty("parent")
&& this._update.parent.toString()===this._conditions._id.toString()
){
next(new Error("parent must be different of _id."));
}else{
next();
}
});
module.exports=mongoose.model("Task",taskSchema);

+ 3565
- 0
back/package-lock.json
File diff suppressed because it is too large
View File


+ 30
- 0
back/package.json View File

@ -0,0 +1,30 @@
{
"name": "back",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "nyc mocha --recursive test/controller && nyc mocha --recursive test/route"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"config": "^3.3.6",
"express": "^4.17.1",
"express-rate-limit": "^5.2.6",
"express-slow-down": "^1.4.0",
"mongoose": "5.11.15",
"mongoose-id-validator": "^0.6.0"
},
"devDependencies": {
"chai": "^4.3.3",
"chai-http": "^4.3.0",
"mocha": "^8.3.1",
"nodemon": "^2.0.7",
"nyc": "^15.1.0"
}
}

+ 75
- 0
back/route/TaskRoute.js View File

@ -0,0 +1,75 @@
const express=require("express");
const { findAllTask, findTaskById, addTask, updateTask, deleteTask } = require("../controller/TaskController");
const router=express.Router();
const Model=require('../model/Task');
router.get("/", (req, res) =>{
findAllTask().then((value)=>{
res.status(200).send(value);
}).catch((reason)=>{
res.status(500).send(reason);
});
});
router.get("/:id", (req, res) =>{
findTaskById(req.params.id).then((value)=>{
res.status(200).send(value);
}).catch((reason)=>{
res.status(500).send(reason);
});
});
router.post("/", (req, res) =>{
//Delete parent if set to ""
if(req.body.parent===""){
delete req.body.parent;
}
try{
addTask(req.body).then((value)=>{
res.status(200).send(value);
}).catch((reason)=>{
res.status(500).send(reason);
});
}catch(error){
res.status(500).send(error);
}
});
router.put("/:id",(req,res)=>{
//Delete parent if set to ""
if(req.body.parent===""){
delete req.body.parent;
req.body=Object.assign(req.body, {$unset:{parent:1}});
}
updateTask(req.params.id,req.body)
.then((value)=>
{
if(value===null){
res.status(500).send("No changes, invalid id ?");
}else{
res.status(200).send(value);
}
},(reason)=>{
res.status(500).send(reason.toString());
})
.catch((reason)=>{
res.status(500).send(reason);
});
});
router.delete("/:id", (req, res) =>{
deleteTask(req.params.id).then((value)=>{
if(value===null){
res.status(500).send("No changes, invalid id ?");
}else{
res.status(200).send(value);
}
}).catch((reason)=>{
res.status(500).send(reason);
});
});
module.exports=router;

+ 171
- 0
back/test/controller/TaskController.test.js View File

@ -0,0 +1,171 @@
process.env.NODE_ENV = 'test';
const { describe }=require("mocha");
const { assert, expect } = require("chai");
const { connectToDatabase, disconnectFromDatabase } = require("../../controller/MongoController");
const { findAllTask,findTaskById,addTask, updateTask, deleteTask } = require("../../controller/TaskController");
const Task = require("../../model/Task");
describe("TaskController",()=>{
before(()=>{
connectToDatabase()
.then(()=>{})
.catch(err=>console.error(err));
});
beforeEach((done) => {
Task.deleteMany({}, (err) => {
if(err){
console.error(err);
}else{
done();
}
});
});
afterEach((done) => {
Task.deleteMany({}, (err) => {
if(err){
console.error(err);
}else{
done();
}
});
});
after(()=>{
disconnectFromDatabase();
});
describe("findAllTask()",(done)=>{
it("it should return empty object",(done)=>{
findAllTask().then((value)=>{
assert.isTrue(Object.keys(value).length===0);
done();
}).catch((reason)=>{assert.fail(reason)});
});
it("it should return objects",(done)=>{
let taskOne=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"});
let taskTwo=new Task({name:"Two", _id:"6e99ac68604ea9031e8545c8"});
taskOne.save((errOne,docOne)=>{
taskTwo.save((errTwo,docTwo)=>{
findAllTask().then((value)=>{
assert.isTrue(Object.keys(value).length===2);
assert.containsAllKeys(value[0],taskOne);
assert.containsAllKeys(value[1],taskTwo);
done();
}).catch((reason)=>{assert.fail(reason)});
});
});
});
});
describe("findTaskById()",(done)=>{
it("it should not return non-existing id",(done)=>{
findTaskById(0)
.then((item)=>{assert.fail("got "+item)})
.catch(done());
});
it("it should return valid id",(done)=>{
let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"});
task.save((err,doc)=>{
findTaskById(task._id)
.then((value)=>{
assert.equal(value._id.toString(),task._id.toString());
assert.equal(value.name,task.name);
assert.equal(value.description,task.description);
done();
})
.catch((reason)=>{assert.fail(reason);});
});
});
});
describe("addTask()",(done)=>{
it("it should not accept empty object",(done)=>{
let data={};
expect(()=>{addTask(data)}).to.throw();
done();
});
it("it should accept object with name",(done)=>{
let data={"name":"random"};
addTask(data)
.then((value)=>{
assert.isObject(value);
assert.equal(value.name,data.name);
existingTask=value;
done();
})
.catch((reason)=>{assert.fail(reason);});
});
it("it should have empty description if none provided",(done)=>{
let data={"name":"random"};
addTask(data)
.then((value)=>{
assert.isObject(value);
assert.equal(value.description,"");
done();
})
.catch((reason)=>{assert.fail(reason);});
});
});
describe("updateTask()",(done)=>{
it("it should not update non-existing id",(done)=>{
expect(()=>{updateTask(0,{})}).not.to.throw();
updateTask(0,{})
.then((value)=>{assert.fail(value);})
.catch((reason)=>{done();})
});
it("it should not accept parent as itself",(done)=>{
let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"});
task.save((err,doc)=>{
let updateParent={"parent":task._id};
updateTask(task._id,updateParent)
.then((value)=>{
assert.fail(value);
},(reason)=>{
assert.isNotEmpty(reason.toString());
done();
})
.catch((reason)=>{assert.fail(reason)});
});
});
it("it should update properties without touching others",(done)=>{
let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"});
task.save((err,doc)=>{
let newName={"name":"fooIsMyNewName"};
let newDescription={"description":"barIsMyNewDescription"};
updateTask(task._id,newName).then((value)=>{
assert.equal(value.name,newName.name);
assert.equal(value.description,task.description);
updateTask(task._id,newDescription).then((valueD)=>{
assert.equal(valueD.name,newName.name);
assert.equal(valueD.description,newDescription.description);
existingTask=valueD;
done();
}).catch((reasonD)=>{assert.fail(reasonD)});
}).catch((reason)=>{assert.fail(reason)});
});
});
});
describe("deleteTask()",(done)=>{
it("it should not delete non-existing id",(done)=>{
deleteTask(0)
.then((value)=>{assert.fail(value)})
.catch((reason)=>{done()})
});
it("it should delete existing id",(done)=>{
let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"});
task.save((err,doc)=>{
deleteTask(task._id)
.then((value)=>{
assert.isObject(value);
assert.equal(value._id.toString(),task._id.toString());
done();
})
.catch((reason)=>{console.error(reason)})
});
});
});
});

+ 385
- 0
back/test/route/TaskRoute.test.js View File

@ -0,0 +1,385 @@
process.env.NODE_ENV = 'test';
const { assert } = require('chai');
const chai=require('chai');
const chaiHttp=require('chai-http');
const { disconnectFromDatabase } = require('../../controller/MongoController');
const server=require("../../index");
const should = chai.should();
chai.use(chaiHttp);
const Task=require("../../model/Task");
const config=require("config");
let baseURL=config.express.baseUrl+"/task";
describe('Task', () => {
beforeEach((done) => {
Task.deleteMany({}, (err) => {
if(err){
console.error(err);
}else{
done();
}
});
});
afterEach((done) => {
Task.deleteMany({}, (err) => {
if(err){
console.error(err);
}else{
done();
}
});
});
after(()=>{
disconnectFromDatabase();
})
describe("/ GET", () => {
it("Should get empty", (done)=>{
chai.request(server)
.get(baseURL)
.end((err,res)=>{
res.should.have.status(200);
res.body.should.not.have.property('errors');
res.body.should.be.a('array');
res.body.length.should.be.eql(0);
done();
});
});
});
describe("/:id GET", () => {
it("Should return empty object if id not found", (done)=>{
chai.request(server)
.get(baseURL+"/5e99ac68604ea9031e8545c8")
.end((err,res)=>{
res.should.have.status(200);
assert.isObject(res.body);
assert.isEmpty(res.body);
done();
});
});
it("Should return existing object", (done)=>{
let task=new Task({name:"Parent", _id:"5e99ac68604ea9031e8545c8"});
task.save((err,parent)=> {
chai.request(server)
.get(baseURL+"/5e99ac68604ea9031e8545c8")
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(task.description);
res.body.should.have.property('_id');
res.body._id.should.be.not.empty;
res.body._id.should.equal(task.id.toString());
done();
});
});
});
});
describe("/ POST", () => {
it("Should not accept empty name", (done)=>{
let task={
}
chai.request(server)
.post(baseURL)
.send(task)
.end((err,res)=>{
res.should.have.status(500);
res.body.should.be.a('object');
res.body.should.have.property('errors');
res.body.errors.should.have.property('name');
res.body.errors.name.should.have.property('name').eql('ValidatorError');
res.body.errors.name.should.have.property('path').eql('name');
res.body.errors.name.should.have.property('kind').eql('required');
done();
});
});
it("Should add non empty task", (done)=>{
let task={
name: "Non empty name",
description: "description"
}
chai.request(server)
.post(baseURL+"/")
.send(task)
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(task.description);
res.body.should.have.property('_id');
res.body._id.should.be.not.empty;
done();
});
});
it("Should not create task with invalid parent id", (done)=>{
let task={
name: "TaskName",
description: "TaskDescription",
parent: "5e99ac36a60d08030772df30"
}
chai.request(server)
.post(baseURL+"/")
.send(task)
.end((err,res)=>{
res.should.have.status(500);
res.body.should.be.a('object');
res.body.should.have.property('errors');
res.body.errors.should.have.property('parent');
res.body.errors.parent.should.have.property('name').eql('ValidatorError');
res.body.errors.parent.should.have.property('path').eql('parent');
res.body.errors.parent.should.have.property('kind').eql('user defined');
done();
});
});
it("Should create task with valid parent id", (done)=>{
let task={
name: "TaskName",
description: "description",
parent: "5e99acf261eea30341340560"
}
let parentTask=new Task({name:"Parent", _id:"5e99acf261eea30341340560"});
parentTask.save((err,parent)=> {
chai.request(server)
.post(baseURL+"/")
.send(task)
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(task.description);
res.body.should.have.property('_id');
res.body._id.should.be.not.empty;
res.body.should.have.property("parent");
res.body.parent.should.equal(task.parent);
done();
});
});
});
it("Should create task with no parent if parent=\"\"", (done)=>{
let task={
name: "TaskName",
description: "description",
parent: ""
}
chai.request(server)
.post(baseURL+"/")
.send(task)
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(task.description);
res.body.should.have.property('_id');
res.body._id.should.be.not.empty;
res.body.should.not.have.property("parent");
done();
});
});
});
describe("/:id PUT", () => {
it("Should not change invalid id", (done)=>{
chai.request(server)
.put(baseURL+"/5e99ac36a60d08030772df30")
.send({description:"New description"})
.end((err,res)=>{
res.should.have.status(500);
assert.isNotEmpty(res.error.text);
done();
});
});
it("Should change Task", (done)=>{
let task=new Task({
name: "TaskName",
description: "description",
_id: "5e99acf261eea30341340560"
});
let updateValue={"description":"New description"}
task.save((err,doc)=> {
chai.request(server)
.put(baseURL+"/"+task._id)
.send(updateValue)
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(updateValue.description);
res.body.should.have.property('_id');
res.body._id.should.equal(task.id.toString());
done();
});
});
});
it("Should not change name empty", (done)=>{
let task=new Task({
name: "TaskName",
description: "description",
_id: "5e99acf261eea30341340560"
});
task.save((err,doc)=> {
chai.request(server)
.put(baseURL+"/"+task._id)
.send({title:""})
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(task.description);
res.body.should.have.property('_id');
res.body._id.should.equal(task.id.toString());
done();
});
});
});
it("Should not change parent to invalid", (done)=>{
let task=new Task({
name: "TaskName",
description: "description",
_id: "5e99acf261eea30341340560"
});
task.save((err,doc)=> {
chai.request(server)
.put(baseURL+"/"+task._id)
.send({parent:"56"})
.end((err,res)=>{
res.should.have.status(500);
res.body.should.be.a('object');
Task.findById(task._id).then((value)=>{
value.parent.should.be(task.parent);
}).catch((reason)=>{assert.fail(reason)});
done();
});
});
});
it("Should not change parent to itself", (done)=>{
let task=new Task({
name: "TaskName",
description: "description",
_id: "5e99acf261eea30341340560"
});
task.save((err,doc)=> {
chai.request(server)
.put(baseURL+"/"+task._id)
.send({"parent":task._id})
.end((err,res)=>{
res.should.have.status(500);
res.body.should.be.a('object');
Task.findById(task._id).then((value)=>{
value.parent.should.be(task.parent);
}).catch((reason)=>{assert.fail(reason)});
done();
});
});
});
it("Should not change non specified property", (done)=>{
let task=new Task({
name: "TaskName",
description: "description",
parent: "5e99ac36a60d08030772df30",
_id: "5e99acf261eea30341340560"
});
let parentTask=new Task({name:"Parent", _id:"5e99ac36a60d08030772df30"});
parentTask.save((perr,pdoc)=>{
task.save((err,doc)=> {
chai.request(server)
.put(baseURL+"/"+task._id)
.send({})
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(task.name);
res.body.should.have.property('description');
res.body.description.should.equal(task.description);
res.body.should.have.property('parent');
res.body.parent.should.equal(task.parent.toString());
res.body.should.have.property('_id');
res.body._id.should.equal(task.id.toString());
done();
});
});
});
});
it("Should remove parent on empty string", (done)=>{
let childTask=new Task({
name: "TaskName",
description: "description",
parent: "5e99ac36a60d08030772df30",
_id: "5e99acf261eea30341340560"
});
let parentTask=new Task({name:"Parent", _id:"5e99ac36a60d08030772df30"});
parentTask.save((perr,pdoc)=>{
childTask.save((err,doc)=> {
chai.request(server)
.put(baseURL+"/"+childTask._id)
.send({parent:""})
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.not.have.property('errors');
res.body.should.have.property('name');
res.body.name.should.equal(childTask.name);
res.body.should.have.property('description');
res.body.description.should.equal(childTask.description);
res.body.should.not.have.property('parent');
res.body.should.have.property('_id');
res.body._id.should.equal(childTask.id.toString());
done();
});
});
});
});
});
describe("/:id DELETE", () => {
it("Should not delete invalid id", (done)=>{
chai.request(server)
.delete(baseURL+"/5e99ac36a60d08030772df99")
.end((err,res)=>{
res.should.have.status(500);
assert.isNotEmpty(res.error.text);
done();
});
});
it("Should delete", (done)=>{
let task=new Task({
name: "TaskName",
description: "description",
_id: "5e99ac36a60d08030772df99"
});
task.save((err,doc)=> {
chai.request(server)
.delete(baseURL+"/"+task._id)
.end((err,res)=>{
res.should.have.status(200);
res.body.should.be.an('object');
res.body._id.should.equal(task._id.toString());
done();
});
});
});
});
});

+ 30
- 0
docker-compose.yml View File

@ -0,0 +1,30 @@
version: "3"
networks:
kanban:
external: false
services:
node:
container_name: kanban_front
image: node:13
networks:
- kanban
depends_on:
- mongo
volumes:
- .:/usr/src/app
# - /etc/localtime:/etc/localtime:ro
ports:
- '3000:3000'
- '3001:3001'
working_dir: "/usr/src/app/"
command: bash -c "npm install --prefix back/ && npm install --prefix front/ && npm install && npm start"
mongo:
container_name: kanban_back
image: mongo:4.2
networks:
- kanban
#volumes:
# - ./mongo:/data/db
# - /etc/localtime:/etc/localtime:ro
#ports:
# - '27017:27017'

+ 23
- 0
front/.gitignore View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

+ 17413
- 0
front/package-lock.json
File diff suppressed because it is too large
View File


+ 45
- 0
front/package.json View File

@ -0,0 +1,45 @@
{
"name": "front",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.21.1",
"node-sass": "4.14.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"web-vitals": "^1.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:3001"
}

BIN
front/public/favicon.ico View File

Before After

+ 20
- 0
front/public/index.html View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Kanban web app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Kanban</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

BIN
front/public/logo192.png View File

Before After
Width: 192  |  Height: 192  |  Size: 5.2 KiB

BIN
front/public/logo512.png View File

Before After
Width: 512  |  Height: 512  |  Size: 9.4 KiB

+ 25
- 0
front/public/manifest.json View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

+ 3
- 0
front/public/robots.txt View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

+ 19
- 0
front/src/App.js View File

@ -0,0 +1,19 @@
import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './store';
import TaskIndex from './component/task/TaskIndex';
function App() {
return (
<Provider store={store}>
<Router>
<Route exact path="/" render={ props =>(
<TaskIndex/>
) } />
</Router>
</Provider>
);
}
export default App;

+ 81
- 0
front/src/action/TaskAction.js View File

@ -0,0 +1,81 @@
import axios from 'axios';
import store from '../store';
import { ADD, GET, GET_ALL, UPDATE, DELETE, INSPECT } from './TaskType';
const base_url='/api/task';
export const getTasks= (error=(reason)=>{console.error(reason)}) => dispatch => {
axios
.get(base_url)
.then(
(value)=>
dispatch({
type: GET_ALL,
payload: value.data
})
,(reason)=>{
if(error&&typeof(error)==="function") error(reason)
}
);
}
export const getTask= (id,error=(reason)=>{console.error(reason)}) => dispatch => {
axios
.get(base_url+`/${id}`)
.then(
(value)=>
dispatch({
type: GET,
payload: value.data
})
,(reason)=>{
if(error&&typeof(error)==="function") error(reason)
}
);
}
export const addTask= (task,error=(reason)=>{console.error(reason)}) => dispatch => {
axios
.post(base_url, task)
.then(
(value)=>
dispatch({
type: ADD,
payload: value.data
})
,(reason)=>{
if(error&&typeof(error)==="function") error(reason)
}
);
}
export const deleteTask= (id,error=(reason)=>{console.error(reason)}) => dispatch => {
axios
.delete(base_url+`/${id}`)
.then(
(value)=>
dispatch({
type: DELETE,
payload: id
})
,(reason)=>{
if(error&&typeof(error)==="function") error(reason)
}
);
}
export const updateTask= (task,error=(reason)=>{console.error(reason)}) => dispatch => {
axios
.put(base_url+`/${task._id}`, task)
.then(
(value)=>
dispatch({
type: UPDATE,
payload: task
})
,(reason)=>{
if(error&&typeof(error)==="function") error(reason)
}
);
}
export const inspectTask= (id) => {
return store.dispatch ({
type: INSPECT,
payload: id
});
}

+ 7
- 0
front/src/action/TaskType.js View File

@ -0,0 +1,7 @@
export const ADD='CREATE';
export const GET='READ';
export const GET_ALL='READ_ALL';
export const UPDATE='UPDATE';
export const DELETE='DELETE';
export const INSPECT='INSPECT';

+ 21
- 0
front/src/component/generic/DisplayError.js View File

@ -0,0 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import "../../style/generic/DisplayError.scss";
function DisplayError(props) {
let message="";
if(props.hasOwnProperty("axios")){
message=<div className="errorTitle">Status {props.axios.response.status} ({props.axios.response.statusText})</div>;
if(typeof(props.axios.response.data)==="object" && props.axios.response.data.hasOwnProperty("message")){
message=<React.Fragment>{message}<div className="errorMessage">{props.axios.response.data.message}</div></React.Fragment>;
}
}
return (
<div className="displayError">{message}</div>
);
}
DisplayError.propTypes = {
axios: PropTypes.object
};
export default DisplayError;

+ 49
- 0
front/src/component/generic/Modal.js View File

@ -0,0 +1,49 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import '../../style/generic/Modal.scss';
export default class Modal extends Component {
static propTypes = {
show: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired
}
componentDidMount(){
if(this.props.show){
this.hideBodyOverflow();
}
}
componentDidUpdate(prevProps, prevState, snapshot){
if(this.props.show){
this.hideBodyOverflow();
}else{
this.resetBodyOverflow();
}
}
componentWillUnmount(){
this.resetBodyOverflow();
}
hideBodyOverflow(){
document.body.style.overflow="hidden";
}
resetBodyOverflow(){
document.body.style.overflow="auto";
}
render() {
return (
<div className="modal" style={{display: this.props.show ? "grid":"none"}}>
<div className="modalForeground">
<div className="modalTitle">{this.props.title}</div>
<div className="modalClose icon-cancel-circled" onClick={this.props.onClose}></div>
<div className="modalContent">{ this.props.children }</div>
</div>
</div>
)
}
}

+ 22
- 0
front/src/component/task/Task.js View File

@ -0,0 +1,22 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { inspectTask } from "../../action/TaskAction";
import "../../style/task/Task.scss";
function Task(props) {
return (
<div id={props.task._id} className="task">
<button className="edit icon-pencil-circled" type="button" onClick={()=>{inspectTask(props.task._id);}} title="Edit"></button>
<div className="id">{props.task._id}</div>
<div className="name">{props.task.name}</div>
<div className="description">{props.task.description}</div>
</div>
);
}
Task.propTypes = {
inspectTask: PropTypes.func.isRequired,
task: PropTypes.object.isRequired
};
export default connect(null,{inspectTask})(Task);

+ 22
- 0
front/src/component/task/TaskCreate.js View File

@ -0,0 +1,22 @@
import React, { Component } from 'react'
import TaskForm from './TaskForm';
import '../../style/task/TaskCreate.scss';
export default class TaskCreate extends Component {
state={
"open":false
}
render() {
return (
<div className="taskCreate">
<button
className={this.state.open?"icon-minus-circled":"icon-plus-circled"}
type="button"
onClick={()=>{this.setState({"open":!this.state.open});}}
title={this.state.open?"Hide add task":"Add task"}
/>
{this.state.open?<TaskForm name="create"/>:""}
</div>
)
}
}

+ 137
- 0
front/src/component/task/TaskForm.js View File

@ -0,0 +1,137 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { addTask, updateTask, deleteTask } from '../../action/TaskAction';
import '../../style/task/TaskForm.scss';
import DisplayError from '../generic/DisplayError';
class TaskForm extends Component {
static propTypes = {
task: PropTypes.object,
addTask: PropTypes.func.isRequired,
updateTask: PropTypes.func.isRequired,
deleteTask: PropTypes.func.isRequired,
}
state={
submitValue:"Create",
isUpdate:false,
task:{
name: "",
description: "",
parent: ""
},
backendError:false
}
componentDidMount(){
if(this.props.task){
this.setState({
"isUpdate":true,
"submitValue":"Update",
"task": {...this.state.task,...this.props.task}
});
}
this.onBackendError=this.onBackendError.bind(this);
}
componentDidUpdate(prevProps, prevState, snapshot){
if(this.props.task && this.props.task!==prevProps.task){
this.setState({
"isUpdate":true,
"submitValue":"Update",
"task": {...this.state.task,...this.props.task}
});
}
}
onChange=(e)=>{
e.preventDefault();
let n=e.target.name;
let v=e.target.value;
this.setState(prevState=>({
task: {
...prevState.task,
[n]:v
}
}));
}
onSubmit=(e)=>{
e.preventDefault();
if(this.state.isUpdate){
this.props.updateTask(this.state.task,this.onBackendError);
}else{
this.props.addTask(this.state.task,this.onBackendError);
}
}
onBackendError(reason){
this.setState({backendError:reason});
}
onDelete=(e)=>{
e.preventDefault();
this.props.deleteTask(this.state.task._id);
}
renderDeleteButton(){
if(!this.state.isUpdate) return;
return(
<button className="taskFormDelete" onClick={this.onDelete}>Delete</button>
);
}
renderFormElement(id,element,labelText=""){
if(labelText==="") labelText=id;
return(
<React.Fragment>
<label htmlFor={id}>{labelText}</label>
{element}
</React.Fragment>
)
}
render() {
let formName="";
if(this.props.hasOwnProperty("name")) formName=this.props.name;
return (
<div className="taskForm">
<form onSubmit={this.onSubmit}>
{this.renderFormElement(
"name"+formName,
<input id={"name"+formName} type="text" name="name" placeholder="name" value={this.state.task.name} onChange={this.onChange}/>,
"Name"
)}
{this.renderFormElement(
"description"+formName,
<textarea id={"description"+formName} type="text" name="description" placeholder="description" value={this.state.task.description} onChange={this.onChange}/>,
"Description"
)}
{this.renderFormElement(
"parent"+formName,
<select id={"parent"+formName} name="parent" value={this.state.task.parent} onChange={this.onChange}>
<option value="">No parent task</option>
{this.props.tasks.map(task=>{
return this.state.isUpdate && task._id === this.state.task._id ? "":<option key={task._id} value={task._id}>{task.name}</option>
})}
</select>,
"Parent task"
)}
<input type="hidden" name="isUpdate" value={this.state.isUpdate}/>
<input type="submit" value={this.state.submitValue}/>
</form>
<div>{this.state.backendError?<DisplayError axios={this.state.backendError}/>:""}</div>
{this.renderDeleteButton()}
</div>
)
}
}
const mapStateToProps = (state) => ({
tasks: state.task.tasks
});
export default connect(mapStateToProps, {addTask, updateTask, deleteTask})(TaskForm);

+ 35
- 0
front/src/component/task/TaskIndex.js View File

@ -0,0 +1,35 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { inspectTask } from '../../action/TaskAction';
import TaskList from './TaskList';
import TaskForm from './TaskForm';
import Modal from '../generic/Modal';
import TaskCreate from './TaskCreate';
class TaskIndex extends Component {
static propTypes = {
inspectTask: PropTypes.func.isRequired
}
closeInspected=()=>{
this.props.inspectTask("");
}
render() {
let taskInspectedShow=this.props.taskInspected ? true:false;
return (
<div>
<TaskCreate/>
<TaskList/>
<Modal show={taskInspectedShow} title="Update" onClose={this.closeInspected}>
<TaskForm task={this.props.taskInspected} name="inspected"/>
</Modal>
</div>
)
}
}
const mapStateToProps = (state) => ({
taskInspected: state.task.tasks.filter(t=>t._id===state.task.taskInspected)[0]
});
export default connect(mapStateToProps, {inspectTask})(TaskIndex);

+ 52
- 0
front/src/component/task/TaskList.js View File

@ -0,0 +1,52 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getTasks } from '../../action/TaskAction';
import Task from './Task';
import DisplayError from '../generic/DisplayError';
import '../../style/task/TaskList.scss';
class TaskList extends Component {
static propTypes = {
getTasks: PropTypes.func.isRequired,
task: PropTypes.object.isRequired
}
state={
error:false
}
componentDidMount(){
this.props.getTasks((reason)=>{this.setState({"error":reason})});
}
renderTaskGroup(parent,generation=0){
let taskCollection;
if(generation===0){
taskCollection=this.props.sortedTask.filter(task=>!task.parent)
}else{
taskCollection=this.props.sortedTask.filter(task=>task.parent===parent._id);
}
return(
taskCollection.map(t=>{
return(
<div key={t._id} className={"taskGroup taskGroup_"+(generation)}>
<Task task={t} />
{this.renderTaskGroup(t,generation+1)}
</div>
)
})
)
}
render(){
return (
<React.Fragment>
{(this.state && this.state.error)?<DisplayError axios={this.state.error}/>:""}
<div className="taskList">{ this.renderTaskGroup() }</div>
</React.Fragment>
);
}
}
const mapStateToProps = (state) => ({
task: state.task,
sortedTask: state.task.tasks.sort((a,b)=>(a.name).localeCompare(b.name))
});
export default connect(mapStateToProps, {getTasks})(TaskList);

+ 34
- 0
front/src/font/fontello/config.json View File

@ -0,0 +1,34 @@
{
"name": "",
"css_prefix_text": "icon-",
"css_use_suffix": false,
"hinting": true,
"units_per_em": 1000,
"ascent": 850,
"glyphs": [
{
"uid": "4ba33d2607902cf690dd45df09774cb0",
"css": "plus-circled",
"code": 59392,
"src": "fontawesome"
},
{
"uid": "0f4cae16f34ae243a6144c18a003f2d8",
"css": "cancel-circled",
"code": 59393,
"src": "fontawesome"
},
{
"uid": "19dae18c34431934a781773e241faec2",
"css": "pencil-circled",
"code": 59395,
"src": "elusive"
},
{
"uid": "eeadb020bb75d089b25d8424aabe19e0",
"css": "minus-circled",
"code": 59394,
"src": "fontawesome"
}
]
}

+ 85
- 0
front/src/font/fontello/css/animation.css View File

@ -0,0 +1,85 @@
/*
Animation example, for spinners
*/
.animate-spin {
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
display: inline-block;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

+ 5
- 0
front/src/font/fontello/css/fontello-codes.css View File

@ -0,0 +1,5 @@
.icon-plus-circled:before { content: '\e800'; } /* '' */
.icon-cancel-circled:before { content: '\e801'; } /* '' */
.icon-minus-circled:before { content: '\e802'; } /* '' */
.icon-pencil-circled:before { content: '\e803'; } /* '' */

+ 62
- 0
front/src/font/fontello/css/fontello-embedded.css
File diff suppressed because it is too large
View File


+ 5
- 0
front/src/font/fontello/css/fontello-ie7-codes.css View File

@ -0,0 +1,5 @@
.icon-plus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-minus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-pencil-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }

+ 16
- 0
front/src/font/fontello/css/fontello-ie7.css View File

@ -0,0 +1,16 @@
[class^="icon-"], [class*=" icon-"] {
font-family: 'fontello';
font-style: normal;
font-weight: normal;
/* fix buttons height */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
}
.icon-plus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-minus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-pencil-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }

+ 61
- 0
front/src/font/fontello/css/fontello.css View File

@ -0,0 +1,61 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?10524901');
src: url('../font/fontello.eot?10524901#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?10524901') format('woff2'),
url('../font/fontello.woff?10524901') format('woff'),
url('../font/fontello.ttf?10524901') format('truetype'),
url('../font/fontello.svg?10524901#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?10524901#fontello') format('svg');
}
}
*/
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: never;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-plus-circled:before { content: '\e800'; } /* '' */
.icon-cancel-circled:before { content: '\e801'; } /* '' */
.icon-minus-circled:before { content: '\e802'; } /* '' */
.icon-pencil-circled:before { content: '\e803'; } /* '' */

BIN
front/src/font/fontello/font/fontello.eot View File


+ 18
- 0
front/src/font/fontello/font/fontello.svg View File

@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2021 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="plus-circled" unicode="&#xe800;" d="M679 314v72q0 14-11 25t-25 10h-143v143q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-143h-143q-14 0-25-10t-10-25v-72q0-14 10-25t25-10h143v-143q0-15 11-25t25-11h71q15 0 25 11t11 25v143h143q14 0 25 10t11 25z m178 36q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="cancel-circled" unicode="&#xe801;" d="M641 224q0 14-10 25l-101 101 101 101q10 11 10 25 0 15-10 26l-51 50q-10 11-25 11-15 0-25-11l-101-101-101 101q-11 11-25 11-16 0-26-11l-50-50q-11-11-11-26 0-14 11-25l101-101-101-101q-11-11-11-25 0-15 11-26l50-50q10-11 26-11 14 0 25 11l101 101 101-101q10-11 25-11 15 0 25 11l51 50q10 11 10 26z m216 126q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="minus-circled" unicode="&#xe802;" d="M679 314v72q0 14-11 25t-25 10h-429q-14 0-25-10t-10-25v-72q0-14 10-25t25-10h429q14 0 25 10t11 25z m178 36q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="pencil-circled" unicode="&#xe803;" d="M0 350q0 207 147 354t353 146 354-146 146-354-146-354-354-146-353 146-147 354z m174-287l219 43-176 176z m86 250l168-166 269 271-166 166z m342 316q-2-25 15-41l84-84q18-17 43-16t44 19 20 44-17 43l-84 84q-16 16-39 16-27 0-47-20-17-19-19-45z" horiz-adv-x="1000" />
</font>
</defs>
</svg>

BIN
front/src/font/fontello/font/fontello.ttf View File


BIN
front/src/font/fontello/font/fontello.woff View File


BIN
front/src/font/fontello/font/fontello.woff2 View File


+ 13
- 0
front/src/index.css View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

+ 17
- 0
front/src/index.js View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

+ 52
- 0
front/src/reducer/TaskReducer.js View File

@ -0,0 +1,52 @@
import { ADD, GET, GET_ALL, UPDATE, DELETE, INSPECT } from '../action/TaskType';
const initialState={
tasks:[],
taskInspected: ""
}
export default function taskReducer(state=initialState, action){
let response;
switch (action.type) {
case GET_ALL:
response={
...state,
tasks: action.payload
};
break;
case GET:
response={
...state,
task: action.payload
};
break;
case ADD:
response={
...state,
tasks: [action.payload, ...state.tasks]
};
break;
case UPDATE:
response={
...state,
tasks: [action.payload, ...state.tasks.filter(task=>task._id !== action.payload._id)]
};
break;
case DELETE:
response={
...state,
tasks: state.tasks.filter(task=>task._id !== action.payload)
};
break;
case INSPECT:
response={
...state,
taskInspected: action.payload
}
break;
default:
response=state;
break;
}
return response;
}

+ 6
- 0
front/src/reducer/index.js View File

@ -0,0 +1,6 @@
import {combineReducers} from 'redux';
import taskReducer from './TaskReducer';
export default combineReducers({
task: taskReducer
});

+ 13
- 0
front/src/reportWebVitals.js View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

+ 5
- 0
front/src/setupTests.js View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

+ 16
- 0
front/src/store.js View File

@ -0,0 +1,16 @@
import {createStore,applyMiddleware,compose} from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducer';
const initialState={};
const middleware=[thunk];
const store=createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() :f=>f
)
);
export default store;

+ 4
- 0
front/src/style/_base.scss View File

@ -0,0 +1,4 @@
@import '_reset.scss';
@import '_screen.scss';
@import '_icon.scss';
@import '_variable.scss';

+ 26
- 0
front/src/style/_icon.scss View File

@ -0,0 +1,26 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello/font/fontello.eot?4356932');
src: url('../font/fontello/font/fontello.eot?4356932#iefix') format('embedded-opentype'),
url('../font/fontello/font/fontello.woff2?4356932') format('woff2'),
url('../font/fontello/font/fontello.woff?4356932') format('woff'),
url('../font/fontello/font/fontello.ttf?4356932') format('truetype'),
url('../font/fontello/font/fontello.svg?4356932#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@import "../font/fontello/css/fontello";
.icon{
font-family: 'fontello';
font-size: 28px;
}
.interactiveIcon{
@extend .icon;
cursor: pointer;
}
[class^="icon-"]:before, [class*=" icon-"]:before{
@extend .interactiveIcon;
}

+ 5
- 0
front/src/style/_reset.scss View File

@ -0,0 +1,5 @@
@import-normalize;
* {
box-sizing : border-box;
}

+ 34
- 0
front/src/style/_screen.scss View File

@ -0,0 +1,34 @@
@mixin media-min-width($width){
@media (min-width: $width){
@content;
}
}
@mixin small-screen() {
@include media-min-width(576px){
@content;
}
}
@mixin medium-screen() {
@include media-min-width(768px){
@content;
}
}
@mixin large-screen() {
@include media-min-width(992px){
@content;
}
}
@mixin xlarge-screen() {
@include media-min-width(1200px){
@content;
}
}
$smallScreenButton:(
small:42px,
medium:60px,
large:72px,
smallSpacing:36px,
mediumSpacing:24px,
largeSpacing:12px
);

+ 17
- 0
front/src/style/_variable.scss View File

@ -0,0 +1,17 @@
$color-task: #BFBFBF;
$colors: (
error: #FF0000,
warning: #FF0000,
modalBackground: rgba(64,64,64,0.7),
modalForeground: $color-task
);
.normalizeButton{
background: unset;
color: inherit;
border: unset;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}

+ 8
- 0
front/src/style/generic/DisplayError.scss View File

@ -0,0 +1,8 @@
@import "../base";
.displayError{
color: map-get($colors,"error");
.errorTitle{
font-weight: bold;
}
}

+ 47
- 0
front/src/style/generic/Modal.scss View File

@ -0,0 +1,47 @@
@import '../_base.scss';
.modal{
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
background-color: map-get($colors,modalBackground);
.modalForeground{
align-self: center;
justify-self: center;
display: grid;
padding: map-get($smallScreenButton,"largeSpacing");
max-width: calc( 100% - #{map-get($smallScreenButton,"smallSpacing")} );
max-height: calc( 100% - #{map-get($smallScreenButton,"smallSpacing")} );
margin: auto;
overflow: hidden;
background-color: map-get($colors,modalForeground);
}
.modalTitle{
grid-row: 1;
grid-column: 1;
align-self: center;
}
.modalClose{
@extend .interactiveIcon;
grid-row: 1;
grid-column: 2;
justify-self: right;
}
.modalContent{
grid-row: 2;
grid-column: 1/3;
overflow: auto;
max-height: calc( 100vh - 150px );
}
}

+ 29
- 0
front/src/style/task/Task.scss View File

@ -0,0 +1,29 @@
@import '../base';
.task{
padding: map-get($smallScreenButton,"largeSpacing");
display: grid;
grid-template-areas:
"name edit"
"id id"
"description description";
.id{
grid-area: id;
font-style: italic;
color: grey;
}
.name{
grid-area: name;
font-weight: bold;
}
.description{
grid-area: description;
}
.edit{
@extend .normalizeButton;
grid-area: edit;
justify-self: right;
}
}

+ 7
- 0
front/src/style/task/TaskCreate.scss View File

@ -0,0 +1,7 @@
@import "../base";
.taskCreate{
button{
@extend .normalizeButton;
}
}

+ 23
- 0
front/src/style/task/TaskForm.scss View File

@ -0,0 +1,23 @@
@import '../base';
.taskForm{
form{
display: grid;
grid-template-columns: repeat(1,1fr);
label,input[type=submit]{
margin-top: map-get($smallScreenButton,"largeSpacing");
}
@include medium-screen{
min-width: 500px;
}
}
.taskFormDelete,input[type=submit]{
cursor: pointer;
}
.taskFormDelete {
margin-top: map-get($smallScreenButton, "smallSpacing");
width: 100%;
color: map-get($colors,"warning");
font-weight: bold;
}
}

+ 19
- 0
front/src/style/task/TaskList.scss View File

@ -0,0 +1,19 @@
@import '../base';
.taskList{
display: grid;
}
.taskGroup{
&:not(.taskGroup_0) &:not(.taskGroup_1){
margin-left: 30px;
}
box-shadow:
2px 0 0 0 #888,
0 2px 0 0 #888,
2px 2px 0 0 #888, /* Just to fix the corner */
2px 0 0 0 #888 inset,
0 2px 0 0 #888 inset;
}
.taskGroup_0{
grid-row: 1;
}

+ 429
- 0
package-lock.json View File

@ -0,0 +1,429 @@
{
"name": "kb",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
"requires": {
"@babel/highlight": "^7.12.13"
}
},
"@babel/helper-validator-identifier": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw=="
},
"@babel/highlight": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA=="
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"dependencies": {
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"concurrently": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.0.tgz",
"integrity": "sha512-Ik9Igqnef2ONLjN2o/OVx1Ow5tymVvvEwQeYCQdD/oV+CN9oWhxLk7ibcBdOtv0UzBqHCEKRwbKceYoTK8t3fQ==",
"requires": {
"chalk": "^4.1.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.20",
"read-pkg": "^5.2.0",
"rxjs": "^6.6.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^16.2.0"
}
},
"date-fns": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.19.0.tgz",
"integrity": "sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg=="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"requires": {
"is-arrayish": "^0.2.1"
}
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-core-module": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
"integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
"requires": {
"has": "^1.0.3"
}
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"requires": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
},
"parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
}
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
"integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
"requires": {
"@types/normalize-package-data": "^2.4.0",
"normalize-package-data": "^2.5.0",
"parse-json": "^5.0.0",
"type-fest": "^0.6.0"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"resolve": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
}
},
"rxjs": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz",
"integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==",
"requires": {
"tslib": "^1.9.0"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A="
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
"requires": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
}
},
"spdx-exceptions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
},
"spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"requires": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
}
},
"spdx-license-ids": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ=="
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"requires": {
"has-flag": "^4.0.0"
}
},
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"type-fest": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
"integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"requires": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
}
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.7",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw=="
}
}
}

+ 17
- 0
package.json View File

@ -0,0 +1,17 @@
{
"name": "kb",
"version": "1.0.0",
"description": "## Description",
"main": "index.js",
"scripts": {
"start": "concurrently 'npm start --prefix back' 'npm start --prefix front'",
"dev": "concurrently 'npm run dev --prefix back' 'npm start --prefix front'",
"test": "npm run test --prefix back"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"concurrently": "^6.0.0"
}
}

Loading…
Cancel
Save