Compare commits

..

1 commit

Author SHA1 Message Date
Max
17be6ea3fd Initial prototype 2021-02-18 14:03:59 +01:00
47 changed files with 1106 additions and 1669 deletions

1
.gitignore vendored
View file

@ -29,3 +29,4 @@ graphics
.vscode
juggl-vue/package-lock.json
package-lock.json
public/api/vendor/

View file

@ -1,19 +0,0 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

416
package-lock.json generated
View file

@ -1855,9 +1855,9 @@
"dev": true
},
"ssri": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.1.tgz",
"integrity": "sha512-w+daCzXN89PseTL99MkA+fxJEcU3wfaE/ah0i0lnOlpG1CYLJ2ZjzEry68YBKfLs4JfoTShrTEsJkAZuNZ/stw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz",
"integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1",
@ -2585,18 +2585,11 @@
"dev": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.14.0"
},
"dependencies": {
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
}
"follow-redirects": "^1.10.0"
}
},
"babel-eslint": {
@ -2986,30 +2979,16 @@
}
},
"browserslist": {
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz",
"integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.723",
"caniuse-lite": "^1.0.30001165",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.621",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001245",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz",
"integrity": "sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA==",
"dev": true
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
}
"node-releases": "^1.1.67"
}
},
"buffer": {
@ -3576,9 +3555,9 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz",
"integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
@ -4159,72 +4138,15 @@
"dev": true
},
"cssnano": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
"integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==",
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
"integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
"dev": true,
"requires": {
"cosmiconfig": "^5.0.0",
"cssnano-preset-default": "^4.0.8",
"cssnano-preset-default": "^4.0.7",
"is-resolvable": "^1.0.0",
"postcss": "^7.0.0"
},
"dependencies": {
"cssnano-preset-default": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz",
"integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==",
"dev": true,
"requires": {
"css-declaration-sorter": "^4.0.1",
"cssnano-util-raw-cache": "^4.0.1",
"postcss": "^7.0.0",
"postcss-calc": "^7.0.1",
"postcss-colormin": "^4.0.3",
"postcss-convert-values": "^4.0.1",
"postcss-discard-comments": "^4.0.2",
"postcss-discard-duplicates": "^4.0.2",
"postcss-discard-empty": "^4.0.1",
"postcss-discard-overridden": "^4.0.1",
"postcss-merge-longhand": "^4.0.11",
"postcss-merge-rules": "^4.0.3",
"postcss-minify-font-values": "^4.0.2",
"postcss-minify-gradients": "^4.0.2",
"postcss-minify-params": "^4.0.2",
"postcss-minify-selectors": "^4.0.2",
"postcss-normalize-charset": "^4.0.1",
"postcss-normalize-display-values": "^4.0.2",
"postcss-normalize-positions": "^4.0.2",
"postcss-normalize-repeat-style": "^4.0.2",
"postcss-normalize-string": "^4.0.2",
"postcss-normalize-timing-functions": "^4.0.2",
"postcss-normalize-unicode": "^4.0.1",
"postcss-normalize-url": "^4.0.1",
"postcss-normalize-whitespace": "^4.0.2",
"postcss-ordered-values": "^4.1.2",
"postcss-reduce-initial": "^4.0.3",
"postcss-reduce-transforms": "^4.0.2",
"postcss-svgo": "^4.0.3",
"postcss-unique-selectors": "^4.0.1"
}
},
"postcss-svgo": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz",
"integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==",
"dev": true,
"requires": {
"postcss": "^7.0.0",
"postcss-value-parser": "^3.0.0",
"svgo": "^1.0.0"
}
},
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true
}
}
},
"cssnano-preset-default": {
@ -4695,9 +4617,9 @@
"dev": true
},
"dns-packet": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
"integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
"dev": true,
"requires": {
"ip": "^1.1.0",
@ -4762,20 +4684,12 @@
"dev": true
},
"domhandler": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz",
"integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"dev": true,
"requires": {
"domelementtype": "^2.2.0"
},
"dependencies": {
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
}
"domelementtype": "1"
}
},
"domutils": {
@ -4856,30 +4770,30 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.775",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz",
"integrity": "sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q==",
"version": "1.3.633",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz",
"integrity": "sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==",
"dev": true
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"dev": true
}
}
@ -5812,8 +5726,7 @@
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==",
"dev": true
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
},
"for-in": {
"version": "1.0.2",
@ -6068,9 +5981,9 @@
}
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
@ -6310,9 +6223,9 @@
"dev": true
},
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"dev": true
},
"hpack.js": {
@ -6438,43 +6351,34 @@
}
},
"htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.5.2",
"entities": "^2.0.0"
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
},
"dependencies": {
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
},
"domutils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
@ -7512,9 +7416,9 @@
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
},
"lodash.defaultsdeep": {
@ -8198,9 +8102,9 @@
}
},
"node-releases": {
"version": "1.1.73",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
"version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"dev": true
},
"node-sass": {
@ -8884,9 +8788,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-to-regexp": {
@ -9016,9 +8920,9 @@
"dev": true
},
"postcss": {
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -10138,16 +10042,16 @@
"dev": true
},
"renderkid": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz",
"integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.4.tgz",
"integrity": "sha512-K2eXrSOJdq+HuKzlcjOlGoOarUu5SDguDEhE7+Ah4zuOWL40j8A/oHvLlLob9PSTNvVnBd+/q0Er1QfpEuem5g==",
"dev": true,
"requires": {
"css-select": "^4.1.3",
"dom-converter": "^0.2.0",
"htmlparser2": "^6.1.0",
"lodash": "^4.17.21",
"strip-ansi": "^3.0.1"
"css-select": "^1.1.0",
"dom-converter": "^0.2",
"htmlparser2": "^3.3.0",
"lodash": "^4.17.20",
"strip-ansi": "^3.0.0"
},
"dependencies": {
"ansi-regex": {
@ -10157,59 +10061,31 @@
"dev": true
},
"css-select": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
"integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"dev": true,
"requires": {
"boolbase": "^1.0.0",
"css-what": "^5.0.0",
"domhandler": "^4.2.0",
"domutils": "^2.6.0",
"nth-check": "^2.0.0"
"boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
}
},
"css-what": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
"integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==",
"dev": true
},
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
"dev": true
},
"domutils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
},
"nth-check": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz",
"integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==",
"dev": true,
"requires": {
"boolbase": "^1.0.0"
"dom-serializer": "0",
"domelementtype": "1"
}
},
"strip-ansi": {
@ -11271,9 +11147,9 @@
}
},
"ssri": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
@ -11422,9 +11298,9 @@
},
"dependencies": {
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
}
}
@ -12206,9 +12082,9 @@
}
},
"url-parse": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
@ -12404,9 +12280,9 @@
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"version": "npm:vue-loader@16.1.2",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
"dev": true,
"optional": true,
"requires": {
@ -12416,9 +12292,9 @@
},
"dependencies": {
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
@ -12468,11 +12344,6 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vue-timers": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/vue-timers/-/vue-timers-2.0.4.tgz",
"integrity": "sha512-QOEVdO4V4o9WjFG6C0Kn9tfdTeeECjqvEQozcQlfL1Tn8v0qx4uUPhTYoc1+s6qoJnSbu8f68x8+nm1ZEir0kw=="
},
"vuex": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
@ -13222,12 +13093,45 @@
"dev": true
},
"wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"dev": true,
"requires": {
"string-width": "^1.0.2 || 2 || 3 || 4"
"string-width": "^1.0.2 || 2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
},
"word-wrap": {
@ -13272,9 +13176,9 @@
}
},
"ws": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"

