Compare commits
334 Commits
0.4.1.895
...
0.5.1.1229
| Author | SHA1 | Date | |
|---|---|---|---|
| bee2001013 | |||
| facb76c568 | |||
| aaa36382bc | |||
| f66f3551fd | |||
| 2671d9a5ba | |||
| 43174bf294 | |||
| d69b74571a | |||
| 1976805f60 | |||
| d2af24a574 | |||
| a7bb39bc76 | |||
| 915fcb94dc | |||
| eb8ac2b7a5 | |||
| 282c2aa1b2 | |||
| eed72aaa28 | |||
| 0650af48af | |||
| 11ccd9f28a | |||
| ef44e4d149 | |||
| 4a220ddc60 | |||
| 0e60a3d93d | |||
| b0a6c5fece | |||
| b3dfeb2add | |||
| 844eeefb4c | |||
| 6d3e145558 | |||
| 25eaf9abe9 | |||
| b505365762 | |||
| 8402fa7dd9 | |||
| 5098b696e3 | |||
| 0487e72c1f | |||
| 40ba71e1d8 | |||
| 643430553b | |||
| 940bf3635c | |||
| 2d119ce531 | |||
| fe3d96ec9c | |||
| bf4f2f20cf | |||
| 6c8e888807 | |||
| d71fb87a21 | |||
| 6695cad008 | |||
| 1e6e21200a | |||
| 32313461c6 | |||
| 34c562c8e9 | |||
| ec4f66f4f3 | |||
| 3e37ab15fc | |||
| 16e7e408bc | |||
| 016a8a3960 | |||
| 022c7ec50f | |||
| bac9e3524c | |||
| e26d46a5d2 | |||
| 314828a089 | |||
| 4090ec125a | |||
| 6f2c5feef6 | |||
| 2fcb5348c9 | |||
| 964fee639d | |||
| b21a76e3c6 | |||
| 344f2628e3 | |||
| fdb4752e87 | |||
| 9afd71a06c | |||
| 6e25bd6b17 | |||
| fd01dda8b3 | |||
| 24ab0f66f3 | |||
| 4a51e8ecdb | |||
| 32ccac2d15 | |||
| 8ba8489594 | |||
| bf4479490f | |||
| 8846a8be5d | |||
| 99f1a4c58a | |||
| e57b5a5d69 | |||
| e44724a173 | |||
| 1513a3016c | |||
| 81c79d3867 | |||
| 8c3f398753 | |||
| 2a2fae8a97 | |||
| 3c632c0488 | |||
| d0519bbab4 | |||
| f399fee29c | |||
| dc3365b594 | |||
| bf9d90d80f | |||
| 2ce8d04cbd | |||
| 5d2ef0fadf | |||
| 7307b179f0 | |||
| 55c6119853 | |||
| aba98783db | |||
| eb1da15c8f | |||
| 698db97eb1 | |||
| 5a5f2349ea | |||
| 86fa51496f | |||
| bf0e1986d4 | |||
| 20d54611b5 | |||
| 68d37f1dc2 | |||
| 819610f4f1 | |||
| 97b1f5c486 | |||
| f39cb9e1cd | |||
| 127ec9df4a | |||
| 4a74df1dea | |||
| 27c7d3b192 | |||
| 8a207f7b81 | |||
| 7f42eabbd1 | |||
| 3aef7c1223 | |||
| 78faa7430a | |||
| c059986a50 | |||
| 03f7b1b56b | |||
| aead3a0021 | |||
| c908e6f157 | |||
| 26ae33f807 | |||
| c4b27a420f | |||
| cf28f8617d | |||
| ca54abe788 | |||
| ee7045a120 | |||
| d267867381 | |||
| e4acf55656 | |||
| 2cacc2175a | |||
| 50f9e3aeea | |||
| 146323a351 | |||
| b0d95991f1 | |||
| 987c9e462a | |||
| c19bb5bfe0 | |||
| e10ed6e41b | |||
| c01817cd97 | |||
| a3e6c8d635 | |||
| 6c71bb4ab2 | |||
| 991580473d | |||
| c6ca171101 | |||
| 520fbbec5f | |||
| 4521832a76 | |||
| 7f07c7d3d6 | |||
| 63891a8385 | |||
| 1c52e25e9e | |||
| af38e4b7d7 | |||
| e99e6170b8 | |||
| 7550a99166 | |||
| 793c4dcf16 | |||
| d5bc6b0af8 | |||
| 80b2980144 | |||
| 7ebcb952a9 | |||
| 99ec72ce98 | |||
| 3b6bf4a5eb | |||
| e11bf75f20 | |||
| af56ce497f | |||
| 2f90f54cb3 | |||
| a94701a335 | |||
| 67d1525d98 | |||
| cdf5475c3b | |||
| c97a202bd5 | |||
| b11332953c | |||
| d5f8649b80 | |||
| fab173afa3 | |||
| 41fbf70428 | |||
| 1df427b4b4 | |||
| 96a0d3bef6 | |||
| 2d917d7858 | |||
| 2e851e5541 | |||
| b5ff93bf58 | |||
| a10350756f | |||
| 133765131c | |||
| 723a72c1ab | |||
| 6567aeda7d | |||
| 2182ff8e74 | |||
| 25865fb372 | |||
| 81ffeb8d85 | |||
| 19203f3629 | |||
| 82ad8f026b | |||
| 38629a9e34 | |||
| 4499232544 | |||
| 960f30295c | |||
| 415373559c | |||
| a7d22e1620 | |||
| f88ef7284f | |||
| 380023668f | |||
| fb90900045 | |||
| ab511a706a | |||
| 407a2889d1 | |||
| 840775314a | |||
| 85c1d93608 | |||
| 8825886c5c | |||
| e924c00f0d | |||
| 03826d108d | |||
| 1e04b4ad70 | |||
| 3ac37d980c | |||
| da8cec72b4 | |||
| 903345b86a | |||
| 9ed297431d | |||
| e763a1ac88 | |||
| 5b3bdc2f1f | |||
| 65b4a68f7e | |||
| 7ff885e28b | |||
| b936f8a0ca | |||
| 93a01aeb5d | |||
| 0326c0bf14 | |||
| cb91a20844 | |||
| 9fdb7461ba | |||
| 15b51020fc | |||
| 2938621b73 | |||
| 624158f3d4 | |||
| 602a8b9c46 | |||
| 62338358d1 | |||
| 9bc69a81af | |||
| c93b1e4eec | |||
| d30728e2e9 | |||
| 7d86b7a2b9 | |||
| bb31e18ad5 | |||
| 73dc5f6a51 | |||
| 6343a10fbc | |||
| 14beb802b0 | |||
| be71c4ea0b | |||
| 4bd34d46be | |||
| 670ac48516 | |||
| 4ce7c625ea | |||
| 551930f47a | |||
| 04ca753466 | |||
| 4169d5235a | |||
| c417698bf2 | |||
| b6bf6373f2 | |||
| f4326cf9e1 | |||
| cca9d5a240 | |||
| 0af3cee18b | |||
| 8d122f0100 | |||
| 62373485f5 | |||
| 472d83e9fb | |||
| 34231dc480 | |||
| 3f6c9c1204 | |||
| d0e627bd83 | |||
| 6e4074b050 | |||
| 963df01b6f | |||
| c807d4aecb | |||
| e2c9d05a7e | |||
| 6204efd453 | |||
| db083732b0 | |||
| 4974049c0b | |||
| 857192ee6d | |||
| 90d07b1faa | |||
| 4abe02da94 | |||
| adca4b41e2 | |||
| 4d9d8e0be5 | |||
| 546ad81f7c | |||
| 1eff27ad26 | |||
| f540e03a56 | |||
| af925741b4 | |||
| 08bb24cac4 | |||
| 8a9ca83e68 | |||
| e6b6a73f1f | |||
| ceca351b07 | |||
| 4e14730db6 | |||
| 3b0e291df4 | |||
| 6bcb2577f7 | |||
| 54e54b89e9 | |||
| 0085d384fd | |||
| 3b4e40261f | |||
| eb8715867e | |||
| 1bcfdf5648 | |||
| 429de6553b | |||
| fce47648c8 | |||
| f6d44dd1f2 | |||
| 4fe0a54277 | |||
| e8bf21e60c | |||
| 472df39ac8 | |||
| ae94f54a7b | |||
| 8c9f0a0e83 | |||
| cb22baf120 | |||
| 0525aae98a | |||
| 4c87cb2947 | |||
| 076a1ee52e | |||
| 7bd9e97396 | |||
| aa014bcdab | |||
| ee308d29ff | |||
| 35ef2144b7 | |||
| 6dc2a21e8f | |||
| 343c921eb2 | |||
| f54dfba058 | |||
| a7d7ef44a6 | |||
| 793d7444b2 | |||
| eaf4d2ce7b | |||
| 79e615ee46 | |||
| 40353cfd35 | |||
| 2be2ce98e6 | |||
| 6659f65763 | |||
| 5b3ba4db07 | |||
| a05281f4e4 | |||
| 2f95548fb3 | |||
| 96ee9e9265 | |||
| 9026b9d3a9 | |||
| 67e38dd554 | |||
| 7ffef67e42 | |||
| 0d195cfb7f | |||
| 0d3e9c0840 | |||
| 6c600f31f8 | |||
| a05a44aaef | |||
| 31d3a5a09e | |||
| 30293cd261 | |||
| 9a279b3417 | |||
| 4a7ff0ea55 | |||
| fd341e9d33 | |||
| bdacc8dd9e | |||
| ad2c8a1ee8 | |||
| 1f58818a1a | |||
| 09442cbfba | |||
| 91e10d1fa8 | |||
| 80267feb54 | |||
| f4ee2dadb6 | |||
| d698f3d13a | |||
| 82cb845061 | |||
| 940738307b | |||
| df011787e1 | |||
| 0af393a1e4 | |||
| 6504cc033b | |||
| c6ccc5e311 | |||
| 3a8899507f | |||
| 2b7026c4ce | |||
| a3fd822252 | |||
| f9d7fa398f | |||
| 7ffc445bba | |||
| 8e9f6a4bb7 | |||
| 05cb560c8c | |||
| 53a57234a0 | |||
| 6af6bb0959 | |||
| 089aad7398 | |||
| 937e89ce5f | |||
| b0685ffd6b | |||
| 18dee50b96 | |||
| 8dbef09ec9 | |||
| 73b71f56a5 | |||
| ce702405d0 | |||
| 21744a1bef | |||
| 82064f2f02 | |||
| adfebc8c44 | |||
| 2f30e67671 | |||
| 0a80d534eb | |||
| a77c495843 | |||
| 5503aff6b1 | |||
| 7cc4567245 | |||
| e7f29ad0bf | |||
| cdbd101428 | |||
| da650ef16b | |||
| 8abfdd7f8e | |||
| 3c66f99363 | |||
| 251f87a072 |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Generated
+90
-6
@@ -947,6 +947,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/chroma-js": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-1.4.3.tgz",
|
||||
"integrity": "sha512-m33zg9cRLtuaUSzlbMrr7iLIKNzrD4+M6Unt5+9mCu4BhR5NwnRjVKblINCwzcBXooukIgld8DtEncP8qpvbNg=="
|
||||
},
|
||||
"@types/events": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/events/download/@types/events-3.0.0.tgz",
|
||||
@@ -964,6 +969,14 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/md5": {
|
||||
"version": "2.1.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.1.33.tgz",
|
||||
"integrity": "sha512-8+X960EtKLoSblhauxLKy3zzotagjoj3Jt1Tx9oaxUdZEPIBl+mkrUz6PNKpzJgkrKSN9YgkWTA29c0KnLshmA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/minimatch/download/@types/minimatch-3.0.3.tgz",
|
||||
@@ -973,8 +986,7 @@
|
||||
"@types/node": {
|
||||
"version": "12.7.2",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-12.7.2.tgz",
|
||||
"integrity": "sha1-xOY69eiCPOnMPws097mYwhcfDEQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-xOY69eiCPOnMPws097mYwhcfDEQ="
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.0",
|
||||
@@ -2478,6 +2490,11 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||
},
|
||||
"check-types": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/check-types/download/check-types-8.0.3.tgz",
|
||||
@@ -2510,6 +2527,14 @@
|
||||
"integrity": "sha1-oY8eCyacimpdPIbrKYvrFMPde/Y=",
|
||||
"dev": true
|
||||
},
|
||||
"chroma-js": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.0.tgz",
|
||||
"integrity": "sha512-uiRdh4ZZy+UTPSrAdp8hqEdVb1EllLtTHOt5TMaOjJUvi+O54/83Fc5K2ld1P+TJX+dw5B+8/sCgzI6eaur/lg==",
|
||||
"requires": {
|
||||
"cross-env": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"chrome-trace-event": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npm.taobao.org/chrome-trace-event/download/chrome-trace-event-1.0.2.tgz",
|
||||
@@ -3113,6 +3138,52 @@
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"cross-env": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz",
|
||||
"integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==",
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-spawn": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
|
||||
"integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
|
||||
"requires": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"path-key": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz",
|
||||
"integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"requires": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz",
|
||||
"integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==",
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-6.0.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcross-spawn%2Fdownload%2Fcross-spawn-6.0.5.tgz",
|
||||
@@ -3126,6 +3197,11 @@
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"crypto-browserify": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npm.taobao.org/crypto-browserify/download/crypto-browserify-3.12.0.tgz",
|
||||
@@ -6010,8 +6086,7 @@
|
||||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npm.taobao.org/is-buffer/download/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-76ouqdqg16suoTqXsritUf776L4="
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.1.4",
|
||||
@@ -6266,8 +6341,7 @@
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
|
||||
"dev": true
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"isobject": {
|
||||
"version": "3.0.1",
|
||||
@@ -6647,6 +6721,16 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
|
||||
"requires": {
|
||||
"charenc": "~0.0.1",
|
||||
"crypt": "~0.0.1",
|
||||
"is-buffer": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npm.taobao.org/md5.js/download/md5.js-1.3.5.tgz",
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^1.4.3",
|
||||
"@types/md5": "^2.1.33",
|
||||
"chroma-js": "^2.1.0",
|
||||
"core-js": "^2.6.5",
|
||||
"echarts": "^4.2.1",
|
||||
"element-ui": "^2.11.1",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"p-wait-for": "^3.1.0",
|
||||
"v-charts": "^1.19.0",
|
||||
|
||||
+6
-2
@@ -3,9 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<!--meta name="viewport" content="width=device-width,initial-scale=1.0"-->
|
||||
<meta name="viewport" content="width=1024">
|
||||
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="icon" href="<%= BASE_URL %>logo@32px.png">
|
||||
<title>Veracross Analyzer</title>
|
||||
</head>
|
||||
|
||||
@@ -24,6 +25,9 @@
|
||||
<!-- ElementUI -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap" rel="stylesheet">
|
||||
</body>
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 60 KiB |
@@ -5,7 +5,7 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 100px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
#app-content
|
||||
@@ -25,7 +25,63 @@
|
||||
--assignment-type-3: #ff9900;
|
||||
--assignment-type-4: #b02b02;
|
||||
|
||||
--font: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
//--font: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
--font: -apple-system, Nunito Sans, Avenir, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.dark
|
||||
{
|
||||
--dark-layer-1: #383838;
|
||||
--dark-layer-2: #525252;
|
||||
--dark-layer-3: #6c6c6c;
|
||||
--dark-foreground: #e9e9e9;
|
||||
|
||||
background: var(--dark-layer-1) !important;
|
||||
|
||||
div, ul
|
||||
{
|
||||
background: var(--dark-layer-2) !important;
|
||||
color: var(--dark-foreground) !important;
|
||||
}
|
||||
|
||||
span, button
|
||||
{
|
||||
color: var(--dark-foreground) !important;
|
||||
}
|
||||
|
||||
.el-card
|
||||
{
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
// Overall
|
||||
#overall, #overall-course, .overall-span, #app-content
|
||||
{
|
||||
background: var(--dark-layer-1) !important;
|
||||
}
|
||||
|
||||
// Course card
|
||||
.entry-box, .none .unread-number {background: #a1a1a1 !important}
|
||||
.entry-box.max {background-color: #949494 !important}
|
||||
.entry-box.percent {background-color: #a7a490 !important}
|
||||
.course-name {color: #cffff6 !important}
|
||||
|
||||
.course-card-content.expand, .assignment-entry, .unread-row,
|
||||
.unread-row .el-col, #assignment-type-head, .course-page-graph.el-col
|
||||
{
|
||||
background-color: var(--dark-layer-3) !important;
|
||||
}
|
||||
|
||||
.overall-span.el-col, .course-page-graph.el-col
|
||||
{
|
||||
div, span
|
||||
{
|
||||
background: #f9f9f9 !important;
|
||||
color: var(--dark-layer-1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ##############
|
||||
@@ -83,3 +139,8 @@ div.el-card.course-card > div.el-card__body
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item
|
||||
{
|
||||
font-family: Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
+35
-52
@@ -3,40 +3,14 @@ import Login from '@/components/login/login';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Overall from '@/pages/overall/overall.vue';
|
||||
import Constants from '@/constants';
|
||||
import JsonUtils from '@/utils/json-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import {HttpUtils} from '@/utils/http-utils';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import Loading from '@/components/loading/loading.vue';
|
||||
import {HttpUtils} from '@/logic/utils/http-utils';
|
||||
import Loading from '@/components/overlays/loading.vue';
|
||||
import CoursePage from '@/pages/course/course-page.vue';
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import Course from '@/logic/course';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
*/
|
||||
export interface Assignment
|
||||
{
|
||||
id: number
|
||||
scoreId: number
|
||||
type: string
|
||||
typeId: number
|
||||
description: string
|
||||
date: Date
|
||||
complete: string
|
||||
include: boolean
|
||||
display: boolean
|
||||
|
||||
unread: boolean
|
||||
|
||||
scoreMax: number
|
||||
score: number
|
||||
|
||||
gradingPeriod: number
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage},
|
||||
})
|
||||
@@ -51,14 +25,14 @@ export default class App extends Vue
|
||||
// List of course that should be displayed
|
||||
public filteredCourses: Course[] = [];
|
||||
|
||||
// Currently selected tab
|
||||
// The currently selected tab
|
||||
public selectedTab: string = 'overall';
|
||||
|
||||
// Are the course assignments loaded from the server.
|
||||
public assignmentsReady: boolean = false;
|
||||
|
||||
// Token
|
||||
public token: string = '';
|
||||
public user: LoginUser = null as any;
|
||||
|
||||
// Loading text
|
||||
public loading: string = '';
|
||||
@@ -87,9 +61,9 @@ export default class App extends Vue
|
||||
/**
|
||||
* This is called when the user logs in.
|
||||
*
|
||||
* @param token Authorization token
|
||||
* @param user Authorization user
|
||||
*/
|
||||
public onLogin(token: string)
|
||||
public onLogin(user: LoginUser)
|
||||
{
|
||||
// Hide login bar
|
||||
this.showLogin = false;
|
||||
@@ -97,18 +71,18 @@ export default class App extends Vue
|
||||
// Show loading message
|
||||
this.logLoading('1. Logging in...');
|
||||
|
||||
// Store token
|
||||
this.token = token;
|
||||
// Store user
|
||||
this.user = user;
|
||||
|
||||
// Assign token to http client
|
||||
App.http.token = token;
|
||||
// Assign user to http client
|
||||
App.http.user = user;
|
||||
|
||||
// Load data
|
||||
this.loadCoursesAfterLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load courses data after login.
|
||||
* Load courses data after logging in.
|
||||
*/
|
||||
public loadCoursesAfterLogin()
|
||||
{
|
||||
@@ -178,32 +152,41 @@ export default class App extends Vue
|
||||
// Loop through all the courses
|
||||
for (const course of this.filteredCourses)
|
||||
{
|
||||
let termGrade = +GPAUtils.getTotalMeanAverage(course.computed.termAssignments[Constants.CURRENT_TERM]).toFixed(2);
|
||||
for (const i of [0, 1, 2, 3])
|
||||
{
|
||||
const cookieIndex = `va.grading.${i}.${course.assignmentsId}`;
|
||||
|
||||
// Check if total-average grade is the same with percent-type grade
|
||||
if (course.rawNumericGrade == termGrade)
|
||||
{
|
||||
course.grading = {method: 'TOTAL_MEAN', weightingMap: {}};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request grading scheme for this course
|
||||
App.http.post('/grading', {'assignmentsId': course.assignmentsId}).then(response =>
|
||||
// Check if already exist in cookies
|
||||
if (this.$cookies.isKey(cookieIndex))
|
||||
{
|
||||
course.termGrading[i] = {method: 'TOTAL_MEAN', weightingMap: {}};
|
||||
continue;
|
||||
}
|
||||
|
||||
// Request grading scheme for this course at this grading period
|
||||
App.http.post('/grading/term', {assignmentsId: course.assignmentsId, term: i}).then(resp =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
if (resp.success)
|
||||
{
|
||||
// Add it to course
|
||||
course.grading = response.data;
|
||||
course.termGrading[i] = resp.data;
|
||||
|
||||
// If it's total_mean, cache it to cookies
|
||||
// This is because only percent_type can update over time
|
||||
if (course.termGrading[i].method == 'TOTAL_MEAN')
|
||||
{
|
||||
this.$cookies.set(cookieIndex, 'TOTAL_MEAN', 'd');
|
||||
}
|
||||
}
|
||||
else throw new Error(response.data);
|
||||
else throw new Error(resp.data);
|
||||
})
|
||||
.catch(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for done
|
||||
pWaitFor(() => this.filteredCourses.every(c => c.grading != undefined)).then(() =>
|
||||
pWaitFor(() => this.filteredCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
|
||||
{
|
||||
// When the assignments are ready
|
||||
this.assignmentsReady = true;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div id="app" class="theme-default">
|
||||
<login v-if="showLogin" v-on:login:token="onLogin"></login>
|
||||
<navigation :courses="filteredCourses"
|
||||
<login v-if="showLogin" v-on:login:user="onLogin"/>
|
||||
<navigation v-if="user != null"
|
||||
:courses="filteredCourses"
|
||||
:activeIndex.sync="selectedTab"
|
||||
:user="user"
|
||||
@sign-out="signOut" @select-time="selectTime">
|
||||
</navigation>
|
||||
|
||||
@@ -15,9 +17,9 @@
|
||||
</course-page>
|
||||
</div>
|
||||
|
||||
<loading v-if="loading !== ''" :text="loading" :error="loadingError"></loading>
|
||||
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./app.ts" lang="ts"></script>
|
||||
<style src="./app.scss" lang="scss"></style>
|
||||
<style src="./app.scss" lang="scss"/>
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
}
|
||||
|
||||
// Logo image
|
||||
#login-logo-image
|
||||
{
|
||||
width: 80%;
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
// Parent overlay
|
||||
.login-overlay
|
||||
{
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import App from '@/components/app/app';
|
||||
import VersionUtils from '@/utils/version-utils';
|
||||
import VersionUtils from '@/logic/utils/version-utils';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import Maintenance from '@/components/overlays/maintenance.vue';
|
||||
|
||||
/**
|
||||
* This component handles user login, and obtains data from the server.
|
||||
*/
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
@Component({components: {Maintenance}})
|
||||
export default class Login extends Vue
|
||||
{
|
||||
public username: any = '';
|
||||
public password: any = '';
|
||||
username = '';
|
||||
password = '';
|
||||
|
||||
public loading: boolean = false;
|
||||
public error: String = '';
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
disableInput = false;
|
||||
|
||||
maintenance = '';
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
created()
|
||||
{
|
||||
// Check login cookies
|
||||
if (this.$cookies.isKey('va.token'))
|
||||
{
|
||||
// Check cookies version
|
||||
if (this.needToUpdateCookies())
|
||||
{
|
||||
console.log('Version Updated! Clearing cookies...');
|
||||
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
if (this.needToUpdateCookies()) this.clearCookies();
|
||||
else
|
||||
{
|
||||
// Already contains valid token / TODO: Validate
|
||||
// TODO: Update token each access
|
||||
this.$emit('login:token', this.$cookies.get('va.token'));
|
||||
// Show loading
|
||||
this.disableInput = this.loading = true;
|
||||
|
||||
// Login with token
|
||||
this.login('/login/token', {token: this.$cookies.get('va.token')});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('Cookies doesn\'t exist');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +47,7 @@ export default class Login extends Vue
|
||||
*
|
||||
* @returns boolean Need to clear cookies or not
|
||||
*/
|
||||
public needToUpdateCookies(): boolean
|
||||
needToUpdateCookies(): boolean
|
||||
{
|
||||
// Version doesn't exist
|
||||
if (!this.$cookies.isKey('va.version')) return true;
|
||||
@@ -61,50 +57,83 @@ export default class Login extends Vue
|
||||
}
|
||||
|
||||
/**
|
||||
* On click, sends username and password to the server.
|
||||
* When the user clicks, send the username and password to the server.
|
||||
*/
|
||||
public onLoginClick()
|
||||
onLoginClick()
|
||||
{
|
||||
// Make login button loading
|
||||
this.loading = true;
|
||||
|
||||
// Format it
|
||||
this.username = this.username.toLowerCase().replace(/ /g, '').replace(/@.*/g, '');
|
||||
|
||||
// Login
|
||||
this.login('/login', {username: this.username, password: this.password});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the login request and process the response
|
||||
*
|
||||
* @param url Posting url
|
||||
* @param data Data to be posted
|
||||
*/
|
||||
login(url: string, data: any)
|
||||
{
|
||||
// Fetch request
|
||||
App.http.post('/login', {username: this.username, password: this.password})
|
||||
App.http.post(url, data)
|
||||
.then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Maintenance
|
||||
if (response.data.maintenance)
|
||||
{
|
||||
this.maintenance = response.data.maintenance;
|
||||
return;
|
||||
}
|
||||
|
||||
// Save token to cookies
|
||||
this.$cookies.set('va.token', response.data, '27d');
|
||||
this.$cookies.set('va.token', response.data.user.token, '27d');
|
||||
this.$cookies.set('va.version', Constants.VERSION, '27d');
|
||||
|
||||
// Call custom event with token
|
||||
this.$emit('login:token', response.data);
|
||||
// Call a custom event with the token
|
||||
this.$emit('login:user', new LoginUser(response.data.user));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message
|
||||
this.error = response.data;
|
||||
// Login expired -> clear cookies
|
||||
if (response.data == 'Error: Login expired')
|
||||
{
|
||||
this.clearCookies();
|
||||
}
|
||||
|
||||
// Allow the user to retry
|
||||
this.loading = false;
|
||||
// Show error message & allow user to retry
|
||||
this.error = response.data;
|
||||
this.disableInput = this.loading = false;
|
||||
}
|
||||
})
|
||||
.catch(err =>
|
||||
{
|
||||
alert(err);
|
||||
|
||||
// Allow the user to retry
|
||||
this.loading = false;
|
||||
// Show error message & allow user to retry
|
||||
this.error = err;
|
||||
this.disableInput = this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the user hits enter in the input boxes.
|
||||
* This is called when the user hits enter on the input boxes.
|
||||
*/
|
||||
public onEnter()
|
||||
onEnter()
|
||||
{
|
||||
this.onLoginClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cookies
|
||||
*/
|
||||
clearCookies()
|
||||
{
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
<div id="login" class="login-overlay">
|
||||
<div class="login-vertical-center">
|
||||
<div class="login-panel">
|
||||
<img alt="Vue logo" src="../../assets/logo.png">
|
||||
<img id="login-logo-image" alt="logo" src="../../assets/logo.png">
|
||||
|
||||
<h1>Veracross Analyzer</h1>
|
||||
<form id="login-form">
|
||||
<el-input v-model="username"
|
||||
placeholder="School Username"
|
||||
placeholder="SJP Username (Eg. flast21)"
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="password"
|
||||
placeholder="Veracross Password"
|
||||
placeholder="SJP Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
|
||||
@@ -25,8 +27,10 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Maintenance v-if="maintenance" :message="maintenance"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login.ts" lang="ts"></script>
|
||||
<style src="./login.scss" lang="scss"></style>
|
||||
<style src="./login.scss" lang="scss"/>
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
#nav-avatar
|
||||
{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
#sign-out-button
|
||||
{
|
||||
// Float right
|
||||
@@ -31,7 +38,7 @@
|
||||
{
|
||||
// Float right
|
||||
position: absolute;
|
||||
right: 110px;
|
||||
right: 80px;
|
||||
|
||||
// Margins
|
||||
margin-top: 12px;
|
||||
@@ -48,24 +55,49 @@
|
||||
height: 60px;
|
||||
|
||||
// Center text
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
// Margins
|
||||
margin-left: 20px;
|
||||
margin-right: 8px;
|
||||
|
||||
// Font
|
||||
font: bold 16px arial;
|
||||
display: inline-flex;
|
||||
|
||||
// Shadow effect
|
||||
-webkit-background-clip: text;
|
||||
background-color: #b1b1b1;
|
||||
color: transparent;
|
||||
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.85);
|
||||
|
||||
// Make it non-clickable
|
||||
pointer-events: none;
|
||||
|
||||
#nav-logo
|
||||
{
|
||||
height: 70%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#nav-logo-text
|
||||
{
|
||||
// Color
|
||||
color: #6bbeff !important;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(90,177,239,1) 0%,
|
||||
rgba(25,212,174,1) 100%) !important;
|
||||
|
||||
// Font
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#nav-logo-text.logo-text
|
||||
{
|
||||
// Override the background
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
-webkit-background-clip: text !important;
|
||||
}
|
||||
|
||||
#nav-logo-version
|
||||
{
|
||||
color: #a7a7a7;
|
||||
margin-left: 5px;
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-course
|
||||
@@ -76,7 +108,7 @@
|
||||
bottom: 0;
|
||||
left: 25%;
|
||||
padding-top: 2px;
|
||||
box-shadow: 0 -2px 9px 0 #ecebeb;
|
||||
box-shadow: 0 -2px 9px 0 #00000029;
|
||||
}
|
||||
|
||||
footer
|
||||
@@ -85,6 +117,8 @@ footer
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#prev-course
|
||||
@@ -95,7 +129,9 @@ footer
|
||||
top: 61px;
|
||||
left: 25%;
|
||||
padding-bottom: 2px;
|
||||
box-shadow: 0 2px 9px 0 #ecebeb;
|
||||
box-shadow: 0 2px 9px 0 #00000029;
|
||||
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.nav-course-operations
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import App from '@/components/app/app';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import Course from '@/logic/course';
|
||||
import Constants from '@/constants';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
|
||||
/**
|
||||
* This component is the top navigation bar
|
||||
*/
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
@Component
|
||||
export default class Navigation extends Vue
|
||||
{
|
||||
@Prop({required: true}) activeIndex: string;
|
||||
|
||||
@Prop({required: true}) courses: Course[];
|
||||
@Prop({required: true}) user: LoginUser;
|
||||
|
||||
private gradingPeriod: string = 'All Year';
|
||||
|
||||
// Instance
|
||||
public static instance: Navigation;
|
||||
static instance: Navigation;
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
created()
|
||||
{
|
||||
// Check selected time
|
||||
if (!this.$cookies.isKey('va.grading-period'))
|
||||
@@ -38,7 +38,7 @@ export default class Navigation extends Vue
|
||||
/**
|
||||
* This is called when the instance is loaded.
|
||||
*/
|
||||
public mounted()
|
||||
mounted()
|
||||
{
|
||||
// Set instance
|
||||
Navigation.instance = this;
|
||||
@@ -66,7 +66,7 @@ export default class Navigation extends Vue
|
||||
};
|
||||
}
|
||||
|
||||
public formatCourseIndex(course: Course)
|
||||
formatCourseIndex(course: Course)
|
||||
{
|
||||
return CourseUtils.formatTabIndex(course);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export default class Navigation extends Vue
|
||||
* @param index The index selected
|
||||
* @param indexPath The path of the index
|
||||
*/
|
||||
public onSelect(index: string, indexPath: string)
|
||||
onSelect(index: string, indexPath: string)
|
||||
{
|
||||
// Update active index
|
||||
this.updateIndex(index);
|
||||
@@ -89,7 +89,7 @@ export default class Navigation extends Vue
|
||||
* @param newIndex New index
|
||||
* @param history Record in history or not (Default true)
|
||||
*/
|
||||
public updateIndex(newIndex: string, history?: boolean)
|
||||
updateIndex(newIndex: string, history?: boolean)
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('update:activeIndex', newIndex);
|
||||
@@ -106,6 +106,9 @@ export default class Navigation extends Vue
|
||||
|
||||
// Update title
|
||||
document.title = 'Veracross Analyzer - ' + this.getTitle(newIndex);
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +116,7 @@ export default class Navigation extends Vue
|
||||
*
|
||||
* @param index Index
|
||||
*/
|
||||
public getTitle(index: string)
|
||||
getTitle(index: string)
|
||||
{
|
||||
// Course
|
||||
if (index.startsWith('course'))
|
||||
@@ -130,7 +133,7 @@ export default class Navigation extends Vue
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public nextCourse(indexOffset: number)
|
||||
nextCourse(indexOffset: number)
|
||||
{
|
||||
// Set tab to the next index
|
||||
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset)))
|
||||
@@ -141,7 +144,7 @@ export default class Navigation extends Vue
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public findNextCourse(indexOffset: number)
|
||||
findNextCourse(indexOffset: number)
|
||||
{
|
||||
return this.findCourse(this.activeIndex.split('/')[1], indexOffset);
|
||||
}
|
||||
@@ -152,7 +155,7 @@ export default class Navigation extends Vue
|
||||
* @param courseId Course ID
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public findCourse(courseId: string, indexOffset: number)
|
||||
findCourse(courseId: string, indexOffset: number)
|
||||
{
|
||||
// Find current course index
|
||||
let courseIndex = this.courses.findIndex(c => c.id == +courseId);
|
||||
@@ -166,21 +169,40 @@ export default class Navigation extends Vue
|
||||
*
|
||||
* @param command Term 1, Term 2, All Year, etc.
|
||||
*/
|
||||
public selectGradingPeriod(command: string)
|
||||
selectGradingPeriod(command: string)
|
||||
{
|
||||
this.gradingPeriod = command;
|
||||
this.$cookies.set('va.grading-period', command, '10y');
|
||||
|
||||
// Call event
|
||||
this.$emit('select-time', this.getSelectedGradingPeriod());
|
||||
this.$emit('select-time', this.getSelectedTerm());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code for selected time
|
||||
*/
|
||||
public getSelectedGradingPeriod(): number
|
||||
getSelectedTerm(): number
|
||||
{
|
||||
if (this.gradingPeriod == 'All Year') return -1;
|
||||
else return +this.gradingPeriod.replace('Term ', '') - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar dropdown menu event
|
||||
*
|
||||
* @param cmd Command: sign-out
|
||||
*/
|
||||
onAvatarMenu(cmd: string)
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case 'sign-out':
|
||||
{
|
||||
this.$emit('sign-out');
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get version() {return Constants.VERSION}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
:default-active="activeIndex" @select="onSelect">
|
||||
|
||||
<div id="nav-title">
|
||||
Veracross Analyzer
|
||||
<img id="nav-logo" alt="logo" src="../../assets/logo.png">
|
||||
<span id="nav-logo-text" class="logo-text">Veracross Analyzer</span>
|
||||
<span id="nav-logo-version">v{{version}}</span>
|
||||
</div>
|
||||
|
||||
<el-menu-item index="overall">Overall</el-menu-item>
|
||||
@@ -19,7 +21,7 @@
|
||||
<!-- Grading period selection -->
|
||||
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
|
||||
<el-button type="primary" size="medium">
|
||||
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item>
|
||||
@@ -30,7 +32,16 @@
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<el-button @click="$emit('sign-out')" id="sign-out-button" type="text">Sign Out</el-button>
|
||||
<!-- User avatar -->
|
||||
<el-dropdown id="nav-avatar" trigger="click" @command="onAvatarMenu">
|
||||
<el-avatar :src="user.avatarUrl"/>
|
||||
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item style="text-align: center">{{user.nickname}}</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item icon="el-icon-switch-button" command="sign-out" divided>Sign Out</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</el-menu>
|
||||
|
||||
<!-- Previous course / Next course (Only when the page is courses) -->
|
||||
@@ -44,8 +55,11 @@
|
||||
▼ NEXT COURSE ▼
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Back to top -->
|
||||
<el-backtop style="box-shadow: rgba(0, 0, 0, 0.23) 0 3px 11px 0;"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./navigation.ts" lang="ts"></script>
|
||||
<style src="./navigation.scss" lang="scss"></style>
|
||||
<style src="./navigation.scss" lang="scss"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="loading">
|
||||
<div id="text" :class="message()">
|
||||
{{message()}}
|
||||
<div id="text" :class="message">
|
||||
{{message}}
|
||||
|
||||
<div v-if="!error" class="el-loading-spinner">
|
||||
<svg viewBox="25 25 50 50" class="circular">
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="error" id="error-details">
|
||||
<span v-for="(line, index) in getText()" :style="`font-size: ${-index === 0 ? 16 : 12}px;`">
|
||||
<span v-for="(line, index) in split" :style="`font-size: ${-index === 0 ? 16 : 12}px;`">
|
||||
{{line}}
|
||||
<br>
|
||||
</span>
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!error" id="details">
|
||||
<span v-for="(line, index) in getText()" :style="`font-size: ${16 - getText().length + index}px;`">
|
||||
<span v-for="(line, index) in split" :style="`font-size: ${16 - split.length + index}px;`">
|
||||
{{line}}
|
||||
<br>
|
||||
</span>
|
||||
@@ -29,21 +29,19 @@
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
@Component
|
||||
export default class Loading extends Vue
|
||||
{
|
||||
@Prop({required: true}) text: string;
|
||||
|
||||
@Prop({required: true}) error: boolean;
|
||||
|
||||
getText()
|
||||
get split()
|
||||
{
|
||||
return this.text.split('\n');
|
||||
}
|
||||
|
||||
message()
|
||||
get message()
|
||||
{
|
||||
return this.error ? 'Error' : 'Loading';
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div id="maintenance">
|
||||
<div id="maintenance-content">
|
||||
<h1>We’ll be back soon!</h1>
|
||||
<div>
|
||||
<p>Sorry for the inconvenience but we’re performing some maintenance at the moment.
|
||||
We’ll be back online shortly!</p>
|
||||
|
||||
<p>What went wrong: {{json.reason}}</p>
|
||||
|
||||
<p>Estimated fix: {{json.eta}}</p>
|
||||
|
||||
<p>— An Average SJP Junior</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Maintenance extends Vue
|
||||
{
|
||||
@Prop({required: true}) message: any;
|
||||
|
||||
get json()
|
||||
{
|
||||
return JSON.parse(this.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#maintenance
|
||||
{
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
#maintenance-content
|
||||
{
|
||||
font: 20px Helvetica, sans-serif;
|
||||
color: #333;
|
||||
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin: 150px;
|
||||
|
||||
h1
|
||||
{
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
a {color: #dc8100; text-decoration: none;}
|
||||
a:hover {color: #333; text-decoration: none;}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+29
-9
@@ -1,20 +1,23 @@
|
||||
/**
|
||||
* This class stores the static constants.
|
||||
*/
|
||||
import {findLastIndex} from '@/logic/utils/general-utils';
|
||||
|
||||
export default class Constants
|
||||
{
|
||||
/** Base url for api access */
|
||||
public static API_URL: string = 'https://va.hydev.org/api';
|
||||
static API_URL: string = 'https://va.hydev.org/api';
|
||||
// static API_URL: string = 'http://localhost:24021/api';
|
||||
|
||||
/** Current version */
|
||||
public static VERSION: string = '0.4.1.895';
|
||||
static VERSION: string = '0.5.1.1229';
|
||||
|
||||
/** Minimum version that still supports the same cookies */
|
||||
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561';
|
||||
/** The minimum version that still supports the same cookies */
|
||||
static MIN_SUPPORTED_VERSION: string = '0.4.6.1087';
|
||||
|
||||
public static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
|
||||
static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
|
||||
|
||||
public static SPLASH: string =
|
||||
static SPLASH: string =
|
||||
'. , ,---. | \n' +
|
||||
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
|
||||
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
|
||||
@@ -24,7 +27,7 @@ export default class Constants
|
||||
` Github: ${Constants.GITHUB}`;
|
||||
|
||||
// Graph Theme
|
||||
public static THEME =
|
||||
static THEME =
|
||||
{
|
||||
// Colors
|
||||
colors:
|
||||
@@ -56,6 +59,23 @@ export default class Constants
|
||||
};
|
||||
|
||||
// Terms (TODO: Actually get the terms dynamically
|
||||
public static TERMS = [new Date('Sep 04 2019'), new Date('Nov 01 2019'), new Date('Feb 02 2020')];
|
||||
public static CURRENT_TERM = 1;
|
||||
static TERMS =
|
||||
[
|
||||
new Date('Sep 04 2019'),
|
||||
new Date('Nov 03 2019'),
|
||||
new Date('Jan 19 2020'),
|
||||
new Date('Mar 22 2020'),
|
||||
new Date('Jun 05 2020'),
|
||||
];
|
||||
static CURRENT_TERM = Constants.getTerm(new Date());
|
||||
|
||||
/**
|
||||
* Find out the specified date is in which term
|
||||
*
|
||||
* @param date
|
||||
*/
|
||||
static getTerm(date: Date)
|
||||
{
|
||||
return findLastIndex(Constants.TERMS, d => d <= date);
|
||||
}
|
||||
}
|
||||
|
||||
+190
-60
@@ -1,10 +1,83 @@
|
||||
import App, {Assignment} from '@/components/app/app';
|
||||
import JsonUtils from '@/utils/json-utils';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import CacheUtils from '@/logic/utils/cache-utils';
|
||||
import Constants from '@/constants';
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
*/
|
||||
export class Assignment
|
||||
{
|
||||
id: number;
|
||||
scoreId: number;
|
||||
type: string;
|
||||
typeId: number;
|
||||
description: string;
|
||||
time: number;
|
||||
complete: string;
|
||||
include: boolean;
|
||||
display: boolean;
|
||||
|
||||
unread: boolean;
|
||||
|
||||
scoreMax: number;
|
||||
score: number;
|
||||
|
||||
gradingPeriod: number;
|
||||
|
||||
/**
|
||||
* Construct assignment with json object
|
||||
*
|
||||
* @param json Json object
|
||||
*/
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id = json.assignment_id;
|
||||
this.scoreId = json.score_id;
|
||||
this.type = json.assignment_type;
|
||||
this.typeId = json.assignment_type_id;
|
||||
this.description = json.assignment_description;
|
||||
this.time = new Date(json._date).getTime();
|
||||
this.complete = json.completion_status;
|
||||
this.include = json.include_in_calculated_grade == 1;
|
||||
this.display = json.display_grade == 1;
|
||||
|
||||
this.unread = json.is_unread == 1;
|
||||
|
||||
this.scoreMax = json.maximum_score;
|
||||
this.score = +json.raw_score;
|
||||
|
||||
this.gradingPeriod = +json.grading_period.replace('Quarter ', '') - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graded or not
|
||||
*/
|
||||
get graded()
|
||||
{
|
||||
return this.complete == 'Complete';
|
||||
}
|
||||
}
|
||||
|
||||
export interface AssignmentType
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
|
||||
weight: number
|
||||
scoreMax: number
|
||||
score: number
|
||||
percent: number
|
||||
assignmentCount: number
|
||||
}
|
||||
|
||||
export interface Grading
|
||||
{
|
||||
method: string
|
||||
weightingMap: {[index: string]: number}
|
||||
}
|
||||
|
||||
export default class Course
|
||||
{
|
||||
@@ -21,17 +94,10 @@ export default class Course
|
||||
level: string;
|
||||
scaleUp: number;
|
||||
|
||||
grading:
|
||||
{
|
||||
method: string
|
||||
weightingMap: {[index: string]: number}
|
||||
};
|
||||
termGrading: Grading[];
|
||||
termAssignments: Assignment[][];
|
||||
|
||||
computed:
|
||||
{
|
||||
termAssignments: Assignment[][]
|
||||
allYearGrade: number
|
||||
};
|
||||
cache: CacheUtils = new CacheUtils();
|
||||
|
||||
/**
|
||||
* Construct a course with a course json object
|
||||
@@ -49,6 +115,13 @@ export default class Course
|
||||
this.rawLetterGrade = courseJson.letterGrade;
|
||||
this.rawNumericGrade = courseJson.numericGrade;
|
||||
|
||||
// Other api issue
|
||||
if (this.rawLetterGrade == '')
|
||||
{
|
||||
this.rawNumericGrade = undefined;
|
||||
this.rawLetterGrade = undefined;
|
||||
}
|
||||
|
||||
// Level and scaleUp
|
||||
let level = CourseUtils.detectLevel(this.name);
|
||||
if (level != undefined)
|
||||
@@ -58,7 +131,7 @@ export default class Course
|
||||
}
|
||||
else this.level = 'Unknown';
|
||||
|
||||
this.grading = courseJson.grading;
|
||||
this.termGrading = new Array(4).fill(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,20 +143,16 @@ export default class Course
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
this.rawAssignments = JsonUtils.filterAssignments(data);
|
||||
this.rawAssignments = data.assignments.map((a: any) => new Assignment(a));
|
||||
|
||||
// Sort by date (Latest is at 0)
|
||||
this.rawAssignments.sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||
this.rawAssignments.sort((a, b) => b.time - a.time);
|
||||
|
||||
// Filter assignments into terms
|
||||
let termAssignments: Assignment[][] = [[], [], [], []];
|
||||
let currentTerm = 0;
|
||||
this.termAssignments = [[], [], [], []];
|
||||
|
||||
// Loop through it by time order
|
||||
this.rawAssignments.forEach(a => termAssignments[a.gradingPeriod].push(a));
|
||||
|
||||
// Set computed data
|
||||
this.computed = {termAssignments: termAssignments, allYearGrade: -1};
|
||||
this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +167,7 @@ export default class Course
|
||||
if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;
|
||||
|
||||
// Skip courses without graded assignments
|
||||
if (this.assignments.filter(a => a.complete == 'Complete').length == 0) return false;
|
||||
if (this.assignments.length == 0) return false;
|
||||
|
||||
// Skip if there are no grading scale
|
||||
// if (course.grading.method == 'NOT_GRADED') return;
|
||||
@@ -108,20 +177,40 @@ export default class Course
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the selected time
|
||||
* Get currently selected grading periods
|
||||
*/
|
||||
get gradingPeriods(): number[]
|
||||
{
|
||||
return this.cache.get('GradingPeriods', () =>
|
||||
{
|
||||
let timeCode = Navigation.instance.getSelectedTerm();
|
||||
return (timeCode == -1 ? [0, 1, 2, 3] : [timeCode]).filter(term =>
|
||||
this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the selected grading periods
|
||||
*/
|
||||
get assignments(): Assignment[]
|
||||
{
|
||||
let timeCode = Navigation.instance.getSelectedGradingPeriod();
|
||||
return this.gradingPeriods
|
||||
.flatMap(term => this.termAssignments[term])
|
||||
.filter(a => a.graded)
|
||||
.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
|
||||
// All year
|
||||
if (timeCode == -1)
|
||||
{
|
||||
return this.rawAssignments;
|
||||
}
|
||||
/**
|
||||
* Get assignments before a certain date
|
||||
*
|
||||
* @param time
|
||||
*/
|
||||
getAssignmentsBefore(time: number): {term: number, assignments: Assignment[]}
|
||||
{
|
||||
let term = Constants.getTerm(new Date(time));
|
||||
let assignments = this.assignments.filter(a => a.gradingPeriod == term && a.time <= time);
|
||||
|
||||
// Specific time
|
||||
return this.computed.termAssignments[timeCode];
|
||||
return {term: term, assignments: assignments}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,39 +218,80 @@ export default class Course
|
||||
*/
|
||||
get letterGrade(): string
|
||||
{
|
||||
// Cached
|
||||
if (this.rawLetterGrade != undefined)
|
||||
return this.rawLetterGrade;
|
||||
return this.cache.get('LetterGrade', () =>
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGrade);
|
||||
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGrade);
|
||||
// Scale not found
|
||||
return scale == undefined ? '--' : scale.letter;
|
||||
})
|
||||
}
|
||||
|
||||
// Scale not found
|
||||
if (scale == undefined) return this.rawLetterGrade = '--';
|
||||
|
||||
// Return
|
||||
return this.rawLetterGrade = scale.letter;
|
||||
get numericGrade()
|
||||
{
|
||||
return this.gradingPeriods
|
||||
.map(term => this.numericGradeTerm(term)).reduce((p, v) => p + v)
|
||||
/ this.gradingPeriods.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric grade
|
||||
* Get numeric grade by term
|
||||
*
|
||||
* @param term
|
||||
*/
|
||||
get numericGrade(): number
|
||||
numericGradeTerm(term: number): number
|
||||
{
|
||||
// Cached
|
||||
if (this.rawNumericGrade != undefined)
|
||||
return this.rawNumericGrade;
|
||||
|
||||
// Calculate
|
||||
if (this.grading.method == 'PERCENT_TYPE')
|
||||
return this.cache.get('NumericGrade' + term, () =>
|
||||
{
|
||||
return this.rawNumericGrade = GPAUtils.getPercentTypeAverage(this, this.assignments);
|
||||
}
|
||||
if (this.grading.method == 'TOTAL_MEAN')
|
||||
{
|
||||
return this.rawNumericGrade = GPAUtils.getTotalMeanAverage(this.assignments);
|
||||
}
|
||||
// Calculate
|
||||
if (this.termGrading[term].method == 'PERCENT_TYPE')
|
||||
{
|
||||
return GPAUtils.getPercentTypeAverage(this.termGrading[term], this.termAssignments[term]);
|
||||
}
|
||||
else if (this.termGrading[term].method == 'TOTAL_MEAN')
|
||||
{
|
||||
return GPAUtils.getTotalMeanAverage(this.termAssignments[term]);
|
||||
}
|
||||
else return -1;
|
||||
})
|
||||
}
|
||||
|
||||
return -1;
|
||||
/**
|
||||
* Get assignment types
|
||||
*/
|
||||
get assignmentTypes(): AssignmentType[]
|
||||
{
|
||||
return this.cache.get('AssignmentTypes', () =>
|
||||
{
|
||||
// Get all types
|
||||
let types = this.assignments.map(a => a.type);
|
||||
|
||||
// Remove duplicates
|
||||
types = types.filter((type, i, a) => a.indexOf(type) == i);
|
||||
|
||||
// Get total possible score for weight calculation
|
||||
let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
|
||||
// For every type...
|
||||
return types.map(type =>
|
||||
{
|
||||
// Get assignments of the type
|
||||
let typeAssignments = this.assignments.filter(a => a.type == type);
|
||||
|
||||
// Count scores and max scores
|
||||
let score = typeAssignments.reduce((sum, a) => sum + a.score, 0);
|
||||
let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
|
||||
// Calculate weight
|
||||
let weight = this.termGrading[0].method == 'PERCENT_TYPE'
|
||||
? this.termGrading[0].weightingMap[type] : scoreMax / totalScoreMax;
|
||||
|
||||
// Return
|
||||
return {name: type, id: typeAssignments[0].typeId, weight: +(weight * 100).toFixed(2),
|
||||
scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2),
|
||||
assignmentCount: typeAssignments.length}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
const md5 = require('md5');
|
||||
|
||||
export default class LoginUser
|
||||
{
|
||||
id: number;
|
||||
schoolPersonPk: number;
|
||||
username: string;
|
||||
lastLogin: Date;
|
||||
firstLogin: Date;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
graduationYear: string;
|
||||
groups: string;
|
||||
emails: string[];
|
||||
classes: string[];
|
||||
birthday: string;
|
||||
avatarUrl: string;
|
||||
token: string;
|
||||
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id = json.id;
|
||||
this.schoolPersonPk = json.schoolPersonPk;
|
||||
this.username = json.username;
|
||||
this.lastLogin = new Date(json.lastLogin);
|
||||
this.firstLogin = new Date(json.firstLogin);
|
||||
this.firstName = json.firstName;
|
||||
this.lastName = json.lastName;
|
||||
this.nickname = json.nickname;
|
||||
this.graduationYear = json.graduationYear;
|
||||
this.groups = json.groups;
|
||||
this.emails = json.emails.split('|').map((e: any) => e.toLowerCase().trim());
|
||||
this.classes = json.classes.split('|');
|
||||
this.birthday = json.birthday;
|
||||
this.avatarUrl = json.avatarUrl;
|
||||
this.token = json.token;
|
||||
|
||||
// Generate default avatar
|
||||
if (this.avatarUrl == null || this.avatarUrl == '')
|
||||
{
|
||||
this.avatarUrl = `https://www.gravatar.com/avatar/${md5(this.emails[0])}?d=404` + encodeURIComponent(
|
||||
`https://ui-avatars.com/api/${this.firstName.charAt(0)}${this.lastName.charAt(0)}/128`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
export default class CacheUtils
|
||||
{
|
||||
map: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* Get a cached value, or if not cached, cache it.
|
||||
*
|
||||
* @param name Name of the cached value
|
||||
* @param callback Callback function
|
||||
*/
|
||||
public get(name: string, callback: () => any)
|
||||
{
|
||||
if (!this.map.has(name))
|
||||
{
|
||||
this.map.set(name, callback());
|
||||
}
|
||||
return this.map.get(name);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import Course from '@/logic/course';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Constants from '@/constants';
|
||||
|
||||
const LEVEL_AP = {level: 'AP', scaleUp: 1};
|
||||
const LEVEL_H = {level: 'H', scaleUp: 0.75};
|
||||
@@ -65,4 +66,24 @@ export class CourseUtils
|
||||
// Really unknown
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the begin date of the selected term
|
||||
*/
|
||||
static getTermBeginDate()
|
||||
{
|
||||
let selected = Navigation.instance.getSelectedTerm();
|
||||
|
||||
return selected == -1 ? Constants.TERMS[0] : Constants.TERMS[selected];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end date of the selected term
|
||||
*/
|
||||
static getTermEndDate()
|
||||
{
|
||||
let selected = Navigation.instance.getSelectedTerm();
|
||||
|
||||
return selected == -1 ? Constants.TERMS[4] : Constants.TERMS[selected + 1];
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export class FormatUtils
|
||||
{
|
||||
/**
|
||||
* Convert date format to yyyy-mm-dd
|
||||
*
|
||||
* @param _date Date
|
||||
*/
|
||||
public static toChartDate(_date: string | Date)
|
||||
{
|
||||
// Convert to Date
|
||||
let date: Date = _date instanceof Date ? _date : new Date(_date);
|
||||
|
||||
// Convert to yyyy-mm-dd
|
||||
return moment(date).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit string length
|
||||
*
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
export function findLastIndex<T>(array: T[], callback: (v: T) => boolean): number
|
||||
{
|
||||
let arr2 = array.slice().reverse();
|
||||
let result = arr2.findIndex(callback);
|
||||
return result == -1 ? -1 : arr2.length - result - 1;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
import Course from '@/logic/course';
|
||||
import {Assignment} from '@/components/app/app';
|
||||
import Course, {Assignment, Grading} from '@/logic/course';
|
||||
|
||||
export interface Scale
|
||||
{
|
||||
@@ -50,7 +48,7 @@ export class GPAUtils
|
||||
{
|
||||
accurate = false;
|
||||
}
|
||||
else if (course.level != 'none')
|
||||
else if (course.level != 'none' && !isNaN(course.numericGrade))
|
||||
{
|
||||
courses.push(course);
|
||||
}
|
||||
@@ -67,7 +65,7 @@ export class GPAUtils
|
||||
let maxTotal = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course, course.letterGrade);
|
||||
totalGPA += this.getGP(course, course.numericGrade);
|
||||
maxTotal += this.getGP(course, 'A+');
|
||||
});
|
||||
|
||||
@@ -85,7 +83,7 @@ export class GPAUtils
|
||||
* @param course Course
|
||||
* @param letterGrade Letter grade
|
||||
*/
|
||||
public static getGP(course: Course, letterGrade: string): number
|
||||
public static getGP(course: Course, letterGrade: string | number): number
|
||||
{
|
||||
// Get scale
|
||||
let scale = this.findScale(letterGrade);
|
||||
@@ -137,17 +135,16 @@ export class GPAUtils
|
||||
});
|
||||
|
||||
// Return
|
||||
return score / max * 100;
|
||||
return +(score / max * 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the percent type
|
||||
* TODO: Combine it with overall-line
|
||||
*
|
||||
* @param course
|
||||
* @param grading
|
||||
* @param assignments
|
||||
*/
|
||||
public static getPercentTypeAverage(course: Course, assignments: Assignment[])
|
||||
public static getPercentTypeAverage(grading: Grading, assignments: Assignment[])
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
@@ -170,11 +167,11 @@ export class GPAUtils
|
||||
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
|
||||
// Without total percentage, the avg grade I get is 25%.
|
||||
let totalPercentage = 0;
|
||||
for (let type in course.grading.weightingMap)
|
||||
for (let type in grading.weightingMap)
|
||||
{
|
||||
if (typeScores[type] != undefined)
|
||||
{
|
||||
totalPercentage += course.grading.weightingMap[type];
|
||||
totalPercentage += grading.weightingMap[type];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,11 +179,11 @@ export class GPAUtils
|
||||
let score = 0;
|
||||
for (let type in typeScores)
|
||||
{
|
||||
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
|
||||
let typeFactor = grading.weightingMap[type] / totalPercentage;
|
||||
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
return score * 100;
|
||||
return +(score * 100).toFixed(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import Constants from '@/constants';
|
||||
|
||||
export default class GraphUtils
|
||||
{
|
||||
static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
|
||||
|
||||
/**
|
||||
* Base settings
|
||||
*
|
||||
* @param title
|
||||
* @param subtitle
|
||||
*/
|
||||
static getBaseSettings(title?: String, subtitle?: String)
|
||||
{
|
||||
return {
|
||||
// Color
|
||||
color: Constants.THEME.colors,
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: title != null,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 13
|
||||
},
|
||||
text: title,
|
||||
subtext: subtitle,
|
||||
x: 'center'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get term mark lines
|
||||
*/
|
||||
static getTermLines()
|
||||
{
|
||||
return {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {color: Constants.THEME.colors[2]},
|
||||
animationDuration: 500,
|
||||
data: Constants.TERMS.map((term, index) =>
|
||||
{
|
||||
return {xAxis: term.getTime(), label: {formatter: `Term ${index + 1}`}}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mark areas for percentage scores
|
||||
*/
|
||||
static getGradeMarkAreas(opacity: number)
|
||||
{
|
||||
return {
|
||||
silent: true,
|
||||
data:
|
||||
[
|
||||
// Above 100
|
||||
[{itemStyle: {color: 'rgba(230,253,255)', opacity: opacity}, yAxis: 120}, {yAxis: 100}],
|
||||
// 90 to 100
|
||||
[{itemStyle: {color: 'rgba(241,255,237)', opacity: opacity}, yAxis: 100}, {yAxis: 90}],
|
||||
// 80 to 90
|
||||
[{itemStyle: {color: 'rgba(255,250,216)', opacity: opacity}, yAxis: 90}, {yAxis: 80}],
|
||||
// 70 to 80
|
||||
[{itemStyle: {color: 'rgba(255,225,199)', opacity: opacity}, yAxis: 80}, {yAxis: 70}],
|
||||
// Below 70 (Fail)
|
||||
[{itemStyle: {color: 'rgb(255,190,184)', opacity: opacity}, yAxis: 70}, {yAxis: -100}]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text style for pie graphs or radar graphs
|
||||
*/
|
||||
static pieTextStyle()
|
||||
{
|
||||
return {
|
||||
fontSize: 14,
|
||||
textShadowColor: '#cfcfcf',
|
||||
textShadowBlur: 2,
|
||||
textShadowOffsetX: 1,
|
||||
textShadowOffsetY: 1,
|
||||
backgroundColor: '#f6f6f6',
|
||||
borderRadius: 3,
|
||||
padding: [3, 5]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS shadow string (extraCssText) for tooltip
|
||||
*/
|
||||
static tooltipCssShadow()
|
||||
{
|
||||
return {extraCssText: 'box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);'}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import Constants from '@/constants';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
|
||||
export class HttpUtils
|
||||
{
|
||||
public token: string = '';
|
||||
public user: LoginUser;
|
||||
|
||||
public post(node: string, body: any): Promise<any>
|
||||
{
|
||||
// Add token
|
||||
if (this.token != '') body['token'] = this.token;
|
||||
if (this.user != null) body['token'] = this.user.token;
|
||||
|
||||
// Create promise
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
@@ -2,8 +2,8 @@
|
||||
<div id="assignment-type-head">
|
||||
<el-card :body-style="{padding: '0px'}">
|
||||
<div id="type-info-card">
|
||||
<span id="type-name">{{typeName}}</span>
|
||||
<span id="type-average">Average: {{average.toFixed(2)}}%</span>
|
||||
<span id="type-name">{{type.name}}</span>
|
||||
<span id="type-average">Average: {{type.percent}}%</span>
|
||||
</div>
|
||||
|
||||
<AssignmentEntry v-for="assignment of filteredAssignments" :key="assignment.id"
|
||||
@@ -16,32 +16,21 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment} from '@/components/app/app';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import {Assignment, AssignmentType} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry}
|
||||
})
|
||||
export default class AssignmentTypeHead extends Vue
|
||||
{
|
||||
@Prop({required: true}) typeName: string;
|
||||
@Prop({required: true}) type: AssignmentType;
|
||||
@Prop({required: true}) assignments: Assignment[];
|
||||
|
||||
filteredAssignments: Assignment[];
|
||||
|
||||
/**
|
||||
* Called when this component is created
|
||||
*/
|
||||
created()
|
||||
get filteredAssignments()
|
||||
{
|
||||
// Filter assignments to only this type
|
||||
this.filteredAssignments = this.assignments.filter(a => a.type == this.typeName);
|
||||
}
|
||||
|
||||
get average()
|
||||
{
|
||||
return this.filteredAssignments.reduce((a, b) => a + b.score, 0) /
|
||||
this.filteredAssignments.reduce((a, b) => a + b.scoreMax, 0) * 100;
|
||||
return this.assignments.filter(a => a.typeId == this.type.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,3 +18,9 @@
|
||||
// https://stackoverflow.com/questions/17572619/inset-box-shadow-only-on-one-side
|
||||
box-shadow: inset 0 7px 9px -7px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.type-graph
|
||||
{
|
||||
padding-top: 23px;
|
||||
height: 420px !important;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
<template>
|
||||
<el-card id="course-card" class="course-card">
|
||||
<course-head :clickable="false" :course="course" :unread="countUnread()"></course-head>
|
||||
<course-head :clickable="false" :course="course" :unread="countUnread()"/>
|
||||
|
||||
<div class="course-card-content expand">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-col :span="24" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center">
|
||||
<course-scatter :course="course"></course-scatter>
|
||||
<course-scatter :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="0">
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AssignmentTypeHead v-for="type in getAssignmentTypes()" :key="type"
|
||||
:type-name="type" :assignments="course.assignments">
|
||||
<el-row>
|
||||
<el-col :span="12" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center type-graph"
|
||||
body-style="padding: 0">
|
||||
<TypeRadar :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center type-graph"
|
||||
body-style="padding: 0">
|
||||
<TypePie :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AssignmentTypeHead v-for="type in course.assignmentTypes" :key="type.id"
|
||||
:type="type" :assignments="course.assignments">
|
||||
</AssignmentTypeHead>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -23,15 +35,16 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment} from '@/components/app/app';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import AssignmentTypeHead from '@/pages/course/assignment-type-head/assignment-type-head.vue';
|
||||
import Course from '@/logic/course';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import TypeRadar from '@/pages/course/type-radar/type-radar';
|
||||
import TypePie from '@/pages/course/type-pie/type-pie';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
|
||||
components: {TypeRadar, TypePie, AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
|
||||
})
|
||||
export default class CoursePage extends Vue
|
||||
{
|
||||
@@ -52,22 +65,7 @@
|
||||
}
|
||||
else return this.unread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the types of the assignments.
|
||||
*/
|
||||
getAssignmentTypes(): string[]
|
||||
{
|
||||
// Get all types
|
||||
let types = this.course.assignments.map(a => a.type);
|
||||
|
||||
// Remove duplicates
|
||||
types = types.filter((type, i, a) => a.indexOf(type) == i);
|
||||
|
||||
// Return it
|
||||
return types;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-page.scss" lang="scss" scoped></style>
|
||||
<style src="./course-page.scss" lang="scss" scoped/>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment} from '@/components/app/app';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import moment from 'moment';
|
||||
import Course from '@/logic/course';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
@Component({
|
||||
})
|
||||
@Component
|
||||
export default class CourseScatter extends Vue
|
||||
{
|
||||
private static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
|
||||
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
@@ -28,40 +26,11 @@ export default class CourseScatter extends Vue
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
// Map assignments
|
||||
let map = this.mapAssignments();
|
||||
|
||||
// Scatter data point style
|
||||
let itemStyle =
|
||||
{
|
||||
normal:
|
||||
{
|
||||
opacity: 0.8,
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
};
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
// Color
|
||||
color: Constants.THEME.colors,
|
||||
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: true,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 13
|
||||
},
|
||||
text: 'Assignments',
|
||||
subtext: 'Assignment scores for ' + this.course.name,
|
||||
x: 'center'
|
||||
},
|
||||
// Base settings
|
||||
...GraphUtils.getBaseSettings('Assignments', 'Assignment scores for ' + this.course.name),
|
||||
|
||||
// X axis represents course names
|
||||
xAxis:
|
||||
@@ -71,7 +40,7 @@ export default class CourseScatter extends Vue
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
max: FormatUtils.toChartDate(new Date())
|
||||
max: new Date().getTime()
|
||||
},
|
||||
|
||||
// Y axis represents GPAs and MaxGPAs
|
||||
@@ -85,21 +54,23 @@ export default class CourseScatter extends Vue
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
max: 100,
|
||||
min: (value: any) => Math.floor(value.min) - 5
|
||||
min: (value: any) => Math.floor(value.min) - 5,
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// Tooltip
|
||||
tooltip:
|
||||
{
|
||||
...GraphUtils.tooltipCssShadow(),
|
||||
|
||||
trigger: 'axis',
|
||||
axisPointer:
|
||||
{
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: (ps: any[]) => ps[0].data[0] + '<br>' + ps.map(p =>
|
||||
`${CourseScatter.DOT.replace('{color}', p.color)}
|
||||
${FormatUtils.limit(p.data[2], 22)}: ${p.data[1]}%<br>`).join('')
|
||||
formatter: (ps: any[]) => moment(ps[0].data[0]).format('MMM DD, YYYY') + '<br>' + ps.map(p =>
|
||||
`${GraphUtils.DOT.replace('{color}', p.color.colorStops[1].color)}
|
||||
${FormatUtils.limit(p.data[2].description, 22)}: ${p.data[1]}%<br>`).join('')
|
||||
},
|
||||
|
||||
// Legend
|
||||
@@ -115,39 +86,70 @@ export default class CourseScatter extends Vue
|
||||
},
|
||||
|
||||
// Data
|
||||
series: Array.from(map, ([type, assignments]) =>
|
||||
{
|
||||
return {
|
||||
type: 'scatter',
|
||||
name: type,
|
||||
data: CourseScatter.assignmentsData(assignments),
|
||||
itemStyle: itemStyle
|
||||
}
|
||||
})
|
||||
series: this.series()
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map assignments to {assignmentType, [assignment]} format.
|
||||
* Get series data
|
||||
*/
|
||||
private mapAssignments(): Map<string, Assignment[]>
|
||||
private series()
|
||||
{
|
||||
// Define map
|
||||
let map = new Map();
|
||||
|
||||
// Move data to map
|
||||
this.course.assignments.forEach(a =>
|
||||
// Create scatter plots
|
||||
let series: any[] = this.course.assignmentTypes.map((type, i) =>
|
||||
{
|
||||
// Null case, create empty array
|
||||
if (!map.has(a.type)) map.set(a.type, []);
|
||||
return {
|
||||
type: 'scatter',
|
||||
name: type.name,
|
||||
data: CourseScatter.assignmentsData(this.course.assignments.filter(a => a.typeId == type.id)),
|
||||
symbolSize: (data: any) => Math.max(Math.sqrt(type.weight * data[2].scoreMax / type.scoreMax) * 12, 12),
|
||||
|
||||
// Put data
|
||||
map.get(a.type).push(a);
|
||||
label:
|
||||
{
|
||||
emphasis:
|
||||
{
|
||||
show: true,
|
||||
formatter: (p: any) => p.data[2].description,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
|
||||
itemStyle:
|
||||
{
|
||||
normal:
|
||||
{
|
||||
opacity: 0.7,
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
color:
|
||||
{
|
||||
type: 'radial',
|
||||
x: 0.4,
|
||||
y: 0.3,
|
||||
colorStops:
|
||||
[
|
||||
{offset: 0, color: chroma(Constants.THEME.colors[i]).set('hsl.l', 0.9).css()},
|
||||
{offset: 1, color: Constants.THEME.colors[i]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,6 +160,6 @@ export default class CourseScatter extends Vue
|
||||
private static assignmentsData(assignments: Assignment[])
|
||||
{
|
||||
return assignments.filter(a => a.complete == 'Complete')
|
||||
.map(a => [FormatUtils.toChartDate(a.date), (a.score / a.scoreMax * 100).toFixed(2), a.description]);
|
||||
.map(a => [a.time, (a.score / a.scoreMax * 100).toFixed(2), a]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
<template>
|
||||
<div id="course-scatter">
|
||||
<ve-scatter height="450px" class="graph" :extend="{heyIUsedCourseObject: this.course.name}" :after-config="afterConfig"></ve-scatter>
|
||||
<ve-scatter height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./course-scatter.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
#overall-bar
|
||||
{
|
||||
.graph
|
||||
{
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import Course from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
|
||||
@Component
|
||||
export default class TypePie extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.chartSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Assignment Type Weight',
|
||||
'How much each type of assignment affect your average'),
|
||||
|
||||
// Data
|
||||
series:
|
||||
{
|
||||
type: 'pie',
|
||||
avoidLabelOverlap: false,
|
||||
radius: ['40%', '60%'],
|
||||
center: ['50%', '55%'],
|
||||
label: GraphUtils.pieTextStyle(),
|
||||
data: this.course.assignmentTypes.map((t, i) => {return {
|
||||
value: t.weight,
|
||||
name: `${t.name}\n${t.weight}%`,
|
||||
itemStyle:
|
||||
{
|
||||
color: Constants.THEME.colors[i],
|
||||
opacity: 0.8,
|
||||
shadowColor: 'rgba(0,0,0,0.22)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
}}).sort((a, b) => a.value - b.value)
|
||||
}
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div id="type-pie">
|
||||
<ve-pie height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-pie>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./type-pie.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,102 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import Course from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
|
||||
@Component
|
||||
export default class TypeRadar extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.chartSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let min = this.course.assignmentTypes.reduce((min, t) => Math.min(min, t.percent), 100);
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Assignment Type Radar',
|
||||
'How are you doing for different types of assignment'),
|
||||
|
||||
// Radar settings
|
||||
radar:
|
||||
{
|
||||
// shape: 'circle',
|
||||
name:
|
||||
{
|
||||
textStyle: GraphUtils.pieTextStyle()
|
||||
},
|
||||
splitArea:
|
||||
{
|
||||
areaStyle:
|
||||
{
|
||||
color:
|
||||
[
|
||||
'rgb(255,161,151)',
|
||||
'rgb(255,190,184)',
|
||||
'rgba(255,225,199)',
|
||||
'rgba(255,250,216)',
|
||||
'rgba(241,255,237)',
|
||||
],
|
||||
opacity: 0.4
|
||||
}
|
||||
},
|
||||
indicator: this.course.assignmentTypes.map((t, i) => {return {
|
||||
name: `${t.name}\n${t.percent}%`,
|
||||
max: 100,
|
||||
min: min - 30,
|
||||
color: Constants.THEME.colors[i]
|
||||
}}),
|
||||
radius: '60%',
|
||||
center: ['50%', '55%']
|
||||
},
|
||||
|
||||
// Data
|
||||
series:
|
||||
{
|
||||
type: 'radar',
|
||||
data:
|
||||
[
|
||||
{
|
||||
name: 'Score',
|
||||
symbol: 'circle',
|
||||
areaStyle:
|
||||
{
|
||||
color:
|
||||
{
|
||||
type: 'radial',
|
||||
x: 0.5, y: 0.55, r: 0.5,
|
||||
colorStops:
|
||||
[
|
||||
{offset: 0, color: '#ffa0a0'},
|
||||
{offset: 0.5, color: '#fffead'},
|
||||
{offset: 1, color: '#d1ffde'}
|
||||
],
|
||||
global: false // 缺省为 false
|
||||
},
|
||||
opacity: 0.2
|
||||
},
|
||||
value: this.course.assignmentTypes.map(t => t.percent)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
color: '#6771c1'
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div id="type-radar">
|
||||
<ve-radar height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./type-radar.ts" lang="ts"></script>
|
||||
@@ -1,11 +1,10 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
|
||||
@Component({
|
||||
})
|
||||
@Component
|
||||
export default class OverallBar extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div id="overall-bar">
|
||||
<ve-bar height="450px" class="graph"
|
||||
:extend="chartSettings"></ve-bar>
|
||||
<ve-bar height="450px" class="graph" :extend="chartSettings"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
<el-row class="unread-row">
|
||||
<el-col :span="3" class="date">
|
||||
<span class="month">{{getMoment(assignment.date).format("MMM D")}}</span>
|
||||
<span class="now">({{getMoment(assignment.date).fromNow()}})</span>
|
||||
<span class="month">{{getMoment(assignment.time).format("MMM D")}}</span>
|
||||
<span class="now">({{getMoment(assignment.time).fromNow()}})</span>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="15" class="description">
|
||||
@@ -36,8 +36,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment} from '@/components/app/app';
|
||||
import moment from 'moment';
|
||||
import {Assignment} from '@/logic/course';
|
||||
|
||||
@Component
|
||||
export default class AssignmentEntry extends Vue
|
||||
@@ -53,7 +53,7 @@
|
||||
*
|
||||
* @param date Date
|
||||
*/
|
||||
getMoment(date: string)
|
||||
getMoment(date: number)
|
||||
{
|
||||
return moment(new Date(date));
|
||||
}
|
||||
@@ -68,4 +68,4 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assignment-entry.scss" lang="scss" scoped></style>
|
||||
<style src="./assignment-entry.scss" lang="scss" scoped/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="course-head" class="course-card-content main vertical-center">
|
||||
<div id="course-head" class="course-card-content main vertical-center" @click="redirect">
|
||||
<el-row>
|
||||
<el-col :span="12" class="course-col-name">
|
||||
<div v-if="clickable" class="course-name clickable" @click="redirect">{{course.name}}</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component
|
||||
@@ -49,4 +49,4 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-head.scss" lang="scss"></style>
|
||||
<style src="./course-head.scss" lang="scss"/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import App, {Assignment} from '@/components/app/app';
|
||||
import App from '@/components/app/app';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import Course from '@/logic/course';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {UnreadEntry: AssignmentEntry, CourseHead}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="overall-course">
|
||||
<el-card class="course-card">
|
||||
<course-head :clickable="true" :course="course" :unread="countUnread()"></course-head>
|
||||
<course-head :clickable="true" :course="course" :unread="countUnread()"/>
|
||||
<div class="course-card-content expand"
|
||||
v-if="countUnread() !== 0">
|
||||
<unread-entry v-for="assignment in unreadAssignments"
|
||||
@@ -16,4 +16,4 @@
|
||||
</template>
|
||||
|
||||
<script src="./overall-course.ts" lang="ts"></script>
|
||||
<style src="./overall-course.scss" lang="scss" scoped></style>
|
||||
<style src="./overall-course.scss" lang="scss" scoped/>
|
||||
|
||||
@@ -1,180 +1,180 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import moment from 'moment';
|
||||
import Course from '@/logic/course';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component({
|
||||
})
|
||||
@Component
|
||||
export default class OverallLine extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
private settings =
|
||||
{
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: true,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 12
|
||||
},
|
||||
text: 'Average Grade',
|
||||
subtext: 'Average score trend for every course',
|
||||
x: 'center'
|
||||
},
|
||||
// Legend
|
||||
legend:
|
||||
{
|
||||
show: false,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 11
|
||||
},
|
||||
icon: 'circle'
|
||||
},
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
{
|
||||
startValue: moment().subtract(30, 'days').format('M/D/YYYY')
|
||||
},
|
||||
{
|
||||
type: 'inside'
|
||||
}
|
||||
],
|
||||
series:
|
||||
{
|
||||
smooth: true
|
||||
},
|
||||
xAxis:
|
||||
{
|
||||
//type: 'time'
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
min: (value: any) => Math.floor(value.min),
|
||||
max: (value: any) => value.max
|
||||
}
|
||||
};
|
||||
|
||||
chartCache: any;
|
||||
filteredCourses: Course[];
|
||||
settings: any;
|
||||
|
||||
/**
|
||||
* Convert assignments list to a graph dataset.
|
||||
* When this component is created
|
||||
*/
|
||||
get convertChart()
|
||||
created()
|
||||
{
|
||||
// Caching
|
||||
if (this.chartCache != undefined) return this.chartCache;
|
||||
// Filter courses
|
||||
this.filteredCourses = this.courses.filter(c => c.isGraded && c.assignments.length > 0);
|
||||
|
||||
let courses = this.courses.filter(c => c.assignments.length > 0);
|
||||
|
||||
// Compute the column names
|
||||
let columns = courses.map(course => course.name);
|
||||
columns.unshift('date');
|
||||
|
||||
// Find the min date
|
||||
let minDates = courses.map(course => course.assignments[course.assignments.length - 1].date.getTime());
|
||||
let minDate: Date = new Date(Math.min.apply(null, minDates));
|
||||
|
||||
// Find the dates in between
|
||||
let now = new Date();
|
||||
let dates = [];
|
||||
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1))
|
||||
// Generate settings
|
||||
this.settings =
|
||||
{
|
||||
dates.push(new Date(date));
|
||||
}
|
||||
...GraphUtils.getBaseSettings('Average Grade', 'Average score trend for every course'),
|
||||
|
||||
// Compute the rows data
|
||||
let rows: {[index: string]: any}[] = [];
|
||||
dates.forEach(date =>
|
||||
{
|
||||
// Define row object
|
||||
let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')};
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
{
|
||||
type: 'slider',
|
||||
startValue: this.getStartDate(),
|
||||
|
||||
// Loop through courses
|
||||
courses.forEach(course =>
|
||||
// Minimum zoom: 1 week
|
||||
minValueSpan: 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
],
|
||||
|
||||
// Tooltip
|
||||
tooltip:
|
||||
{
|
||||
// Total Mean
|
||||
if (course.grading.method == 'TOTAL_MEAN')
|
||||
... GraphUtils.tooltipCssShadow(),
|
||||
|
||||
trigger: 'axis'
|
||||
},
|
||||
|
||||
// Axis
|
||||
xAxis:
|
||||
{
|
||||
type: 'time',
|
||||
axisLabel:
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Date is being looked at
|
||||
if (assignment.date.getTime() < date.getTime())
|
||||
{
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
}
|
||||
});
|
||||
|
||||
// Add average to the row
|
||||
row[course.name] = score / max * 100;
|
||||
}
|
||||
else if (course.grading.method == 'PERCENT_TYPE')
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
axisLabel:
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
min: (value: any) => Math.floor(value.min),
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Date is being looked at
|
||||
if (assignment.date.getTime() < date.getTime())
|
||||
{
|
||||
// Record scores
|
||||
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
|
||||
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
|
||||
|
||||
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
|
||||
typeCounts[assignment.type] ++;
|
||||
}
|
||||
});
|
||||
|
||||
// Count total percentage (This is to avoid less than expected cases)
|
||||
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
|
||||
// Without total percentage, the avg grade I get is 25%.
|
||||
let totalPercentage = 0;
|
||||
for (let type in course.grading.weightingMap)
|
||||
{
|
||||
if (typeScores[type] != undefined)
|
||||
{
|
||||
totalPercentage += course.grading.weightingMap[type];
|
||||
}
|
||||
}
|
||||
|
||||
// Count
|
||||
let score = 0;
|
||||
for (let type in typeScores)
|
||||
{
|
||||
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
|
||||
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
if (score != 0) row[course.name] = score * 100;
|
||||
}
|
||||
});
|
||||
|
||||
// Add it to the array
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
return this.chartCache =
|
||||
{
|
||||
columns: columns,
|
||||
rows: rows
|
||||
// Series data
|
||||
series: this.series()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starting date
|
||||
*/
|
||||
private getStartDate()
|
||||
{
|
||||
// If it's a past term, use the term's end date, else use today.
|
||||
let selected = Navigation.instance.getSelectedTerm();
|
||||
let end = selected == Constants.CURRENT_TERM || selected == -1
|
||||
? moment() : moment(CourseUtils.getTermEndDate());
|
||||
|
||||
return Math.max(end.subtract(30, 'days').toDate().getTime(),
|
||||
CourseUtils.getTermBeginDate().getTime())
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate series data
|
||||
*/
|
||||
private series()
|
||||
{
|
||||
// Each course
|
||||
let series: any[] = this.filteredCourses.map(course => this.getCourseSeries(course));
|
||||
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate series data for a course
|
||||
*
|
||||
* @param course
|
||||
*/
|
||||
private getCourseSeries(course: Course)
|
||||
{
|
||||
// Graded assignments
|
||||
let assignments = course.assignments.slice().reverse();
|
||||
|
||||
// Create series
|
||||
return {
|
||||
name: course.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle', // circle, diamond, emptyCircle, none
|
||||
data: this.toDateRange([...new Set(assignments.map(a => a.time))].map(time =>
|
||||
{
|
||||
// Find subset before this assignment
|
||||
let subset = course.getAssignmentsBefore(time);
|
||||
|
||||
// Find grade
|
||||
if (course.termGrading[subset.term].method == 'PERCENT_TYPE')
|
||||
return [time, GPAUtils.getPercentTypeAverage(course.termGrading[subset.term], subset.assignments)];
|
||||
else return [time, GPAUtils.getTotalMeanAverage(subset.assignments)];
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert point data to date range data.
|
||||
* Eg. [[Mon, 10], [Wed, 5]] to [[Mon, 10], [Tue, 10], [Wed, 5]]
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
private toDateRange(data: any[])
|
||||
{
|
||||
// Find the min date
|
||||
let minDate: Date = new Date(data[0][0]);
|
||||
|
||||
// Find the dates in between
|
||||
let now = new Date(Math.min(new Date().getTime(), CourseUtils.getTermEndDate().getTime()));
|
||||
let times: number[] = [];
|
||||
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1)) times.push(date.getTime());
|
||||
|
||||
// Map the points
|
||||
let lastValue: any = null;
|
||||
return times.map(time =>
|
||||
{
|
||||
// Data point on this specific date
|
||||
let thisValue = data.find(a => a[0] == time);
|
||||
|
||||
// Switching terms
|
||||
if (Constants.TERMS.find(t => t.getTime() == time))
|
||||
lastValue = null;
|
||||
|
||||
// Find value
|
||||
return thisValue == null
|
||||
? lastValue == null ? null : [time, lastValue[1]]
|
||||
: [time, (lastValue = thisValue)[1]];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="overall-line">
|
||||
<ve-line :data="convertChart" :extend="settings"></ve-line>
|
||||
<ve-line :extend="{a: this.courses}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,6 +34,17 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-grade
|
||||
{
|
||||
font-size: 30px;
|
||||
color: #b1b1b1;
|
||||
|
||||
// Disable selecting
|
||||
display:block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Cards
|
||||
.el-card.overall-bar-card
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<el-card class="large gpa-card vertical-center">
|
||||
<el-row v-if="getGPA().gpa !== -1">
|
||||
<el-col :span="4" class="overall-span">
|
||||
<el-card class="large gpa-card vertical-center" body-style="padding: 0">
|
||||
<div style="padding: 14px;">
|
||||
<span class="gpa header">GPA:</span>
|
||||
<span class="gpa text">{{getGPA().gpa}}</span>
|
||||
@@ -13,18 +13,24 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-card class="large overall-line-card vertical-center">
|
||||
<overall-line :courses="courses"></overall-line>
|
||||
<el-col :span="14" class="overall-span">
|
||||
<el-card class="large overall-line-card vertical-center" body-style="padding: 0 10px">
|
||||
<overall-line :courses="courses"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="large overall-bar-card vertical-center">
|
||||
<overall-bar :courses="courses"></overall-bar>
|
||||
<el-col :span="6" class="overall-span">
|
||||
<el-card class="large overall-bar-card vertical-center" body-style="padding: 0 10px">
|
||||
<overall-bar :courses="courses"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-if="getGPA().gpa === -1">
|
||||
<el-card class="large gpa-card vertical-center">
|
||||
<div class="no-grade">This quarter has no grades yet...</div>
|
||||
</el-card>
|
||||
</el-row>
|
||||
|
||||
<overall-course v-for="course in courses"
|
||||
:course="course"
|
||||
:key="course.id">
|
||||
@@ -38,7 +44,7 @@
|
||||
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
|
||||
import OverallCourse from '@/pages/overall/overall-course/overall-course';
|
||||
import Course from '@/logic/course';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {OverallLine, OverallBar, OverallCourse}
|
||||
@@ -58,4 +64,4 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./overall.scss" lang="scss" scoped></style>
|
||||
<style src="./overall.scss" lang="scss" scoped/>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import {Assignment} from '@/components/app/app';
|
||||
|
||||
export default class JsonUtils
|
||||
{
|
||||
/**
|
||||
* This method filters the information provided in an assignments json.
|
||||
*
|
||||
* @param assignments Assignments object
|
||||
* @returns Assignment[] Filtered assignment grade object list
|
||||
*/
|
||||
public static filterAssignments(assignments: any): Assignment[]
|
||||
{
|
||||
return assignments.assignments.map((assignment: any) =>
|
||||
{
|
||||
return {
|
||||
id: assignment.assignment_id,
|
||||
scoreId: assignment.score_id,
|
||||
type: assignment.assignment_type,
|
||||
typeId: assignment.assignment_type_id,
|
||||
description: assignment.assignment_description,
|
||||
date: new Date(assignment._date),
|
||||
complete: assignment.completion_status,
|
||||
include: assignment.include_in_calculated_grade == 1,
|
||||
display: assignment.display_grade == 1,
|
||||
|
||||
unread: assignment.is_unread == 1,
|
||||
|
||||
scoreMax: assignment.maximum_score,
|
||||
score: +assignment.raw_score,
|
||||
|
||||
gradingPeriod: +assignment.grading_period.replace('Quarter ', '') - 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -15,9 +15,7 @@
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
|
||||
Reference in New Issue
Block a user