View file

@ -9,13 +9,12 @@
"lint:fix": "eslint --fix --ext .js,.vue ."
},
"dependencies": {
"axios": "^0.21.4",
"axios": "^0.21.0",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.21.1",
"core-js": "^3.6.5",
"vue": "^2.6.12",
"vue-router": "^3.2.0",
"vue-timers": "^2.0.4",
"vuex": "^3.4.0"
},
"devDependencies": {

5
public/api/composer.json Normal file
View file

@ -0,0 +1,5 @@
{
"require": {
"tonyhhyip/sse": "^2.1"
}
}

678
public/api/composer.lock generated Normal file
View file

@ -0,0 +1,678 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "361f9482844d6c22d6efec567d317681",
"packages": [
{
"name": "symfony/deprecation-contracts",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665",
"reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-09-07T11:33:47+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v4.4.19",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "8888741b633f6c3d1e572b7735ad2cae3e03f9c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/8888741b633f6c3d1e572b7735ad2cae3e03f9c5",
"reference": "8888741b633f6c3d1e572b7735ad2cae3e03f9c5",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/mime": "^4.3|^5.0",
"symfony/polyfill-mbstring": "~1.1",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"predis/predis": "~1.0",
"symfony/expression-language": "^3.4|^4.0|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpFoundation\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-27T09:09:26+00:00"
},
{
"name": "symfony/mime",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/7dee6a43493f39b51ff6c5bb2bd576fe40a76c86",
"reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0",
"symfony/polyfill-php80": "^1.15"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<4.4"
},
"require-dev": {
"egulias/email-validator": "^2.1.10",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/property-access": "^4.4|^5.1",
"symfony/property-info": "^4.4|^5.1",
"symfony/serializer": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-02T06:10:15+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "2d63434d922daf7da8dd863e7907e67ee3031483"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483",
"reference": "2d63434d922daf7da8dd863e7907e67ee3031483",
"shasum": ""
},
"require": {
"php": ">=7.1",
"symfony/polyfill-intl-normalizer": "^1.10",
"symfony/polyfill-php72": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Trevor Rowbotham",
"email": "trevor.rowbotham@pm.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248",
"reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9",
"reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php72\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "tonyhhyip/sse",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/tonyhhyip/libSSE-php.git",
"reference": "804af074e46247b5d9290b509c5cab8de523c93a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tonyhhyip/libSSE-php/zipball/804af074e46247b5d9290b509c5cab8de523c93a",
"reference": "804af074e46247b5d9290b509c5cab8de523c93a",
"shasum": ""
},
"require": {
"php": ">=5.3.9",
"symfony/http-foundation": "~2.7 | ~3.0 | ~4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.2 | ~5.0"
},
"suggest": {
"predis/predis": "For using RedisMechnism",
"symfony/polyfill-apcu": "For APCMecnism on PHP < 7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Sse\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Licson Lee",
"email": "licson0729@gmail.com",
"homepage": "https://licson.net/",
"role": "Owner"
},
{
"name": "Tony Yip",
"email": "tony@opensource.hk",
"homepage": "https://github.com/tonyhhyip",
"role": "Developer"
}
],
"description": "An easy-to-use, object-oriented library for Server-Sent Events",
"homepage": "https://github.com/licson0729/libSSE-php",
"keywords": [
"sse"
],
"time": "2018-03-08T02:55:29+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

View file

@ -5,8 +5,8 @@ error_reporting(E_ALL | E_STRICT);
$config = [
"host" => "localhost",
"dbname" => "admin_juggl",
"username" => "admin_juggl",
"password" => "}dyn{5O!tUlZD;9R?lbi$.@=I,_a2L",
"dbname" => "juggl",
"username" => "juggl",
"password" => "?=5,}f_F&){;@xthx-[i",
"table_prefix" => "ju_"
];

44
public/api/events.php Normal file
View file

@ -0,0 +1,44 @@
<?php
require_once(__DIR__.'/vendor/autoload.php');
require_once(__DIR__."/services/authenticator.inc.php");
require_once(__DIR__."/services/responses.inc.php");
use Sse\Event;
use Sse\SSE;
//create the event handler
class YourEventHandler implements Event {
public function update(){
//Here's the place to send data
return 'Hello, world!';
}
public function check(){
//Here's the place to check when the data needs update
return true;
}
}
$SSE_KEY_KEY = "sse_key";
$auth = new Authenticator();
if (!isset($_GET["user_id"]) || !isset($_GET[$SSE_KEY_KEY]) || !$auth->isSseAuthenticated($_GET["user_id"], $_GET[$SSE_KEY_KEY])) {
respondStatus(403);
}
$sse = new SSE(); //create a libSSE instance
$sse->exec_limit = 600; //the execution time of the loop in seconds. Default: 600. Set to 0 to allow the script to run as long as possible.
$sse->sleep_time = 5; //The time to sleep after the data has been sent in seconds. Default: 0.5.
$sse->client_reconnect = 10; //the time for the client to reconnect after the connection has lost in seconds. Default: 1.
$sse->use_chunked_encoding = false; //Use chunked encoding. Some server may get problems with this and it defaults to false
$sse->keep_alive_time = 600; //The interval of sending a signal to keep the connection alive. Default: 300 seconds.
$sse->allow_cors = true; //Allow cross-domain access? Default: false. If you want others to access this must set to true.
$sse->addEventListener('change', new YourEventHandler());//register your event handler
$sse->start();//start the event loop
?>

View file

@ -24,12 +24,8 @@ class GetRecordsBranch extends ApiBranch
if ($params->exists(["finished"])) {
$finished = $params->get("finished");
}
$visible = NULL;
if ($params->exists(["visible"])) {
$visible = $params->get("visible");
}
$records = getRecords($user_id, $limit, $finished, $visible);
$records = getRecords($user_id, $limit, $finished);
$json = new JsonBuilder();
$json->addRecords($records);

View file

@ -1,38 +0,0 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetStatsBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$from_date = date("Y-m-d");
if ($params->exists(["from_date"])) {
$from_date = $params->get("from_date");
}
$until_date = date("Y-m-d");
if ($params->exists(["until_date"])) {
$until_date = $params->get("until_date");
}
$stats = getStats($user_id, $from_date, $until_date);
$json = new JsonBuilder();
$json->addStats($stats);
respondJson($json);
}
}
$branch = new GetStatsBranch();
$branch->execute();

View file

@ -1,5 +1,7 @@
<?php
require_once(__DIR__."/dbOperations.inc.php");
require_once(__DIR__."/paramCleaner.inc.php");
require_once(__DIR__."/jugglDbApi.inc.php");
class Authenticator {
function isApiKeyAuthenticated($api_key, $user_id) {
@ -16,5 +18,10 @@ class Authenticator {
function isAuthenticated($params) {
return $this->isApiKeyAuthenticated($params->get('api_key'), $params->get('user_id'));
}
function isSseAuthenticated($user_id, $sse_key) {
$params = new ParamCleaner(["sse_key" => $sse_key]);
return validatedSseKey($user_id, $params);
}
}
?>

View file

@ -7,7 +7,6 @@ class DbOperations
{
$this->resetQuery();
$this->tablePrefix = $tablePrefix;
$this->customValueId = 0;
require(__DIR__ . "/../config/config.php");
$this->config = $config;
@ -56,22 +55,6 @@ class DbOperations
return $this;
}
function groupBy(array $attributes, bool $addTableName = true)
{
for ($i = 0; $i < count($attributes); $i++) {
$a = $attributes[$i];
// Add table name prefix if missing
if ($addTableName && strpos($a, ".") === false) {
$attributes[$i] = "$this->table.$a";
}
}
$formattedAttributes = implode(', ', $attributes);
$this->addToQuery("GROUP BY $formattedAttributes");
return $this;
}
function orderBy(string $attribute, string $order = Order::ASC)
{

View file

@ -78,6 +78,24 @@ class JsonBuilder
return $this;
}
function addSseKey(array $sse)
{
if ($sse === null) return;
$columns = array(
"sse_key_id" => "",
"sse_key" => "",
"user_id" => "",
"created" => ""
);
$this->jsonData['sse'] = array();
foreach ($sse as $sse) {
$this->jsonData['sse'][] = $this->createJsonArray($sse, $columns);
}
return $this;
}
function addRecordTags(array $record_tags)
{
if ($record_tags === null) return;
@ -86,8 +104,7 @@ class JsonBuilder
"record_tag_id" => "",
"name" => "",
"user_id" => "",
"visible" => "",
"bundle" => ""
"visible" => ""
);
$this->jsonData['record_tags'] = array();
@ -97,27 +114,6 @@ class JsonBuilder
return $this;
}
function addStats(array $stats)
{
if ($stats === null) return;
$columns = array(
"name" => "",
"project_id" => "",
"color" => "",
"visible" => "",
"duration" => "",
"record_count" => "",
"date" => ""
);
$this->jsonData['stats'] = array();
foreach ($stats as $tag) {
$this->jsonData['stats'][] = $this->createJsonArray($tag, $columns);
}
return $this;
}
private function createJsonArray(array $data, array $columns)
{
$jsonArray = array();

View file

@ -160,17 +160,11 @@ function getRunningRecords($user_id)
return $results;
}
function getRecords($user_id, $limit = NULL, $finished = NULL, $visible = NULL)
function getRecords($user_id, $limit = NULL, $finished = NULL)
{
$db = new DbOperations();
$db->select("time_records", ["record_id", "start_time", "end_time", "duration", "user_id", "project_id", "start_device_id"]);
if ($visible != NULL) {
$db->innerJoin("projects", "project_id");
}
$db->where("ju_time_records.user_id", Comparison::EQUAL, $user_id);
if ($visible != NULL) {
$db->where("ju_projects.visible", Comparison::EQUAL, $visible);
}
$db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
if ($finished != NULL) {
if ($finished) {
$db->addSql(" AND end_time IS NOT NULL");
@ -216,7 +210,7 @@ function updateRecord($user_id, $record)
$data = [];
$props = ["end_time", "start_time", "duration", "project_id"];
foreach ($props as $p) {
if (array_key_exists($p, $record)) {
if (array_key_exists ($p, $record)) {
$data[$p] = $record[$p];
}
}
@ -236,7 +230,7 @@ function updateProject($user_id, $project)
$data = [];
$props = ["name", "start_date", "color", "visible"];
foreach ($props as $p) {
if (array_key_exists($p, $project)) {
if (array_key_exists ($p, $project)) {
$data[$p] = $project[$p];
}
}
@ -254,9 +248,9 @@ function updateRecordTag($user_id, $tag)
// Update given parameters
$data = [];
$props = ["name", "visible", "bundle"];
$props = ["name", "visible"];
foreach ($props as $p) {
if (array_key_exists($p, $tag)) {
if (array_key_exists ($p, $tag)) {
$data[$p] = $tag[$p];
}
}
@ -292,6 +286,108 @@ function removeProject($user_id, $params)
removeRecordsOfProject($user_id, $project_id);
}
function secureRandomBytes ()
{
$pr_bits = '';
// Unix/Linux platform?
$fp = @fopen('/dev/urandom','rb');
if ($fp !== FALSE) {
$pr_bits .= @fread($fp,16);
@fclose($fp);
}
// MS-Windows platform?
if (@class_exists('COM')) {
// http://msdn.microsoft.com/en-us/library/aa388176(VS.85).aspx
try {
$CAPI_Util = new COM('CAPICOM.Utilities.1');
$pr_bits .= $CAPI_Util->GetRandom(16,0);
// if we ask for binary data PHP munges it, so we
// request base64 return value. We squeeze out the
// redundancy and useless ==CRLF by hashing...
if ($pr_bits) { $pr_bits = md5($pr_bits,TRUE); }
} catch (Exception $ex) {
// echo 'Exception: ' . $ex->getMessage();
}
}
return $pr_bits;
}
function createSseKey($user_id)
{
// Generate random key
$sse_key = secureRandomBytes();
$data = [
"user_id" => $user_id,
"sse_key" => $sse_key
];
$db = new DbOperations();
$db->insert("sse_keys", $data);
$db->execute();
return $sse_key;
}
function deleteSseKey($user_id, $params)
{
$sse_key_id = $params->get("sse_key_id");
$sse_key = $params->get("sse_key");
$db = new DbOperations();
$db->delete("sse_keys");
$db->where("user_id", Comparison::EQUAL, $user_id);
if ($sse_key_id != null) {
$db->where("sse_key_id", Comparison::EQUAL, $sse_key_id);
}
if ($sse_key != null) {
$db->where("sse_key", Comparison::EQUAL, $sse_key);
}
$db->execute();
}
function getSseKey($user_id, $params)
{
$sse_key_id = $params->get("sse_key_id");
$sse_key = $params->get("sse_key");
$db = new DbOperations();
$db->select("sse_keys");
$db->where("user_id", Comparison::EQUAL, $user_id);
if ($sse_key_id != null) {
$db->where("sse_key_id", Comparison::EQUAL, $sse_key_id);
}
if ($sse_key != null) {
$db->where("sse_key", Comparison::EQUAL, $sse_key);
}
$result = $db->execute();
if (count($result) <= 0) {
return null;
}
$result = $result[0];
return $result;
}
function getAllSseKeys($user_id)
{
$db = new DbOperations();
$db->select("sse_keys");
$db->where("user_id", Comparison::EQUAL, $user_id);
return $db->execute();
}
function validatedSseKey($user_id, $params)
{
$key_entry = getSseKey($user_id, $params);
return $key_entry != null;
}
function removeRecordsOfProject($user_id, $project_id)
{
$db = new DbOperations();
@ -379,9 +475,7 @@ function getRecordExternalData($record)
$data = [
"record_tag_id" => $tag["record_tag_id"],
"name" => $tag["name"],
"user_id" => $tag["user_id"],
"visible" => $tag["visible"],
"bundle" => $tag["bundle"]
"user_id" => $tag["user_id"]
];
$tags[] = $data;
}
@ -464,30 +558,3 @@ function removeTagFromRecord($tag_id, $record_id)
$db->where("record_id", Comparison::EQUAL, $record_id);
$db->execute();
}
function getStats($user_id, $from_date, $until_date)
{
$sum_duration = "SUM(ju_time_records.duration) AS duration";
$date = "DATE(ju_time_records.start_time) AS date";
$record_count = "COUNT(*) AS record_count";
$where1 = " AND DATE(ju_time_records.start_time) >= DATE( ";
$where2 = " ) AND DATE(ju_time_records.start_time) <= DATE( ";
$where3 = " ) AND ju_time_records.end_time IS NOT NULL ";
$db = new DbOperations();
$db->select("projects", ["ju_projects.name AS name", "ju_projects.project_id", "color", "visible", $sum_duration, $date, $record_count], false);
$db->innerJoin("time_records", "project_id");
// $db->innerJoin("tags_on_records", "record_id", "record_id", "time_records");
// $db->innerJoin("record_tags", "record_tag_id", "record_tag_id", "tags_on_records");
$db->where("ju_projects.user_id", Comparison::EQUAL, $user_id);
$db->addSql($where1);
$db->addValue($from_date);
$db->addSql($where2);
$db->addValue($until_date);
$db->addSql($where3);
$db->groupBy(["ju_projects.project_id", "date"], false);
return $db->execute();
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<script>
var source = new EventSource('https://juggl.giller.dev/api/events.php?sse_key=9f8u592845vn982z4&user_id=1');
source.onmessage = function(e) {
document.body.innerHTML += e.data + '<br>';
};
</script>
</body>
</html>

View file

@ -1,2 +0,0 @@
User-agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,15 +1,17 @@
<template>
<div>
<b-link to="/">
<b-img :src="image" alt="Juggl" :width="widthSize" :center="center" />
<b-img
:src="require('../../assets/logo.png')"
alt="Juggl"
:width="widthSize"
:center="center"
/>
</b-link>
</div>
</template>
<script>
import logo from "./../../assets/logo.png";
import title from "./../../assets/title.png";
export default {
name: "BaseLogo",
props: {
@ -20,15 +22,11 @@ export default {
center: {
default: false,
type: Boolean
},
iconOnly: {
default: false,
type: Boolean
}
},
computed: {
widthSize: function() {
let titleSizes = {
let sizes = {
mini: "35px",
tiny: "80px",
smaller: "110px",
@ -40,34 +38,9 @@ export default {
huge: "800px",
massive: "960px"
};
let logoSizes = {
mini: "12px",
tiny: "30px",
smaller: "50px",
small: "65px",
normal: "80x",
medium: "100px",
large: "150px",
big: "200px",
huge: "300px",
massive: "400px"
};
var sizes = titleSizes;
if (this.iconOnly) {
sizes = logoSizes;
}
let targetSize = sizes[this.size];
if (targetSize === undefined) return sizes["small"];
else return targetSize;
},
image: function() {
if (this.iconOnly) {
return logo;
} else {
return title;
}
}
}
};

View file

@ -1,6 +1,6 @@
<template>
<section>
<BaseTitle v-if="title" center size="huge">{{ title }}</BaseTitle>
<BaseTitle v-if="title" center size="large">{{ title }}</BaseTitle>
<slot />
</section>
</template>

View file

@ -1,9 +1,5 @@
<template>
<h1
id="title"
:class="[size, { center: center }, { outline: outline }]"
class="bold"
>
<h1 id="title" :class="[size, { center: center }]" class="bold pt-5 pb-3">
<slot />
</h1>
</template>
@ -19,18 +15,12 @@ export default {
center: {
default: true,
type: Boolean
},
outline: {
default: false,
type: Boolean
}
}
};
</script>
<style lang="sass" scoped>
@import '@/style/theme.sass'
#title.tiny
font-size: 1em
#title.small
@ -41,17 +31,10 @@ export default {
font-size: 1.7em
#title.huge
font-size: 2em
#title.giant
font-size: 3em
#title.center
text-align: center
#title.outline
color: transparent
-webkit-text-stroke-width: 1.5px
-webkit-text-stroke-color: $secondary
.bold
font-weight: bold
</style>

View file

@ -5,7 +5,6 @@
variant="outline-primary"
right
>
<b-dropdown-item to="/statistics">Statistics</b-dropdown-item>
<b-dropdown-item to="/logout">Log out</b-dropdown-item>
</b-dropdown>
</template>

View file

@ -1,12 +1,9 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="id-group" label-for="id" label="Project ID">
<b-form-group id="id-group" label-for="id" label="Record ID">
<b-form-input id="id" v-model="form.project_id" required trim disabled>
</b-form-input>
</b-form-group>
<b-form-checkbox id="visible" v-model="form.visible" dark>
Visible
</b-form-checkbox>
<b-form-group id="name-group" label-for="name" label="Name">
<b-form-input id="name" v-model="form.name" required trim> </b-form-input>
</b-form-group>
@ -66,8 +63,7 @@ export default {
project_id: undefined,
start_date: undefined,
name: undefined,
color: undefined,
visible: undefined
color: undefined
}
};
},
@ -109,7 +105,6 @@ export default {
this.form.name = this.project.name;
this.form.start_date = this.project.start_date;
this.form.color = this.project.color;
this.form.visible = this.project.visible;
}
};
</script>

View file

@ -156,7 +156,6 @@ export default {
start_time: this.times.start.date + " " + this.times.start.time,
end_time: this.times.end.date + " " + this.times.end.time,
duration: this.times.duration,
tags: this.record.tags,
start_device_id: this.record.start_device_id // TODO: Remove at some time
};

View file

@ -1,12 +1,9 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="id-group" label-for="id" label="Tag ID">
<b-form-group id="id-group" label-for="id" label="Record ID">
<b-form-input id="id" v-model="form.record_tag_id" required trim disabled>
</b-form-input>
</b-form-group>
<b-form-checkbox id="visible" v-model="form.visible" dark>
Visible
</b-form-checkbox>
<b-form-group id="name-group" label-for="name" label="Name">
<b-form-input id="name" v-model="form.name" required trim> </b-form-input>
</b-form-group>
@ -51,8 +48,7 @@ export default {
working: false,
form: {
record_tag_id: undefined,
name: undefined,
visible: undefined
name: undefined
}
};
},
@ -88,7 +84,6 @@ export default {
created: function() {
this.form.record_tag_id = this.tag.record_tag_id;
this.form.name = this.tag.name;
this.form.visible = this.tag.visible;
}
};
</script>

View file

@ -1,151 +0,0 @@
<template>
<b-table
:items="aggregatedStatistics"
primary-key="day"
hover
:busy="isLoading"
:fields="statistic_fields"
sort-by="day"
sort-desc
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(distribution)="data">
<JugglProjectDistribution :projects="data.item.projects" />
</template>
<template #cell(details)="data">
<b-button
size="sm"
@click="data.toggleDetails"
variant="outline-secondary"
>
<b-icon class="icon-btn" icon="bar-chart-steps" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<JugglProjectDistribution
:projects="data.item.projects"
names
class="mb-3"
/>
<JugglProjectStatisticsList :statistics="data.item.statistics" />
</b-card>
</template>
</b-table>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
import JugglProjectStatisticsList from "./JugglProjectStatisticsList.vue";
import JugglProjectDistribution from "./JugglProjectDistribution.vue";
export default {
name: "JugglDailyStatisticsList",
components: { JugglProjectStatisticsList, JugglProjectDistribution },
props: {
statistics: {
required: true,
type: Array
}
},
data: () => {
return {
statistic_fields: [
{
key: "day",
label: "Day"
},
{
key: "duration",
label: "Duration"
},
{
key: "distribution",
lavel: "Distribution"
},
{
key: "record_count",
label: "Records"
},
{
key: "details",
label: "Details"
}
]
};
},
computed: {
isLoading: function() {
return this.statistics === undefined;
},
aggregatedStatistics: function() {
if (this.statistics === undefined) {
return [];
}
var aggregated = {};
this.statistics.forEach(stat => {
var dayName = stat.date;
if (aggregated[dayName] === undefined) {
aggregated[dayName] = {
day: dayName,
duration: 0,
record_count: 0,
statistics: [],
projects: {}
};
}
if (aggregated[dayName].projects[stat.project_id] === undefined) {
aggregated[dayName].projects[stat.project_id] = {
project_id: stat.project_id,
duration: 0,
record_count: 0,
color: stat.color,
name: stat.name,
visible: stat.visible,
statistics: []
};
}
aggregated[dayName].duration += Number(stat.duration);
aggregated[dayName].record_count += Number(stat.record_count);
aggregated[dayName].statistics.push(stat);
aggregated[dayName].projects[stat.project_id].duration += Number(
stat.duration
);
aggregated[dayName].projects[stat.project_id].record_count += Number(
stat.record_count
);
aggregated[dayName].projects[stat.project_id].statistics.push(stat);
});
// Simplyfying object to lists
aggregated = Object.values(aggregated);
aggregated.forEach(stat => {
stat.projects = Object.values(stat.projects);
});
return aggregated;
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass"></style>

View file

@ -1,151 +0,0 @@
<template>
<b-table
:items="aggregatedStatistics"
primary-key="month"
hover
:busy="isLoading"
:fields="statistic_fields"
sort-by="month"
sort-desc
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(distribution)="data">
<JugglProjectDistribution :projects="data.item.projects" class="mb-3" />
</template>
<template #cell(details)="data">
<b-button
size="sm"
@click="data.toggleDetails"
variant="outline-secondary"
>
<b-icon class="icon-btn" icon="bar-chart-steps" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<JugglProjectDistribution
:projects="data.item.projects"
names
class="mb-3"
/>
<JugglProjectStatisticsList :statistics="data.item.statistics" />
</b-card>
</template>
</b-table>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
import JugglProjectStatisticsList from "./JugglProjectStatisticsList.vue";
import JugglProjectDistribution from "./JugglProjectDistribution.vue";
export default {
name: "JugglMonthlyStatisticsList",
components: { JugglProjectStatisticsList, JugglProjectDistribution },
props: {
statistics: {
required: true,
type: Array
}
},
data: () => {
return {
statistic_fields: [
{
key: "month",
label: "Month"
},
{
key: "duration",
label: "Duration"
},
{
key: "distribution",
lavel: "Distribution"
},
{
key: "record_count",
label: "Records"
},
{
key: "details",
label: "Details"
}
]
};
},
computed: {
isLoading: function() {
return this.statistics === undefined;
},
aggregatedStatistics: function() {
if (this.statistics === undefined) {
return [];
}
var aggregated = {};
this.statistics.forEach(stat => {
var monthName = stat.date.substring(0, 7); // date is in the format "YYYY-MM-DD", so simply cutting of the day
if (aggregated[monthName] === undefined) {
aggregated[monthName] = {
month: monthName,
duration: 0,
record_count: 0,
statistics: [],
projects: {}
};
}
if (aggregated[monthName].projects[stat.project_id] === undefined) {
aggregated[monthName].projects[stat.project_id] = {
project_id: stat.project_id,
duration: 0,
record_count: 0,
color: stat.color,
name: stat.name,
visible: stat.visible,
statistics: []
};
}
aggregated[monthName].duration += Number(stat.duration);
aggregated[monthName].record_count += Number(stat.record_count);
aggregated[monthName].statistics.push(stat);
aggregated[monthName].projects[stat.project_id].duration += Number(
stat.duration
);
aggregated[monthName].projects[stat.project_id].record_count += Number(
stat.record_count
);
aggregated[monthName].projects[stat.project_id].statistics.push(stat);
});
// Simplyfying object to lists
aggregated = Object.values(aggregated);
aggregated.forEach(stat => {
stat.projects = Object.values(stat.projects);
});
return aggregated;
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass"></style>

View file

@ -1,45 +0,0 @@
<template>
<b-progress :max="totalDuration">
<b-progress-bar
v-for="(project, index) in projects"
v-bind:key="project.project_id"
:value="project.duration"
:style="'background-color: ' + project.color + ' !important'"
:variant="index % 2 == 0 ? 'primary' : 'dark'"
v-b-tooltip.hover
:title="project.name + ' · ' + getDurationTimestamp(project.duration)"
><span v-if="names">{{ project.name }}</span></b-progress-bar
>
</b-progress>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
export default {
name: "JugglProjectDistribution",
props: {
projects: {
required: true,
type: Array
},
names: {
required: false,
type: Boolean,
default: false
}
},
computed: {
totalDuration: function() {
var duration = 0;
this.projects.forEach(stat => (duration += Number(stat.duration)));
return duration;
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style></style>

View file

@ -1,112 +0,0 @@
<template>
<b-table
:items="projectStatistics"
primary-key="project_id"
hover
:busy="isLoading"
:fields="statistic_fields"
sort-by="duration"
sort-desc
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(project)="data">
<JugglProjectName :project="data.item" />
</template>
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(distribution)="data">
{{ ((data.item.duration / totalDuration) * 100).toFixed(0) }}%
</template>
</b-table>
</template>
<script>
import JugglProjectName from "@/components/juggl/JugglProjectName";
import { helperService } from "@/services/helper.service.js";
export default {
name: "JugglProjectStatisticsList",
components: {
JugglProjectName
},
props: {
statistics: {
required: true,
type: Array
}
},
data: () => {
return {
statistic_fields: [
{
key: "project",
label: "Project"
},
{
key: "duration",
label: "Duration"
},
{
key: "record_count",
label: "Records"
},
{
key: "distribution",
label: "Distribution"
}
]
};
},
computed: {
isLoading: function() {
return this.statistics === undefined;
},
totalDuration: function() {
var duration = 0;
this.statistics.forEach(stat => (duration += Number(stat.duration)));
return duration;
},
projectStatistics: function() {
if (this.statistics === undefined) {
return [];
}
var projects = {};
this.statistics.forEach(stat => {
if (projects[stat.project_id] === undefined) {
projects[stat.project_id] = {
project_id: stat.project_id,
duration: 0,
record_count: 0,
color: stat.color,
name: stat.name,
visible: stat.visible,
statistics: []
};
}
projects[stat.project_id].duration += Number(stat.duration);
projects[stat.project_id].record_count += Number(stat.record_count);
projects[stat.project_id].statistics.push(stat);
});
// Simplyfying object to lists
return Object.values(projects);
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass"></style>

View file

@ -1,7 +1,6 @@
<template>
<b-table
:items="records"
primary-key="record_id"
hover
:busy="isLoading"
:fields="fields"
@ -28,10 +27,7 @@
</template>
<template #cell(tags)="data">
<JugglTagField
:recordId="data.item.record_id"
:onlyVisible="onlyVisibleTags"
/>
<JugglTagField :recordId="data.item.record_id" />
</template>
<template #cell(details)="data">
@ -94,17 +90,11 @@ export default {
required: false,
type: Boolean,
default: false
},
onlyVisibleTags: {
required: false,
type: Boolean,
default: true
}
},
data() {
return {
iconScale: 1.6,
timerDelta: 0,
requiredFields: [
{
key: "project",
@ -149,18 +139,8 @@ export default {
return fields;
}
},
timers: {
"increaseTimer": { time: 1000, autostart: true, repeat: true }
},
methods: {
getDurationTimestamp: function(duration) {
var localDuration = duration;
if (this.running) {
localDuration += this.timerDelta;
}
return helperService.getDurationTimestamp(localDuration);
},
getDurationTimestamp: helperService.getDurationTimestamp,
getProject: function(id) {
var project = store.getters.getProjectById(id);
@ -175,15 +155,6 @@ export default {
},
detailsRecord: function(id) {
this.$router.push("/record/" + id);
},
increaseTimer: function() {
if (this.records.length <= 0) {
return;
}
var record = this.records[0];
var liveDuration = (new Date().getTime() - new Date(record.start_time).getTime()) / 1000;
this.timerDelta = liveDuration - record.duration;
}
}
};

View file

@ -1,6 +1,5 @@
<template>
<div :id="containerId" class="tag-container">
<!-- Tag item list -->
<div
class="tag-item"
v-for="tag in addedTags"
@ -10,12 +9,10 @@
{{ tag.name }}
</div>
<!-- Add-button -->
<div :id="btnId">
<b-icon icon="plus" />
</div>
<!-- Popover -->
<b-popover
:target="btnId"
triggers="click"
@ -56,11 +53,6 @@ export default {
props: {
recordId: {
required: true
},
onlyVisible: {
required: false,
type: Boolean,
default: false
}
},
data() {
@ -103,16 +95,10 @@ export default {
);
},
allTags: function() {
if (this.onlyVisible) {
return store.getters.getFilteredTags({ visible: true });
} else {
return store.getters.tags;
}
return store.getters.tags;
},
addedTags: function() {
return Object.values(this.record.tags).filter(
t => !this.onlyVisible || t.visible
);
return this.record.tags;
},
usedTagIds: function() {
var ids = [];

View file

@ -1,33 +0,0 @@
<template>
<footer>
<BaseContainer
width="medium"
center
>
<b-link to="/tools" class="surround-space">Tools</b-link>
<b-link to="/changelog" class="surround-space">Changelog</b-link>
<b-link to="/credits" class="surround-space">Credits</b-link>
</BaseContainer>
</footer>
</template>
<script>
import BaseContainer from "@/components/base/BaseContainer.vue";
export default {
name: "LayoutFooter",
components: {
BaseContainer,
},
};
</script>
<style lang="sass" scoped>
footer
margin-bottom: 3rem
text-align: center
.surround-space
margin-left: 1rem
margin-right: 1rem
</style>

View file

@ -1,46 +1,35 @@
<template>
<div>
<main>
<BaseLogo id="logo" size="medium" center class="space-top space-bottom-small" icon-only />
<BaseContainer
:width="width"
center
class="space-bottom"
:class="{ 'center-content': center }"
>
<BaseTitle
v-if="title"
center
size="giant"
class="centered space-bottom"
outline
>
{{ title.toUpperCase() }}
</BaseTitle>
<slot />
</BaseContainer>
</main>
<LayoutFooter />
</div>
<main>
<BaseLogo id="logo" size="medium" center class="space-top" />
<BaseContainer
:width="width"
center
class="space-bottom"
:class="{ 'center-content': center }"
>
<BaseTitle v-if="title" center size="huge" class="centered">
{{ title }}
</BaseTitle>
<slot />
</BaseContainer>
</main>
</template>
<script>
import BaseContainer from "@/components/base/BaseContainer";
import BaseLogo from "@/components/base/BaseLogo";
import BaseTitle from "@/components/base/BaseTitle";
import LayoutFooter from "@/components/layout/LayoutFooter";
export default {
name: "LayoutMinimal",
components: {
BaseContainer,
BaseLogo,
BaseTitle,
LayoutFooter
BaseTitle
},
props: {
width: {
default: "slim",
default: "",
type: String
},
title: {
@ -65,7 +54,4 @@ export default {
.center-content
text-align: center
.space-bottom-small
margin-bottom: 1rem
</style>

View file

@ -18,13 +18,12 @@
center
:class="{ 'center-content': center }"
>
<BaseTitle v-if="title" center size="giant" class="centered" outline>
{{ title.toUpperCase() }}
<BaseTitle v-if="title" center size="huge" class="centered">
{{ title }}
</BaseTitle>
<slot />
</BaseContainer>
</main>
<LayoutFooter/>
</div>
</template>
@ -33,7 +32,6 @@ import BaseContainer from "@/components/base/BaseContainer.vue";
import BaseLogo from "@/components/base/BaseLogo.vue";
import BaseUserDropdown from "@/components/base/BaseUserDropdown.vue";
import BaseTitle from "@/components/base/BaseTitle";
import LayoutFooter from "@/components/layout/LayoutFooter";
export default {
name: "LayoutNavbarPrivate",
@ -41,8 +39,7 @@ export default {
BaseContainer,
BaseLogo,
BaseUserDropdown,
BaseTitle,
LayoutFooter
BaseTitle
},
props: {
title: {
@ -89,14 +86,6 @@ main
margin-top: 6rem
margin-bottom: 6rem
footer
margin-bottom: 3rem
text-align: center
*
margin-left: 1rem
margin-right: 1rem
.center-content
text-align: center
</style>

View file

@ -3,12 +3,9 @@ import App from "./App.vue";
import router from "./router";
import store from "./store";
import { BootstrapVue, BootstrapVueIcons } from "bootstrap-vue";
import VueTimers from 'vue-timers'
Vue.config.productionTip = false;
Vue.use(VueTimers);
// Install BootstrapVue
Vue.use(BootstrapVue);
Vue.use(BootstrapVueIcons);

View file

@ -6,10 +6,6 @@ import NotFound from "../views/NotFound.vue";
import Home from "../views/Home.vue";
import History from "../views/History.vue";
import Manage from "../views/Manage.vue";
import Statistics from "../views/Statistics.vue";
import Changelog from "../views/Changelog.vue";
import Credits from "../views/Credits.vue";
import ToolsDocumentation from "../views/ToolsDocumentation.vue";
Vue.use(VueRouter);
@ -41,27 +37,6 @@ const routes = [
component: Home,
beforeEnter: requireAuth
},
{
path: "/statistics",
name: "Statistics",
component: Statistics,
beforeEnter: requireAuth
},
{
path: "/changelog",
name: "Changelog",
component: Changelog
},
{
path: "/credits",
name: "Credits",
component: Credits
},
{
path: "/tools",
name: "ToolsDocumentation",
component: ToolsDocumentation
},
{
path: "/logout",
name: "Logout",

View file

@ -64,28 +64,13 @@ export const helperService = {
* @returns Date as string in the used format
*/
toISODate(date) {
return (
date.getFullYear() +
"-" +
(date.getMonth() + 1).toString().padStart(2, "0") +
"-" +
date
.getDate()
.toString()
.padStart(2, "0") +
"T00:00.000Z"
);
},
var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
var timestamp = date.getTime() + timezoneOffset * 1000;
var correctDate = new Date(timestamp);
/**
* Converts a date object into a date-time-timezone string and only keeps the date.
*
* @param {*} date A date object
*
* @returns Date as string in YYYY-MM-DD format
*/
toISODateOnly(date) {
return helperService.toISODate(date).substring(0, 10);
correctDate.setUTCHours(0, 0, 0, 0);
return correctDate.toISOString();
},
/**

View file

@ -22,7 +22,7 @@ export const jugglService = {
getProjects() {
return apiService.post("/getProjects.php").then(r => {
return {
data: { projects: processProjects(r.data.projects) },
data: r.data,
msg: ""
};
});
@ -31,33 +31,12 @@ export const jugglService = {
getTags() {
return apiService.post("/getRecordTags.php").then(r => {
return {
data: { record_tags: processTags(r.data.record_tags) },
data: r.data,
msg: ""
};
});
},
getStatistics(options = {}) {
if (options.from === undefined) {
options.from = new Date();
}
if (options.until === undefined) {
options.until = new Date();
}
return apiService
.post("/getStats.php", {
from_date: helperService.toISODateOnly(options.from),
until_date: helperService.toISODateOnly(options.until)
})
.then(r => {
return {
data: { statistics: processVisibility(r.data.stats) },
msg: ""
};
});
},
getRecord(recordId) {
return apiService
.post("/getRecord.php", {
@ -119,9 +98,6 @@ export const jugglService = {
if (options.finished !== undefined) {
payload.finished = options.finished;
}
if (options.visible !== undefined) {
payload.visible = options.visible;
}
return apiService.post("/getRecords.php", payload).then(r => {
return {
@ -272,26 +248,7 @@ function processRecords(data) {
rec.duration = helperService.calcDurationInSeconds(rec.start_time);
}
rec.tags = processTags(Object.values(rec.tags));
rec.tags = Object.values(rec.tags);
});
return data;
}
function processTags(tags) {
return processVisibility(tags);
}
function processProjects(projects) {
return processVisibility(projects);
}
function processVisibility(items) {
Object.values(items).forEach(item => {
if (item.visible === "1") {
item.visible = true;
} else {
item.visible = false;
}
});
return items;
}

View file

@ -6,7 +6,6 @@ export const juggl = {
projects: [],
records: [],
tags: [],
statistics: [],
user: undefined,
auth: undefined,
recordsLimit: 0
@ -21,9 +20,6 @@ export const juggl = {
setTags(state, tags) {
state.tags = tags;
},
setStatistics(state, statistics) {
state.statistics = statistics;
},
setRecordsLimit(state, limit) {
state.recordsLimit = limit;
},
@ -48,55 +44,10 @@ export const juggl = {
finishedRecords: state => {
return Object.values(state.records).filter(record => !record.running);
},
getFilteredRecords: (state, getters) => ({
running = undefined,
projectVisible = undefined,
records = undefined
}) => {
if (records == undefined) {
records = getters.records;
}
var visibleProjects = getters.visibleProjects;
var visibleIds = [];
Object.values(visibleProjects)
.filter(p => p.visible)
.forEach(p => {
visibleIds.push(p.project_id);
});
return Object.values(records).filter(rec => {
if (running !== undefined && running !== rec.running) {
return false;
}
var recProjectVisible = visibleIds.includes(rec.project_id);
if (
projectVisible !== undefined &&
projectVisible !== recProjectVisible
) {
return false;
}
return true;
});
},
getFilteredStatistics: (state, getters) => ({
projectVisible = undefined
}) => {
return Object.values(getters.statistics).filter(statistic => {
if (
projectVisible !== undefined &&
statistic.visible !== projectVisible
) {
return false;
}
return true;
});
},
auth: state => state.auth,
apiUrl: state => state.apiUrl,
user: state => state.user,
isLoggedIn: state => !!state.auth,
statistics: state => state.statistics,
records: state => state.records,
projects: state => state.projects,
tags: state => state.tags,
@ -122,10 +73,10 @@ export const juggl = {
return getters.projectIds.filter(id => !runningProjectIds.includes(id));
},
finishedProjects: (state, getters) => {
return getters.getFilteredProjects({ finished: true });
},
visibleProjects: (state, getters) => {
return getters.getFilteredProjects({ visible: true });
var ids = getters.finishedProjectIds;
return Object.values(state.projects).filter(project =>
ids.includes(project.project_id)
);
},
runningProjects: (state, getters) => {
var ids = getters.runningProjectIds;
@ -138,43 +89,6 @@ export const juggl = {
project => project.project_id === id
);
},
getFilteredProjects: (state, getters) => ({
finished = undefined,
visible = undefined,
projects = undefined
}) => {
if (projects == undefined) {
projects = getters.projects;
}
var runningIds = getters.runningProjectIds;
return Object.values(projects).filter(project => {
var projectFinished = !runningIds.includes(project.project_id);
if (finished !== undefined && finished !== projectFinished) {
return false;
}
if (visible !== undefined && visible !== project.visible) {
return false;
}
return true;
});
},
getFilteredTags: (state, getters) => ({
visible = undefined,
tags = undefined
}) => {
if (tags == undefined) {
tags = getters.tags;
}
return Object.values(tags).filter(tag => {
if (visible != undefined && visible !== tag.visible) {
return false;
}
return true;
});
},
getTagById: (state, getters) => id => {
return Object.values(getters.tags).find(tag => tag.record_tag_id === id);
},
@ -221,16 +135,12 @@ export const juggl = {
return true;
});
},
loadRecords({ commit, state, getters }, { limit, finished, visible }) {
loadRecords({ commit, state, getters }, { limit, finished }) {
if (limit !== undefined) {
commit("setRecordsLimit", limit);
}
var payload = {
limit: state.recordsLimit,
finished: finished,
visible: visible
};
var payload = { limit: state.recordsLimit, finished: finished };
return jugglService.getRecords(payload).then(r => {
var allRecords = Object.values(r.data.records);
@ -244,29 +154,6 @@ export const juggl = {
commit("setRecords", allRecords);
});
},
loadDailyStatistics({ dispatch }, { date }) {
dispatch("loadStatistics", { from: date, until: date });
},
loadMonthlyStatistics(
{ dispatch },
{ startYear, startMonth, endYear, endMonth }
) {
// Month in date object goes from 0 - 11
var options = {
from: new Date(startYear, startMonth - 1, 1),
until: new Date(endYear, endMonth, 0) // 0 leads to the last day of the previous month
};
dispatch("loadStatistics", options);
},
async loadStatistics({ commit }, options) {
var results = Object.values(
(await jugglService.getStatistics(options)).data.statistics
);
commit("setStatistics", results);
},
loadRunningRecords({ commit, getters }) {
return jugglService.getRunningRecords().then(r => {
var allRecords = {

View file

@ -1,82 +0,0 @@
<template>
<LayoutMinimal title="Changelog">
<BaseSection>
<BaseTitle size="small">
11.02.2022
</BaseTitle>
<ul>
<li>Implemented daily and monthly statistics</li>
<li>Added tool</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
23.11.2021
</BaseTitle>
<ul>
<li>Added tools page</li>
<li>Visual tweaks</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
07.11.2021
</BaseTitle>
<ul>
<li>Added credits page</li>
<li>Created basic footer</li>
<li>Added live record timer</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
27.07.2021
</BaseTitle>
<ul>
<li>Added simple statistics</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
21.05.2021
</BaseTitle>
<ul>
<li>Visual tweaks</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
12.04.2021
</BaseTitle>
<ul>
<li>Added toggle to change visibility of single projects and tags</li>
</ul>
</BaseSection>
</LayoutMinimal>
</template>
<script>
// @ is an alias to /src
import LayoutMinimal from "@/components/layout/LayoutMinimal";
import BaseTitle from "@/components/base/BaseTitle";
import BaseSection from "../components/base/BaseSection.vue";
export default {
name: "Changelog",
components: {
LayoutMinimal,
BaseTitle,
BaseSection
}
};
</script>
<style lang="sass" scoped>
ul
list-style-type: '+ '
</style>

View file

@ -1,51 +0,0 @@
<template>
<LayoutMinimal title="Credits" >
<BaseSection>
<BaseTitle>
vue-timers
</BaseTitle>
<BaseTitle size="tiny">
<b-link href="https://github.com/Kelin2025/vue-timers">
<b-icon-github></b-icon-github>
GitHub
</b-link>
</BaseTitle>
<p class="monospace">
MIT License
<br/>
<br/>
Copyright (c) 2017 Anton Kosykh
<br/>
<br/>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
<br/>
<br/>
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
<br/>
<br/>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
</BaseSection>
</LayoutMinimal>
</template>
<script>
// @ is an alias to /src
import LayoutMinimal from "@/components/layout/LayoutMinimal";
import BaseTitle from "@/components/base/BaseTitle";
import BaseSection from "@/components/base/BaseSection";
export default {
name: "Credits",
components: {
LayoutMinimal,
BaseTitle,
BaseSection
}
};
</script>
<style lang="sass" scoped>
.monospace
font-family: Courier New, Courier, monospace
</style>

View file

@ -26,7 +26,7 @@ import { helperService } from "@/services/helper.service.js";
import store from "@/store";
export default {
name: "History",
name: "Home",
data: () => {
return {
working: true
@ -60,7 +60,7 @@ export default {
store.dispatch("loadTags");
store.dispatch("loadProjects");
store
.dispatch("loadRecords", { limit: 0, finished: true, visible: true })
.dispatch("loadRecords", { limit: 0, finished: true })
.then(() => {
this.working = false;
})

View file

@ -4,8 +4,8 @@
<JugglRecordsList :records="runningRecords" running />
</BaseSection>
<BaseSection title="Projects">
<div v-if="availableProjects.length > 0">
<JugglProjectsPanel :projects="availableProjects" />
<div v-if="finishedProjects.length > 0">
<JugglProjectsPanel :projects="finishedProjects" />
</div>
<div id="add-project-form">
<FormProjectAdd />
@ -43,30 +43,21 @@ export default {
BaseSection
},
computed: {
availableProjects: () => {
return store.getters.getFilteredProjects({
finished: true,
visible: true
});
finishedProjects: () => {
return store.getters.finishedProjects;
},
finishedRecords: () => {
return store.getters.getFilteredRecords({
running: false,
projectVisible: true
});
return store.getters.finishedRecords;
},
runningRecords: () => {
return store.getters.getFilteredRecords({
running: true,
projectVisible: true
});
return store.getters.runningRecords;
}
},
created: () => {
store.dispatch("loadProjects");
store.dispatch("loadTags");
store.dispatch("loadRunningRecords");
store.dispatch("loadRecords", { limit: 10, finished: true, visible: true });
store.dispatch("loadRecords", { limit: 10, finished: true });
}
};
</script>

View file

@ -97,7 +97,7 @@ import FormTagAdd from "@/components/forms/FormTagAdd";
import store from "@/store";
export default {
name: "Manage",
name: "Home",
components: {
LayoutNavbarPrivate,
FormProjectDetails,

View file

@ -1,6 +1,6 @@
<template>
<LayoutMinimal center title="404">
Couldn't find what you were looking for :(
<LayoutMinimal center title="Couldn't find what you were looking for :(">
404
</LayoutMinimal>
</template>

View file

@ -1,180 +0,0 @@
<template>
<LayoutNavbarPrivate title="Statistics">
<BaseContainer class="centered mb-5">
<b-form-radio-group
id="mode-radio"
v-model="mode"
:options="options"
button-variant="outline-primary"
name="radio-btn-outline"
buttons
size="sm"
class="mb-3"
@input="updateStatistics"
></b-form-radio-group>
<b-form id="form-daily" v-if="mode == 'daily'" inline>
<b-form-datepicker
id="startdate"
v-model="daily.startDate"
required
placeholder="Choose a start date"
:max="daily.endDate"
dark
@input="updateStatistics"
>
</b-form-datepicker>
<b-form-datepicker
id="enddate"
v-model="daily.endDate"
required
placeholder="Choose an end date"
:min="daily.startDate"
dark
@input="updateStatistics"
>
</b-form-datepicker>
</b-form>
<b-form id="form-monthly" v-if="mode == 'monthly'" inline>
<b-input-group>
<b-input
id="start-year"
type="number"
placeholder="Start year"
v-model="monthly.startYear"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
<b-input
id="start-month"
type="number"
placeholder="Start month"
v-model="monthly.startMonth"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
</b-input-group>
<b-input-group>
<b-input
id="end-year"
type="number"
placeholder="End year"
v-model="monthly.endYear"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
<b-input
id="end-month"
type="number"
placeholder="End month"
v-model="monthly.endMonth"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
</b-input-group>
</b-form>
</BaseContainer>
<BaseSection id="projects" :title="modeTitle">
<JugglDailyStatisticsList
:statistics="visibleStatistics"
v-if="mode == 'daily'"
/>
<JugglMonthlyStatisticsList
:statistics="visibleStatistics"
v-if="mode == 'monthly'"
/>
</BaseSection>
</LayoutNavbarPrivate>
</template>
<script>
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
import JugglDailyStatisticsList from "@/components/juggl/JugglDailyStatisticsList";
import JugglMonthlyStatisticsList from "@/components/juggl/JugglMonthlyStatisticsList";
import { helperService } from "@/services/helper.service.js";
import BaseSection from "@/components/base/BaseSection";
import BaseContainer from "@/components/base/BaseContainer";
import store from "@/store";
export default {
name: "Statistics",
components: {
LayoutNavbarPrivate,
JugglMonthlyStatisticsList,
JugglDailyStatisticsList,
BaseSection,
BaseContainer
},
data: () => {
return {
working: true,
mode: "daily",
options: [
{ text: "Daily", value: "daily" },
{ text: "Monthly", value: "monthly" }
],
daily: {
startDate: new Date(),
endDate: new Date()
},
monthly: {
startYear: new Date().getFullYear(),
startMonth: new Date().getMonth() + 1,
endYear: new Date().getFullYear(),
endMonth: new Date().getMonth() + 1
}
};
},
mounted: function() {
this.updateStatistics();
},
computed: {
visibleStatistics: () => {
return store.getters.getFilteredStatistics({
projectVisible: true
});
},
modeTitle: function() {
return this.mode[0].toUpperCase() + this.mode.substring(1);
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp,
updateStatistics: function() {
if (this.mode == "daily") {
store.dispatch("loadStatistics", {
from: new Date(this.daily.startDate),
until: new Date(this.daily.endDate)
});
} else if (this.mode == "monthly") {
store.dispatch("loadMonthlyStatistics", this.monthly);
}
}
}
};
</script>
<style lang="sass">
.centered
text-align: center
#form-daily *, #form-monthly *
margin: auto
.slim
max-width: 6rem
</style>

View file

@ -1,61 +0,0 @@
<template>
<LayoutMinimal title="Tools">
<BaseTitle>
Graph Day Distribution
<br />
<small>by Linus Kämmerer</small>
</BaseTitle>
<BaseSection>
<BaseTitle size="tiny">
<b-link
href="https://gist.github.com/linuskmr/f89ac638da036e25d67e147ece67577c"
>
<b-icon-github></b-icon-github>
GitHub
</b-link>
</BaseTitle>
<p class="monospace">
Python script to generate a time distribution graph from the exported
Juggl data. It accumulates the tracked record and shows the total
working hours for different times for each weekday.
</p>
</BaseSection>
<BaseSection>
<BaseTitle>
JSON to CSV
<br />
<small>by Linus Kämmerer</small>
</BaseTitle>
<BaseTitle size="tiny">
<b-link
href="https://gist.github.com/linuskmr/856459cdf6c7ea460503b92e00b2aa26"
>
<b-icon-github></b-icon-github>
GitHub
</b-link>
</BaseTitle>
<p class="monospace">
Python script to convert exported Juggl data from a JSON to a CSV file.
</p>
</BaseSection>
</LayoutMinimal>
</template>
<script>
// @ is an alias to /src
import LayoutMinimal from "@/components/layout/LayoutMinimal";
import BaseTitle from "@/components/base/BaseTitle";
import BaseSection from "../components/base/BaseSection.vue";
export default {
name: "ToolsDocumentation",
components: {
LayoutMinimal,
BaseTitle,
BaseSection
}
};
</script>
<style lang="sass" scoped></style>